You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2022/06/12 20:34:57 UTC

[calcite] branch main updated: [CALCITE-5143] Allow custom time unit abbreviations in FLOOR, CEIL, EXTRACT, DATE_PART, DATEADD, DATEDIFF and similar functions

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 75d064f65 [CALCITE-5143] Allow custom time unit abbreviations in FLOOR, CEIL, EXTRACT, DATE_PART, DATEADD, DATEDIFF and similar functions
75d064f65 is described below

commit 75d064f659a038371d84f9fdba7847e1631d1f1d
Author: xiejiajun <ji...@foxmail.com>
AuthorDate: Thu May 12 09:13:58 2022 +0800

    [CALCITE-5143] Allow custom time unit abbreviations in FLOOR, CEIL, EXTRACT, DATE_PART, DATEADD, DATEDIFF and similar functions
    
    Time unit abbreviations are specified in
    SqlParser.Config.timeUnitCodes().
    
    Since [CALCITE-2496], EXTRACT supports MICROSECOND and
    NANOSECOND by default but FLOOR and CEIL do not. With this
    change, you could configure FLOOR and CEIL to also support
    those units.
    
    Close apache/calcite#2807
---
 .../org/apache/calcite/test/BabelParserTest.java   |  30 ++++++
 core/src/main/codegen/templates/Parser.jj          | 102 +++++++++++++++------
 .../calcite/sql/parser/SqlAbstractParserImpl.java  |   7 ++
 .../org/apache/calcite/sql/parser/SqlParser.java   |  28 ++++++
 .../apache/calcite/sql/parser/SqlParserTest.java   |  94 ++++++++++++++++++-
 .../org/apache/calcite/test/SqlOperatorTest.java   |  10 +-
 6 files changed, 235 insertions(+), 36 deletions(-)

diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
index 777e34aa0..3fafb4899 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.test;
 
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.dialect.MysqlSqlDialect;
 import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
@@ -34,6 +35,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 
 import static org.hamcrest.CoreMatchers.is;
@@ -184,6 +186,34 @@ class BabelParserTest extends SqlParserTest {
     sql(sql).ok(expected);
   }
 
