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

commit a505b25eacc473c6ec0ef8abd40c1ccae86297b6
Author: TJ Banghart <[email protected]>
AuthorDate: Mon Sep 19 21:15:38 2022 +0000

    [CALCITE-5269] Implement BigQuery TIME_TRUNC and TIMESTAMP_TRUNC functions
    
    Make negative parser tests pass on Windows. (Previously, we
    assumed that the error message would have Linux line
    endings.)
    
    Close apache/calcite#2913
---
 babel/src/main/codegen/config.fmpp                 |  2 +
 babel/src/test/resources/sql/big-query.iq          | 57 ++++++++++------
 core/src/main/codegen/default_config.fmpp          |  2 +
 core/src/main/codegen/templates/Parser.jj          | 77 +++++++++++++++++++++-
 .../calcite/adapter/enumerable/RexImpTable.java    | 13 ++++
 .../calcite/sql/fun/SqlLibraryOperators.java       | 25 +++++++
 .../org/apache/calcite/sql/type/OperandTypes.java  |  6 ++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 15 +++++
 site/_docs/reference.md                            |  4 ++
 .../apache/calcite/sql/parser/SqlParserTest.java   | 18 +++++
 .../apache/calcite/sql/test/AbstractSqlTester.java |  2 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   | 68 +++++++++++++++++++
 12 files changed, 266 insertions(+), 23 deletions(-)