+  /** Overrides, adding tests for DATEADD, DATEDIFF, DATE_PART functions
+   * in addition to EXTRACT. */
+  @Override protected void checkTimeUnitCodes(
+      Map<String, TimeUnit> timeUnitCodes) {
+    super.checkTimeUnitCodes(timeUnitCodes);
+
+    SqlParserFixture f = fixture()
+        .withConfig(config -> config.withTimeUnitCodes(timeUnitCodes));
+
+    timeUnitCodes.forEach((abbrev, timeUnit) -> {
+      String sql = "SELECT "
+          + "DATEADD(" + abbrev + ", 1, '2022-06-03 15:30:00.000'),"
+          + "DATEDIFF(" + abbrev + ", '2021-06-03 12:00:00.000', '2022-06-03 15:30:00.000'),"
+          + "DATE_PART(" + abbrev + ", '2022-06-03 15:30:00.000')";
+      String expected = "SELECT "
+          + "`DATEADD`(" + timeUnit + ", 1, '2022-06-03 15:30:00.000'), "
+          + "`DATEDIFF`(" + timeUnit + ", '2021-06-03 12:00:00.000', '2022-06-03 15:30:00.000'), "
+          + "`DATE_PART`(" + timeUnit + ", '2022-06-03 15:30:00.000')";
+      f.sql(sql).ok(expected);
+    });
+    f.sql("SELECT DATEADD(^A^, 1, NOW())")
+        .fails("'A' is not a valid datetime format");
+    if (timeUnitCodes.containsKey("S")) {
+      f.sql("SELECT DATEADD(S^.^A, 1, NOW())")
+          .fails("(?s).*Encountered \".\" at .*");
+    }
+  }
+
   /** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
    * date part. */
   @Test void testShortTimestampLiteral() {
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 6b26d1066..d216a6af9 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -127,6 +127,7 @@ import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import org.slf4j.Logger;
 
 import java.io.Reader;
@@ -137,6 +138,7 @@ import java.util.Calendar;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 
 import static org.apache.calcite.util.Static.RESOURCE;
 
@@ -161,6 +163,7 @@ public class ${parser.class} extends SqlAbstractParserImpl
     private Casing unquotedCasing;
     private Casing quotedCasing;
     private int identifierMaxLength;
+    private ImmutableMap<String, TimeUnit> timeUnitCodes;
     private SqlConformance conformance;
 
     /**
@@ -222,6 +225,10 @@ public class ${parser.class} extends SqlAbstractParserImpl
         this.identifierMaxLength = identifierMaxLength;
     }
 
+    public void setTimeUnitCodes(Map<String, TimeUnit> timeUnitCodes) {
+        this.timeUnitCodes = ImmutableMap.copyOf(timeUnitCodes);
+    }
+
     public void setConformance(SqlConformance conformance) {
         this.conformance = conformance;
     }
@@ -5037,29 +5044,72 @@ SqlIntervalQualifier IntervalQualifierStart() :
 }
 
 /**
- * Parses time unit for EXTRACT, CEIL and FLOOR functions.
- * Note that it does't include NANOSECOND and MICROSECOND.
+ * Parses time unit for CEIL and FLOOR functions.
  */
 TimeUnit TimeUnit() :
-{}
 {
-    <MILLISECOND> { return TimeUnit.MILLISECOND; }
-|   <SECOND> { return TimeUnit.SECOND; }
-|   <MINUTE> { return TimeUnit.MINUTE; }
-|   <HOUR> { return TimeUnit.HOUR; }
-|   <DAY> { return TimeUnit.DAY; }
-|   <DOW> { return TimeUnit.DOW; }
-|   <DOY> { return TimeUnit.DOY; }
-|   <ISODOW> { return TimeUnit.ISODOW; }
-|   <ISOYEAR> { return TimeUnit.ISOYEAR; }
-|   <WEEK> { return TimeUnit.WEEK; }
-|   <MONTH> { return TimeUnit.MONTH; }
-|   <QUARTER> { return TimeUnit.QUARTER; }
-|   <YEAR> { return TimeUnit.YEAR; }
-|   <EPOCH> { return TimeUnit.EPOCH; }
-|   <DECADE> { return TimeUnit.DECADE; }
-|   <CENTURY> { return TimeUnit.CENTURY; }
-|   <MILLENNIUM> { return TimeUnit.MILLENNIUM; }
+    final TimeUnit unit;
+}
+{
+    LOOKAHEAD(1)
+    (
+        <MILLISECOND> { return TimeUnit.MILLISECOND; }
+    |   <SECOND> { return TimeUnit.SECOND; }
+    |   <MINUTE> { return TimeUnit.MINUTE; }
+    |   <HOUR> { return TimeUnit.HOUR; }
+    |   <DAY> { return TimeUnit.DAY; }
+    |   <DOW> { return TimeUnit.DOW; }
+    |   <DOY> { return TimeUnit.DOY; }
+    |   <ISODOW> { return TimeUnit.ISODOW; }
+    |   <ISOYEAR> { return TimeUnit.ISOYEAR; }
+    |   <WEEK> { return TimeUnit.WEEK; }
+    |   <MONTH> { return TimeUnit.MONTH; }
+    |   <QUARTER> { return TimeUnit.QUARTER; }
+    |   <YEAR> { return TimeUnit.YEAR; }
+    |   <EPOCH> { return TimeUnit.EPOCH; }
+    |   <DECADE> { return TimeUnit.DECADE; }
+    |   <CENTURY> { return TimeUnit.CENTURY; }
+    |   <MILLENNIUM> { return TimeUnit.MILLENNIUM; }
+    )
+|
+    unit = TimeUnitIdentifier() { return unit; }
+}
+
+/**
+ * Parses time unit for the EXTRACT function.
+ * As for FLOOR and CEIL, but also includes NANOSECOND and MICROSECOND.
+ */
+TimeUnit TimeUnitForExtract() :
+{
+    final TimeUnit unit;
+}
+{
+    LOOKAHEAD(1)
+    (
+        <NANOSECOND> { return TimeUnit.NANOSECOND; }
+    |   <MICROSECOND> { return TimeUnit.MICROSECOND; }
+    )
+|
+    unit = TimeUnit() { return unit; }
+}
+
+/**
+ * Parses a simple identifier as a TimeUnit.
+ */
+TimeUnit TimeUnitIdentifier() :
+{
+    final List<String> names = new ArrayList<String>();
+    final List<SqlParserPos> positions = new ArrayList<SqlParserPos>();
+}
+{
+    IdentifierSegment(names, positions) {
+        TimeUnit unit = timeUnitCodes.get(names.get(0));
+        if (unit != null) {
+          return unit;
+        }
+        throw SqlUtil.newContextException(positions.get(0),
+            RESOURCE.invalidDatetimeFormat(SqlIdentifier.getString(names)));
+    }
 }
 
 TimeUnit TimestampInterval() :
@@ -5930,16 +5980,10 @@ SqlNode BuiltinFunctionCall() :
             return SqlStdOperatorTable.CAST.createCall(s.end(this), args);
         }
     |