diff --git a/babel/src/main/codegen/config.fmpp 
b/babel/src/main/codegen/config.fmpp
index 3562e254dc..c4168665b7 100644
--- a/babel/src/main/codegen/config.fmpp
+++ b/babel/src/main/codegen/config.fmpp
@@ -463,7 +463,9 @@ data: {
       "TEMPORARY"
 #     "THEN"
 #     "TIME"
+      "TIME_TRUNC"
 #     "TIMESTAMP"
+      "TIMESTAMP_TRUNC"
       "TIMEZONE_HOUR"
       "TIMEZONE_MINUTE"
       "TINYINT"
diff --git a/babel/src/test/resources/sql/big-query.iq 
b/babel/src/test/resources/sql/big-query.iq
index b6a23a0f3c..61c07698ae 100755
--- a/babel/src/test/resources/sql/big-query.iq
+++ b/babel/src/test/resources/sql/big-query.iq
@@ -766,15 +766,25 @@ SELECT TIMESTAMP(DATE "2008-12-25") AS timestamp_date;
 
 # Display of results may differ, depending upon the environment and
 # time zone where this query was executed.
-!if (false) {
+
+# Note that BigQuery prints "2008-12-25 15:30:00 UTC" as the result,
+# whereas Calcite's result does not include "UTC". This is because
+# BigQuery's TIMESTAMP type is an instant (the equivalent of Calcite's
+# TIMESTAMP WITH LOCAL TIME ZONE type). It would have been too confusing
+# for a function called 'TIMESTAMP_SECONDS' to return a TIMESTAMP WITH
+# LOCAL TIME ZONE, so this function returns a Calcite TIMESTAMP which,
+# as a local time does not include UTC or any time zone.
+
 SELECT TIMESTAMP_SECONDS(1230219000) AS timestamp_value;
-+-------------------------+
-| timestamp_value         |
-+-------------------------+
-| 2008-12-25 15:30:00 UTC |
-+-------------------------+
++---------------------+
+| timestamp_value     |
++---------------------+
+| 2008-12-25 15:30:00 |
++---------------------+
+(1 row)
+
 !ok
-!}
+
 
 #####################################################################
 # TIMESTAMP_MILLIS
@@ -786,15 +796,20 @@ SELECT TIMESTAMP_SECONDS(1230219000) AS timestamp_value;
 
 # Display of results may differ, depending upon the environment and
 # time zone where this query was executed.
-!if (false) {
+
+# BigQuery's result ends in "UTC" but Calcite's does not, for the reasons
+# explained in TIMESTAMP_SECONDS.
+
 SELECT TIMESTAMP_MILLIS(1230219000000) AS timestamp_value;
-+-------------------------+
-| timestamp_value         |
-+-------------------------+
-| 2008-12-25 15:30:00 UTC |
-+-------------------------+
++---------------------+
+| timestamp_value     |
++---------------------+
+| 2008-12-25 15:30:00 |
++---------------------+
+(1 row)
+
 !ok
-!}
+
 
 #####################################################################
 # TIMESTAMP_MICROS
@@ -1812,17 +1827,17 @@ SELECT
 #
 # Returns TIME
 
-!if (false) {
 SELECT
   TIME "15:30:00" as original,
   TIME_TRUNC(TIME "15:30:00", HOUR) as truncated;
-+----------------------------+------------------------+
-| original                   | truncated              |
-+----------------------------+------------------------+
-| 15:30:00                   | 15:00:00               |
-+----------------------------+------------------------+
++----------+-----------+
+| original | truncated |
++----------+-----------+
+| 15:30:00 | 15:00:00  |
++----------+-----------+
+(1 row)
+
 !ok
-!}
 
 #####################################################################
 # TIMESTAMP_TRUNC
diff --git a/core/src/main/codegen/default_config.fmpp 
b/core/src/main/codegen/default_config.fmpp
index 8e9cdc0041..22ca9289ec 100644
--- a/core/src/main/codegen/default_config.fmpp
+++ b/core/src/main/codegen/default_config.fmpp
@@ -314,8 +314,10 @@ parser: {
     "TABLE_NAME"
     "TEMPORARY"
     "TIES"
+    "TIME_TRUNC"
     "TIMESTAMPADD"
     "TIMESTAMPDIFF"
+    "TIMESTAMP_TRUNC"
     "TOP_LEVEL_COUNT"
     "TRANSACTION"
     "TRANSACTIONS_ACTIVE"
diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index 9ad6582315..7f26f806f3 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -4986,6 +4986,19 @@ TimeUnit TimeUnitForExtract() :
     unit = TimeUnit() { return unit; }
 }
 
+/**
+ * Parses time unit for the TIME_TRUNC function.
+ * This is a subset of time units appropriate for TIME data type.
+ */
+TimeUnit TimeUnitForTimeTrunc() :
+{}
+{
+    <MILLISECOND> { return TimeUnit.MILLISECOND; }
+|   <SECOND> { return TimeUnit.SECOND; }
+|   <MINUTE> { return TimeUnit.MINUTE; }
+|   <HOUR> { return TimeUnit.HOUR; }
+}
+
 /**
  * Parses a simple identifier as a TimeUnit.
  */
@@ -6023,6 +6036,10 @@ SqlNode BuiltinFunctionCall() :
     |
         node = TimestampDiffFunctionCall() { return node; }
     |
+        node = TimestampTruncFunctionCall() { return node; }
+    |
+        node = TimeTruncFunctionCall() { return node; }
+    |
 <#list 
(parser.builtinFunctionCallMethods!default.parser.builtinFunctionCallMethods) 
as method>
         node = ${method} { return node; }
     |
@@ -6571,11 +6588,55 @@ SqlCall TimestampDiffFunctionCall() :
     }
 }
 
+/**
+ * Parses a call to TIMESTAMP_TRUNC.
+ */
+SqlCall TimestampTruncFunctionCall() :
+{
+    final List<SqlNode> args = new ArrayList<SqlNode>();
+    final Span s;
+    final TimeUnit unit;
+}
+{
+    <TIMESTAMP_TRUNC> { s = span(); }
+    <LPAREN>
+    AddExpression(args, ExprContext.ACCEPT_SUB_QUERY)
+    <COMMA>
+    unit = TimeUnit() {
+        args.add(new SqlIntervalQualifier(unit, null, getPos()));
+    }
+    <RPAREN> {
+        return SqlLibraryOperators.TIMESTAMP_TRUNC.createCall(s.end(this), 
args);
+    }
+}
+
+/**
+ * Parses a call to TIME_TRUNC.
+ */
+SqlCall TimeTruncFunctionCall() :
+{
+    final List<SqlNode> args = new ArrayList<SqlNode>();
+    final Span s;
+    final TimeUnit unit;
+}
+{
+    <TIME_TRUNC> { s = span(); }
+    <LPAREN>
+    AddExpression(args, ExprContext.ACCEPT_SUB_QUERY)
+    <COMMA>
+    unit = TimeUnitForTimeTrunc() {
+        args.add(new SqlIntervalQualifier(unit, null, getPos()));
+    }
+    <RPAREN> {
+        return SqlLibraryOperators.TIME_TRUNC.createCall(s.end(this), args);
+    }
+}
+
 /**
  * Parses a call to a grouping function inside the GROUP BY clause,
  * for example {@code TUMBLE(rowtime, INTERVAL '1' MINUTE)}.
  */
-SqlCall GroupByWindowingCall():
+SqlCall GroupByWindowingCall() :
 {
     final Span s;
     final List<SqlNode> args;
@@ -7064,6 +7125,18 @@ SqlNode JdbcFunctionCall() :
             name = call.getOperator().getName();
             args = new SqlNodeList(call.getOperandList(), getPos());
         }
+    |
+        LOOKAHEAD(1)
+        call = TimestampTruncFunctionCall() {
+            name = call.getOperator().getName();
+            args = new SqlNodeList(call.getOperandList(), getPos());
+        }
+    |
+        LOOKAHEAD(1)
+        call = TimeTruncFunctionCall() {
+            name = call.getOperator().getName();
+            args = new SqlNodeList(call.getOperandList(), getPos());
+        }
     |
         LOOKAHEAD(3)
         call = TimestampDiffFunctionCall() {
@@ -7905,9 +7978,11 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < THEN: "THEN" >
 |   < TIES: "TIES" >
 |   < TIME: "TIME" >
+|   < TIME_TRUNC: "TIME_TRUNC" >
 |   < TIMESTAMP: "TIMESTAMP" >
 |   < TIMESTAMPADD: "TIMESTAMPADD" >
 |   < TIMESTAMPDIFF: "TIMESTAMPDIFF" >
+|   < TIMESTAMP_TRUNC: "TIMESTAMP_TRUNC" >
 |   < TIMEZONE_HOUR: "TIMEZONE_HOUR" >
 |   < TIMEZONE_MINUTE: "TIMEZONE_MINUTE" >
 |   < TINYINT: "TINYINT" >
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 1864b1e459..f081d7dee7 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -156,6 +156,8 @@ import static 
org.apache.calcite.sql.fun.SqlLibraryOperators.TANH;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.TIMESTAMP_MICROS;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.TIMESTAMP_MILLIS;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.TIMESTAMP_SECONDS;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TIMESTAMP_TRUNC;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TIME_TRUNC;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_BASE64;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRANSLATE3;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.UNIX_DATE;
@@ -459,6 +461,17 @@ public class RexImpTable {
             BuiltInMethod.UNIX_TIMESTAMP_CEIL.method,
             BuiltInMethod.UNIX_DATE_CEIL.method));
 