-        <EXTRACT> {
-            s = span();
+        <EXTRACT> { s = span(); }
+        <LPAREN> unit = TimeUnitForExtract() {
+            args = startList(new SqlIntervalQualifier(unit, null, getPos()));
         }
-        <LPAREN>
-        (
-            <NANOSECOND> { unit = TimeUnit.NANOSECOND; }
-        |   <MICROSECOND> { unit = TimeUnit.MICROSECOND; }
-        |   unit = TimeUnit()
-        )
-        { args = startList(new SqlIntervalQualifier(unit, null, getPos())); }
         <FROM>
         e = Expression(ExprContext.ACCEPT_SUB_QUERY) { args.add(e); }
         <RPAREN> {
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
index 01e9d576a..0fa5602bf 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.parser;
 
 import org.apache.calcite.avatica.util.Casing;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.config.CharLiteralStyle;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.runtime.CalciteException;
@@ -49,6 +50,7 @@ import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.NavigableSet;
 import java.util.Set;
 import java.util.TreeSet;
@@ -542,6 +544,11 @@ public abstract class SqlAbstractParserImpl {
    */
   public abstract void setIdentifierMaxLength(int identifierMaxLength);
 
+  /**
+   * Sets the map from identifier to time unit.
+   */
+  public abstract void setTimeUnitCodes(Map<String, TimeUnit> timeUnitCodes);
+
   /**
    * Sets the SQL language conformance level.
    */
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
index 352f1eadc..a0c514774 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
@@ -18,6 +18,7 @@ package org.apache.calcite.sql.parser;
 
 import org.apache.calcite.avatica.util.Casing;
 import org.apache.calcite.avatica.util.Quoting;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.config.CharLiteralStyle;
 import org.apache.calcite.config.Lex;
 import org.apache.calcite.runtime.CalciteContextException;
@@ -29,6 +30,7 @@ import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlDelegatingConformance;
 import org.apache.calcite.util.SourceStringReader;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import org.immutables.value.Value;
@@ -36,6 +38,7 @@ import org.immutables.value.Value;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -45,6 +48,13 @@ import java.util.Set;
 @SuppressWarnings("deprecation")
 public class SqlParser {
   public static final int DEFAULT_IDENTIFIER_MAX_LENGTH = 128;
+
+  /** Default value of {@link Config#timeUnitCodes()}.
+   * The map is empty, which means that there are no abbreviations other than
+   * the time unit names ("YEAR", "SECOND", etc.) */
+  public static final ImmutableMap<String, TimeUnit> DEFAULT_IDENTIFIER_TIMEUNIT_MAP =
+      ImmutableMap.of();
+
   @Deprecated // to be removed before 2.0
   public static final boolean DEFAULT_ALLOW_BANG_EQUAL =
       SqlConformanceEnum.DEFAULT.isBangEqualAllowed();
@@ -60,6 +70,7 @@ public class SqlParser {
     parser.setQuotedCasing(config.quotedCasing());
     parser.setUnquotedCasing(config.unquotedCasing());
     parser.setIdentifierMaxLength(config.identifierMaxLength());
+    parser.setTimeUnitCodes(config.timeUnitCodes());
     parser.setConformance(config.conformance());
     parser.switchTo(SqlAbstractParserImpl.LexicalState.forConfig(config));
   }
@@ -309,6 +320,18 @@ public class SqlParser {
     /** Sets {@link #charLiteralStyles()}. */
     Config withCharLiteralStyles(Iterable<CharLiteralStyle> charLiteralStyles);
 
+    /** Returns a mapping from abbreviations to time units.
+     *
+     * <p>For example, if the map contains the entry
+     * ("Y", {@link TimeUnit#YEAR}) then you can write
+     * "{@code EXTRACT(S FROM orderDate)}". */
+    @Value.Default default Map<String, TimeUnit> timeUnitCodes() {
+      return DEFAULT_IDENTIFIER_TIMEUNIT_MAP;
+    }
+
+    /** Sets {@link #timeUnitCodes()}. */
+    Config withTimeUnitCodes(Map<String, ? extends TimeUnit> timeUnitCodes);
+
     @Value.Default default SqlParserImplFactory parserFactory() {
       return SqlParserImpl.FACTORY;
     }
@@ -358,6 +381,11 @@ public class SqlParser {
       return setConfig(config.withIdentifierMaxLength(identifierMaxLength));
     }
 
+    public ConfigBuilder setIdentifierTimeUnitMap(
+        ImmutableMap<String, TimeUnit> identifierTimeUnitMap) {
+      return setConfig(config.withTimeUnitCodes(identifierTimeUnitMap));
+    }
+
     @SuppressWarnings("unused")
     @Deprecated // to be removed before 2.0
     public ConfigBuilder setAllowBangEqual(final boolean allowBangEqual) {
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 7416d2758..31ae30ba7 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.parser;
 
 import org.apache.calcite.avatica.util.Quoting;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.SqlExplain;
@@ -30,6 +31,7 @@ import org.apache.calcite.sql.SqlSetOption;
 import org.apache.calcite.sql.SqlWriterConfig;
 import org.apache.calcite.sql.dialect.AnsiSqlDialect;
 import org.apache.calcite.sql.dialect.SparkSqlDialect;
+import org.apache.calcite.sql.parser.SqlParser.Config;
 import org.apache.calcite.sql.pretty.SqlPrettyWriter;
 import org.apache.calcite.sql.test.SqlTestFactory;
 import org.apache.calcite.sql.test.SqlTests;
@@ -65,6 +67,7 @@ import java.util.Map;
 import java.util.Random;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
@@ -7676,6 +7679,93 @@ public class SqlParserTest {
         .fails("(?s)Encountered \"to\".*");
   }
 
+  /** Tests that EXTRACT, FLOOR, CEIL functions accept abbreviations for
+   * time units (such as "Y" for "YEAR") when configured via
+   * {@link Config#timeUnitCodes()}. */
+  @Test void testTimeUnitCodes() {
+    final Map<String, TimeUnit> simpleCodes =
+        ImmutableMap.<String, TimeUnit>builder()
+            .put("Y", TimeUnit.YEAR)
+            .put("M", TimeUnit.MONTH)
+            .put("D", TimeUnit.DAY)
+            .put("H", TimeUnit.HOUR)
+            .put("N", TimeUnit.MINUTE)
+            .put("S", TimeUnit.SECOND)
+            .build();
+
+    // Time unit abbreviations for Microsoft SQL Server
+    final Map<String, TimeUnit> mssqlCodes =
+        ImmutableMap.<String, TimeUnit>builder()
+            .put("Y", TimeUnit.YEAR)
+            .put("YY", TimeUnit.YEAR)
+            .put("YYYY", TimeUnit.YEAR)
+            .put("Q", TimeUnit.QUARTER)
+            .put("QQ", TimeUnit.QUARTER)
+            .put("M", TimeUnit.MONTH)
+            .put("MM", TimeUnit.MONTH)
+            .put("W", TimeUnit.WEEK)
+            .put("WK", TimeUnit.WEEK)
+            .put("WW", TimeUnit.WEEK)
+            .put("DY", TimeUnit.DOY)
+            .put("DW", TimeUnit.DOW)
+            .put("D", TimeUnit.DAY)
+            .put("DD", TimeUnit.DAY)
+            .put("H", TimeUnit.HOUR)
+            .put("HH", TimeUnit.HOUR)
+            .put("N", TimeUnit.MINUTE)
+            .put("MI", TimeUnit.MINUTE)
+            .put("S", TimeUnit.SECOND)
+            .put("SS", TimeUnit.SECOND)
+            .put("MS", TimeUnit.MILLISECOND)
+            .build();
+
+    checkTimeUnitCodes(Config.DEFAULT.timeUnitCodes());
+    checkTimeUnitCodes(simpleCodes);
+    checkTimeUnitCodes(mssqlCodes);
+  }
+
+  /** Checks parsing of built-in functions that accept time unit
+   * abbreviations.
+   *
+   * <p>For example, {@code EXTRACT(Y FROM orderDate)} is using
+   * "Y" as an abbreviation for "YEAR".
+   *
+   * <p>Override if your parser supports more such functions. */
+  protected void checkTimeUnitCodes(Map<String, TimeUnit> timeUnitCodes) {
+    SqlParserFixture f = fixture()
+        .withConfig(config -> config.withTimeUnitCodes(timeUnitCodes));
+    BiConsumer<String, TimeUnit> validConsumer = (abbrev, timeUnit) -> {
+      f.sql("select extract(" + abbrev + " from x)")
+          .ok("SELECT EXTRACT(" + timeUnit + " FROM `X`)");
+      f.sql("select floor(x to " + abbrev + ")")
+          .ok("SELECT FLOOR(`X` TO " + timeUnit + ")");
+      f.sql("select ceil(x to " + abbrev + ")")
+          .ok("SELECT CEIL(`X` TO " + timeUnit + ")");
+    };
+    BiConsumer<String, TimeUnit> invalidConsumer = (abbrev, timeUnit) -> {
+      final String upAbbrev = abbrev.toUpperCase(Locale.ROOT);
+      f.sql("select extract(^" + abbrev + "^ from x)")
+          .fails("'" + upAbbrev + "' is not a valid datetime format");
+      f.sql("SELECT FLOOR(x to ^" + abbrev + "^)")
+          .fails("'" + upAbbrev + "' is not a valid datetime format");
+      f.sql("SELECT CEIL(x to ^" + abbrev + "^)")
+          .fails("'" + upAbbrev + "' is not a valid datetime format");
+    };
+
+    // Check that each valid code passes each query that it should.
+    timeUnitCodes.forEach(validConsumer);
+
+    // If "M" is a valid code then "m" should be also.
+    timeUnitCodes.forEach((abbrev, timeUnit) ->
+        validConsumer.accept(abbrev.toLowerCase(Locale.ROOT), timeUnit));
+
+    // Check that invalid codes generate the right error messages.
+    final Map<String, TimeUnit> invalidCodes =
+        ImmutableMap.of("A", TimeUnit.YEAR,
+            "a", TimeUnit.YEAR);
+    invalidCodes.forEach(invalidConsumer);
+  }
+
   @Test void testGeometry() {
     expr("cast(null as ^geometry^)")
         .fails("Geo-spatial extensions and the GEOMETRY data type are not enabled");
@@ -10085,7 +10175,7 @@ public class SqlParserTest {
           .withClauseEndsLine(random.nextBoolean());
     }
 
-    private String toSqlString(SqlNodeList sqlNodeList,
+    static String toSqlString(SqlNodeList sqlNodeList,
         UnaryOperator<SqlWriterConfig> transform) {
       return sqlNodeList.stream()
           .map(node -> node.toSqlString(transform).getSql())
@@ -10101,7 +10191,7 @@ public class SqlParserTest {
       return constants[random.nextInt(constants.length)];
     }
 
-    private void checkList(SqlNodeList sqlNodeList,
+    static void checkList(SqlNodeList sqlNodeList,
         UnaryOperator<String> converter, List<String> expected) {
       assertThat(sqlNodeList.size(), is(expected.size()));
 
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 26919dfb9..a277b1ea6 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -2641,7 +2641,7 @@ public class SqlOperatorTest {
     checkNullOperand(f1, "<>");
   }
 
-  private void checkNullOperand(SqlOperatorFixture f, String op) {
+  private static void checkNullOperand(SqlOperatorFixture f, String op) {
     f.checkBoolean("1 " + op + " null", null);
     f.checkBoolean("null " + op + " -3", null);
     f.checkBoolean("null " + op + " null", null);
@@ -7254,9 +7254,9 @@ public class SqlOperatorTest {
     f.checkFails("^floor('abcde' to minute)^",
         "(?s)Cannot apply 'FLOOR' to arguments .*", false);
     f.checkFails("floor(timestamp '2015-02-19 12:34:56.78' to ^microsecond^)",
-        "(?s)Encountered \"microsecond\" at .*", false);
+        "'MICROSECOND' is not a valid datetime format", false);
     f.checkFails("floor(timestamp '2015-02-19 12:34:56.78' to ^nanosecond^)",
-        "(?s)Encountered \"nanosecond\" at .*", false);
+        "'NANOSECOND' is not a valid datetime format", false);
     f.checkScalar("floor(time '12:34:56' to minute)",
         "12:34:00", "TIME(0) NOT NULL");
     f.checkScalar("floor(timestamp '2015-02-19 12:34:56.78' to second)",
@@ -7295,9 +7295,9 @@ public class SqlOperatorTest {
     f.checkFails("^ceil('abcde' to minute)^",
         "(?s)Cannot apply 'CEIL' to arguments .*", false);
     f.checkFails("ceil(timestamp '2015-02-19 12:34:56.78' to ^microsecond^)",
-        "(?s)Encountered \"microsecond\" at .*", false);
+        "'MICROSECOND' is not a valid datetime format", false);
     f.checkFails("ceil(timestamp '2015-02-19 12:34:56.78' to ^nanosecond^)",
-        "(?s)Encountered \"nanosecond\" at .*", false);
+        "'NANOSECOND' is not a valid datetime format", false);
     f.checkScalar("ceil(time '12:34:56' to minute)",
         "12:35:00", "TIME(0) NOT NULL");
     f.checkScalar("ceil(time '12:59:56' to minute)",