+    // TIMESTAMP_TRUNC and TIME_TRUNC methods are syntactic sugar for standard 
datetime FLOOR
+    map.put(TIMESTAMP_TRUNC,
+        new FloorImplementor(BuiltInMethod.FLOOR.method.getName(),
+            BuiltInMethod.UNIX_TIMESTAMP_FLOOR.method,
+            BuiltInMethod.UNIX_DATE_FLOOR.method));
+    map.put(TIME_TRUNC,
+        new FloorImplementor(BuiltInMethod.FLOOR.method.getName(),
+            BuiltInMethod.UNIX_TIMESTAMP_FLOOR.method,
+            BuiltInMethod.UNIX_DATE_FLOOR.method));
+
+
     defineMethod(LAST_DAY, "lastDay", NullPolicy.STRICT);
     map.put(DAYNAME,
         new PeriodNameImplementor("dayName",
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 794270682c..2691ad4935 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -602,6 +602,31 @@ public abstract class SqlLibraryOperators {
           OperandTypes.STRING_STRING,
           SqlFunctionCategory.TIMEDATE);
 
+  /** The "TIME_TRUNC(time_expression, time_part)" function (BigQuery);
+   * truncates a TIME value to the granularity of time_part. The TIME value is
+   * always rounded to the beginning of time_part. */
+  @LibraryOperator(libraries = {BIG_QUERY})
+  public static final SqlFunction TIME_TRUNC =
+      new SqlFunction("TIME_TRUNC",
+          SqlKind.OTHER_FUNCTION,
+          ReturnTypes.TIME_NULLABLE,
+          null,
+          OperandTypes.TIME_INTERVAL,
+          SqlFunctionCategory.TIMEDATE);
+
+  /** The "TIMESTAMP_TRUNC(timestamp_expression, date_time_part[, time_zone])"
+   * function (BigQuery); truncates a TIMESTAMP value to the granularity of
+   * date_time_part. The TIMESTAMP value is always rounded to the beginning of
+   * date_time_part. */
+  @LibraryOperator(libraries = {BIG_QUERY})
+  public static final SqlFunction TIMESTAMP_TRUNC =
+      new SqlFunction("TIMESTAMP_TRUNC",
+          SqlKind.OTHER_FUNCTION,
+          ReturnTypes.TIMESTAMP_NULLABLE,
+          null,
+          OperandTypes.TIMESTAMP_INTERVAL,
+          SqlFunctionCategory.TIMEDATE);
+
   /** The "TIMESTAMP_SECONDS(bigint)" function; returns a TIMESTAMP value
    * a given number of seconds after 1970-01-01 00:00:00. */
   @LibraryOperator(libraries = {BIG_QUERY})
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 67db9053e7..280bb14afb 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -551,6 +551,12 @@ public abstract class OperandTypes {
   public static final SqlSingleOperandTypeChecker INTERVAL_NUMERIC =
       family(SqlTypeFamily.DATETIME_INTERVAL, SqlTypeFamily.NUMERIC);
 
+  public static final SqlSingleOperandTypeChecker TIME_INTERVAL =
+      family(SqlTypeFamily.TIME, SqlTypeFamily.DATETIME_INTERVAL);
+
+  public static final SqlSingleOperandTypeChecker TIMESTAMP_INTERVAL =
+      family(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.DATETIME_INTERVAL);
+
   public static final SqlSingleOperandTypeChecker DATETIME_INTERVAL =
       family(SqlTypeFamily.DATETIME, SqlTypeFamily.DATETIME_INTERVAL);
 
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index a77987d52f..0db4ea2466 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -1738,6 +1738,21 @@ class RelToSqlConverterTest {
     sql(query).withBigQuery().ok(expected);
   }
 
+  @Test void testBigQueryTimeTruncFunctions() {
+    String timestampTrunc = "select timestamp_trunc(timestamp '2012-02-03 
15:30:00', month)\n"
+        + "from \"foodmart\".\"product\"\n";
+    final String expectedTimestampTrunc =
+        "SELECT TIMESTAMP_TRUNC(TIMESTAMP '2012-02-03 15:30:00', MONTH)\n"
+        + "FROM \"foodmart\".\"product\"";
+    
sql(timestampTrunc).withLibrary(SqlLibrary.BIG_QUERY).ok(expectedTimestampTrunc);
+
+    String timeTrunc = "select time_trunc(time '15:30:00', minute)\n"
+        + "from \"foodmart\".\"product\"\n";
+    final String expectedTimeTrunc = "SELECT TIME_TRUNC(TIME '15:30:00', 
MINUTE)\n"
+        + "FROM \"foodmart\".\"product\"";
+    sql(timeTrunc).withLibrary(SqlLibrary.BIG_QUERY).ok(expectedTimeTrunc);
+  }
+
   /** Test case for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-3220";>[CALCITE-3220]
    * HiveSqlDialect should transform the SQL-standard TRIM function to TRIM,
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index e49c5fa6dd..98a9bd7c0e 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1006,8 +1006,10 @@ TIES,
 **TIMESTAMP**,
 TIMESTAMPADD,
 TIMESTAMPDIFF,
+TIMESTAMP_TRUNC,
 **TIMEZONE_HOUR**,
 **TIMEZONE_MINUTE**,
+TIME_TRUNC,
 **TINYINT**,
 **TO**,
 TOP_LEVEL_COUNT,
@@ -2627,6 +2629,8 @@ semantics.
 | b | TIMESTAMP_MICROS(integer)                      | Returns the TIMESTAMP 
that is *integer* microseconds after 1970-01-01 00:00:00
 | b | TIMESTAMP_MILLIS(integer)                      | Returns the TIMESTAMP 
that is *integer* milliseconds after 1970-01-01 00:00:00
 | b | TIMESTAMP_SECONDS(integer)                     | Returns the TIMESTAMP 
that is *integer* seconds after 1970-01-01 00:00:00
+| b | TIMESTAMP_TRUNC(timestamp, timeUnit)           | Truncates a *timestamp* 
value to the granularity of *timeUnit*. The *timestamp* value is always rounded 
to the beginning of the *timeUnit*.
+| b | TIME_TRUNC(time, timeUnit)                     | Truncates a *time* 
value to the granularity of *timeUnit*. The *time* value is always rounded to 
the beginning of timeUnit, which can be one of the following: MILLISECOND, 
SECOND, MINUTE, HOUR.
 | o p | TO_DATE(string, format)                      | Converts *string* to a 
date using the format *format*
 | o p | TO_TIMESTAMP(string, format)                 | Converts *string* to a 
timestamp using the format *format*
 | o p | TRANSLATE(expr, fromString, toString)        | Returns *expr* with all 
occurrences of each character in *fromString* replaced by its corresponding 
character in *toString*. Characters in *expr* that are not in *fromString* are 
not replaced
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 e692dfaee2..198518f0db 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
@@ -8150,6 +8150,24 @@ public class SqlParserTest {
     sql(sql).ok(expected);
   }
 
+  @Test void testTimeTrunc() {
+    final String sql = "select time_trunc(TIME '15:30:00', hour) from t";
+    final String expected = "SELECT TIME_TRUNC(TIME '15:30:00', HOUR)\n"
+        + "FROM `T`";
+    sql(sql).ok(expected);
+
+    // should fail for time unit not appropriate for TIME type.
+    final String weekSql = "select time_trunc(time '15:30:00', ^week^) from t";
+    sql(weekSql).fails("(?s).*Was expecting one of.*");
+  }
+
+  @Test void testTimestampTrunc() {
+    final String sql = "select timestamp_trunc(timestamp '2008-12-25 
15:30:00', week) from t";
+    final String expected = "SELECT TIMESTAMP_TRUNC(TIMESTAMP '2008-12-25 
15:30:00', WEEK)\n"
+        + "FROM `T`";
+    sql(sql).ok(expected);
+  }
+
   @Test void testUnnest() {
     sql("select*from unnest(x)")
         .ok("SELECT *\n"
diff --git 
a/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java 
b/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
index fc50c9c6db..b2c474a96a 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
@@ -119,7 +119,7 @@ public abstract class AbstractSqlTester implements 
SqlTester, AutoCloseable {
       if (expectedMsgPattern == null) {
         throw new RuntimeException("Error while parsing query:" + sap, spe);
       } else if (errMessage == null
-          || !errMessage.matches(expectedMsgPattern)) {
+          || !Util.toLinux(errMessage).matches(expectedMsgPattern)) {
         throw new RuntimeException("Error did not match expected ["
             + expectedMsgPattern + "] while parsing query ["
             + sap + "]", spe);
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 8c94c1ed4b..b281989593 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -7602,6 +7602,74 @@ public class SqlOperatorTest {
         isNullValue(), "INTEGER");
   }
 
+  @Test void testTimeTrunc() {
+    SqlOperatorFixture nonBigQuery = fixture()
+        .setFor(SqlLibraryOperators.TIME_TRUNC);
+    nonBigQuery.checkFails("^time_trunc(time '15:30:00', hour)^",
+        "No match found for function signature "
+            + "TIME_TRUNC\\(<TIME>, <INTERVAL_DAY_TIME>\\)",
+        false);
+
+    final SqlOperatorFixture f = fixture()
+        .withLibrary(SqlLibrary.BIG_QUERY)
+        .setFor(SqlLibraryOperators.TIME_TRUNC);
+    f.checkFails("time_trunc(time '12:34:56', ^year^)",
+        "Encountered \"year\" at line 1, column 37\\.\n"
+            + "Was expecting one of:\n"
+            + "    \"HOUR\" \\.\\.\\.\n"
+            + "    \"MILLISECOND\" \\.\\.\\.\n"
+            + "    \"MINUTE\" \\.\\.\\.\n"
+            + "    \"SECOND\" \\.\\.\\.\n"
+            + "    ", false);
+    f.checkFails("^time_trunc(123.45, minute)^",
+        "Cannot apply 'TIME_TRUNC' to arguments of type "
+            + "'TIME_TRUNC\\(<DECIMAL\\(5, 2\\)>, <INTERVAL MINUTE>\\)'\\. "
+            + "Supported form\\(s\\): 'TIME_TRUNC\\(<TIME>, 
<DATETIME_INTERVAL>\\)'", false);
+    f.checkScalar("time_trunc(time '12:34:56', second)",
+        "12:34:56", "TIME(0) NOT NULL");
+    f.checkScalar("time_trunc(time '12:34:56', minute)",
+        "12:34:00", "TIME(0) NOT NULL");
+    f.checkScalar("time_trunc(time '12:34:56', hour)",
+        "12:00:00", "TIME(0) NOT NULL");
+    f.checkNull("time_trunc(cast(null as time), second)");
+  }
+
+  @Test void testTimestampTrunc() {
+    SqlOperatorFixture nonBigQuery = fixture()
+        .setFor(SqlLibraryOperators.TIMESTAMP_TRUNC);
+    nonBigQuery.checkFails("^timestamp_trunc(timestamp '2012-05-02 15:30:00', 
hour)^",
+        "No match found for function signature "
+            + "TIMESTAMP_TRUNC\\(<TIMESTAMP>, <INTERVAL_DAY_TIME>\\)",
+        false);
+
+    final SqlOperatorFixture f = fixture()
+        .withLibrary(SqlLibrary.BIG_QUERY)
+        .setFor(SqlLibraryOperators.TIMESTAMP_TRUNC);
+    f.checkFails("^timestamp_trunc(100, hour)^",
+        "Cannot apply 'TIMESTAMP_TRUNC' to arguments of type "
+            + "'TIMESTAMP_TRUNC\\(<INTEGER>, <INTERVAL HOUR>\\)'\\. "
+            + "Supported form\\(s\\): 'TIMESTAMP_TRUNC\\(<TIMESTAMP>, 
<DATETIME_INTERVAL>\\)'",
+        false);
+    f.checkFails("timestamp_trunc(timestamp '2015-02-19 12:34:56.78', 
^microsecond^)",
+        "'MICROSECOND' is not a valid datetime format", false);
+    f.checkFails("timestamp_trunc(timestamp '2015-02-19 12:34:56.78', 
^nanosecond^)",
+        "'NANOSECOND' is not a valid datetime format", false);
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56.78', 
second)",
+        "2015-02-19 12:34:56", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', minute)",
+        "2015-02-19 12:34:00", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', hour)",
+        "2015-02-19 12:00:00", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', day)",
+        "2015-02-19 00:00:00", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', week)",
+        "2015-02-15 00:00:00", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', month)",
+        "2015-02-01 00:00:00", "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_trunc(timestamp '2015-02-19 12:34:56', year)",
+        "2015-01-01 00:00:00", "TIMESTAMP(0) NOT NULL");
+  }
+
   @Test void testDenseRankFunc() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.DENSE_RANK, VM_FENNEL, VM_JAVA);

Reply via email to