From bc4cfa32833649f44f6f2e9455229fa0355ad81e Mon Sep 17 00:00:00 2001 From: Tanner Clary Date: Wed, 7 Dec 2022 22:02:01 +0000 Subject: [PATCH] [CALCITE-5423] Implement TIMESTAMP_DIFF function (compatible with BigQuery, and enabled in the BigQuery library) The TIMESTAMP_DIFF function is implemented by SqlLibraryOperators.TIMESTAMP_DIFF3 and only enabled with lib=bigquery, whereas the built-in TIMESTAMPDIFF function is implemented by SqlStdOperatorTable.TIMESTAMP_DIFF and is always enabled. Close apache/calcite#3000 --- babel/src/main/codegen/config.fmpp | 1 + babel/src/test/resources/sql/big-query.iq | 37 +++--- core/src/main/codegen/default_config.fmpp | 1 + core/src/main/codegen/templates/Parser.jj | 36 ++++++ .../calcite/sql/fun/SqlLibraryOperators.java | 13 +- .../calcite/sql/fun/SqlStdOperatorTable.java | 3 +- .../sql/fun/SqlTimestampDiffFunction.java | 43 +++++-- .../sql2rel/StandardConvertletTable.java | 19 ++- site/_docs/reference.md | 4 +- .../apache/calcite/test/SqlOperatorTest.java | 120 ++++++++++++++++++ 10 files changed, 238 insertions(+), 39 deletions(-) diff --git a/babel/src/main/codegen/config.fmpp b/babel/src/main/codegen/config.fmpp index 2d1ababb4276..2b5f7f6b9cf9 100644 --- a/babel/src/main/codegen/config.fmpp +++ b/babel/src/main/codegen/config.fmpp @@ -465,6 +465,7 @@ data: { # "TIME" "TIME_TRUNC" # "TIMESTAMP" + "TIMESTAMP_DIFF" "TIMESTAMP_TRUNC" "TIMEZONE_HOUR" "TIMEZONE_MINUTE" diff --git a/babel/src/test/resources/sql/big-query.iq b/babel/src/test/resources/sql/big-query.iq index 281604d2910d..d0c5779fc3c5 100755 --- a/babel/src/test/resources/sql/big-query.iq +++ b/babel/src/test/resources/sql/big-query.iq @@ -1622,43 +1622,42 @@ SELECT # Display of results may differ, depending upon the environment and # time zone where this query was executed. -!if (false) { SELECT - TIMESTAMP("2010-07-07 10:20:00+00") AS later_timestamp, - TIMESTAMP("2008-12-25 15:30:00+00") AS earlier_timestamp, - TIMESTAMP_DIFF(TIMESTAMP "2010-07-07 10:20:00+00", TIMESTAMP "2008-12-25 15:30:00+00", HOUR) AS hours; -+-------------------------+-------------------------+-------+ -| later_timestamp | earlier_timestamp | hours | -+-------------------------+-------------------------+-------+ -| 2010-07-07 10:20:00 UTC | 2008-12-25 15:30:00 UTC | 13410 | -+-------------------------+-------------------------+-------+ + TIMESTAMP "2010-07-07 10:20:00" AS later_timestamp, + TIMESTAMP "2008-12-25 15:30:00" AS earlier_timestamp, + TIMESTAMP_DIFF(TIMESTAMP "2010-07-07 10:20:00", TIMESTAMP "2008-12-25 15:30:00", HOUR) AS hours; ++---------------------+---------------------+-------+ +| later_timestamp | earlier_timestamp | hours | ++---------------------+---------------------+-------+ +| 2010-07-07 10:20:00 | 2008-12-25 15:30:00 | 13410 | ++---------------------+---------------------+-------+ +(1 row) + !ok -!} # In the following example, the first timestamp occurs # before the second timestamp, resulting in a negative output. -!if (false) { -SELECT TIMESTAMP_DIFF(TIMESTAMP "2018-08-14", TIMESTAMP "2018-10-14", DAY); +SELECT TIMESTAMP_DIFF(TIMESTAMP "2018-08-14", TIMESTAMP "2018-10-14", DAY) AS negative_diff; +---------------+ | negative_diff | +---------------+ -| -61 | +| -61 | +---------------+ +(1 row) + !ok -!} # In this example, the result is 0 because only the number # of whole specified HOUR intervals are included. -!if (false) { -SELECT TIMESTAMP_DIFF("2001-02-01 01:00:00", "2001-02-01 00:00:01", HOUR); +SELECT TIMESTAMP_DIFF("2001-02-01 01:00:00", "2001-02-01 00:00:01", HOUR) AS negative_diff; +---------------+ | negative_diff | +---------------+ -| 0 | +| 0 | +---------------+ -!ok -!} +(1 row) +!ok ##################################################################### # DATE_TRUNC # diff --git a/core/src/main/codegen/default_config.fmpp b/core/src/main/codegen/default_config.fmpp index ea59d6c48d0b..e27cff2e9aed 100644 --- a/core/src/main/codegen/default_config.fmpp +++ b/core/src/main/codegen/default_config.fmpp @@ -317,6 +317,7 @@ parser: { "TIME_TRUNC" "TIMESTAMPADD" "TIMESTAMPDIFF" + "TIMESTAMP_DIFF" "TIMESTAMP_TRUNC" "TOP_LEVEL_COUNT" "TRANSACTION" diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 09eabf64d395..6e1e0e320911 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -6039,6 +6039,8 @@ SqlNode BuiltinFunctionCall() : node = TimestampAddFunctionCall() { return node; } | node = TimestampDiffFunctionCall() { return node; } + | + node = TimestampDiff3FunctionCall() { return node; } | node = TimestampTruncFunctionCall() { return node; } | @@ -6588,6 +6590,33 @@ SqlCall TimestampDiffFunctionCall() : } } +/** + * Parses a call to BigQuery's TIMESTAMP_DIFF. + * + *

The difference between TIMESTAMPDIFF and TIMESTAMP_DIFF is the ordering of + * the parameters and the arrangement of the subtraction. + * TIMESTAMPDIFF uses (unit, timestamp1, timestamp2) with (t2 - t1), while + * TIMESTAMP_DIFF uses (timestamp1, timestamp2, unit) with (t1 - t2). + */ +SqlCall TimestampDiff3FunctionCall() : +{ + final List args = new ArrayList(); + final Span s; + final SqlIntervalQualifier unit; +} +{ + { s = span(); } + + AddExpression(args, ExprContext.ACCEPT_SUB_QUERY) + + AddExpression(args, ExprContext.ACCEPT_SUB_QUERY) + + unit = TimeUnitOrName() { args.add(unit); } + { + return SqlLibraryOperators.TIMESTAMP_DIFF3.createCall(s.end(this), args); + } +} + /** * Parses a call to TIMESTAMP_TRUNC. */ @@ -7132,6 +7161,12 @@ SqlNode JdbcFunctionCall() : name = call.getOperator().getName(); args = new SqlNodeList(call.getOperandList(), getPos()); } + | + LOOKAHEAD(1) + call = TimestampDiff3FunctionCall() { + name = call.getOperator().getName(); + args = new SqlNodeList(call.getOperandList(), getPos()); + } | LOOKAHEAD(3) call = TimestampDiffFunctionCall() { @@ -7977,6 +8012,7 @@ SqlPostfixOperator PostfixRowOperator() : | < TIMESTAMP: "TIMESTAMP" > | < TIMESTAMPADD: "TIMESTAMPADD" > | < TIMESTAMPDIFF: "TIMESTAMPDIFF" > +| < TIMESTAMP_DIFF: "TIMESTAMP_DIFF" > | < TIMESTAMP_TRUNC: "TIMESTAMP_TRUNC" > | < TIMEZONE_HOUR: "TIMEZONE_HOUR" > | < TIMEZONE_MINUTE: "TIMEZONE_MINUTE" > 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 c7fb48a1a29c..43fe6e98ee3a 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 @@ -107,7 +107,8 @@ private SqlLibraryOperators() { * argument. */ @LibraryOperator(libraries = {MSSQL, POSTGRESQL}) public static final SqlFunction DATEDIFF = - new SqlTimestampDiffFunction("DATEDIFF"); + new SqlTimestampDiffFunction("DATEDIFF", + OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.DATE, SqlTypeFamily.DATE)); /** The "DATE_PART(timeUnit, datetime)" function * (Databricks, Postgres, Redshift, Snowflake). */ @@ -660,6 +661,16 @@ private SqlLibraryOperators() { OperandTypes.TIMESTAMP_INTERVAL) .withFunctionType(SqlFunctionCategory.TIMEDATE); + /** The "TIMESTAMP_DIFF(timestamp_expression, timestamp_expression, date_time_part)" + * function (BigQuery) returns the number of date_time_part between the two timestamp + * expressions. + * + *

TIMESTAMP_DIFF(t1, t2, unit) is equivalent to TIMESTAMPDIFF(

*/ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction TIMESTAMP_DIFF3 = + new SqlTimestampDiffFunction("TIMESTAMP_DIFF", + OperandTypes.family(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.TIMESTAMP, SqlTypeFamily.ANY)); + /** 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. */ diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java index 8a867471d73c..6a1f6403ba00 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java @@ -1889,7 +1889,8 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { /** The TIMESTAMPDIFF function. */ public static final SqlFunction TIMESTAMP_DIFF = - new SqlTimestampDiffFunction("TIMESTAMPDIFF"); + new SqlTimestampDiffFunction("TIMESTAMPDIFF", + OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.TIMESTAMP, SqlTypeFamily.TIMESTAMP)); /** * Use of the IN_FENNEL operator forces the argument to be diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlTimestampDiffFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlTimestampDiffFunction.java index eb776395820c..a4d3811ecf2f 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlTimestampDiffFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlTimestampDiffFunction.java @@ -25,8 +25,7 @@ import org.apache.calcite.sql.SqlIntervalQualifier; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperatorBinding; -import org.apache.calcite.sql.type.OperandTypes; -import org.apache.calcite.sql.type.SqlTypeFamily; +import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.validate.SqlValidator; import org.apache.calcite.sql.validate.SqlValidatorScope; @@ -61,23 +60,33 @@ class SqlTimestampDiffFunction extends SqlFunction { private static RelDataType inferReturnType2(SqlOperatorBinding opBinding) { final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); - TimeUnit timeUnit = opBinding.getOperandLiteralValue(0, TimeUnit.class); + TimeUnit timeUnit; + RelDataType type1; + RelDataType type2; + if (opBinding.isOperandLiteral(0, true)) { + type1 = opBinding.getOperandType(0); + type2 = opBinding.getOperandType(1); + timeUnit = opBinding.getOperandLiteralValue(2, TimeUnit.class); + } else { + timeUnit = opBinding.getOperandLiteralValue(0, TimeUnit.class); + type1 = opBinding.getOperandType(1); + type2 = opBinding.getOperandType(2); + } SqlTypeName sqlTypeName = timeUnit == TimeUnit.NANOSECOND ? SqlTypeName.BIGINT : SqlTypeName.INTEGER; return typeFactory.createTypeWithNullability( typeFactory.createSqlType(sqlTypeName), - opBinding.getOperandType(1).isNullable() - || opBinding.getOperandType(2).isNullable()); + type1.isNullable() + || type2.isNullable()); } /** Creates a SqlTimestampDiffFunction. */ - SqlTimestampDiffFunction(String name) { + SqlTimestampDiffFunction(String name, SqlOperandTypeChecker operandTypeChecker) { super(name, SqlKind.TIMESTAMP_DIFF, SqlTimestampDiffFunction::inferReturnType2, null, - OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.DATETIME, - SqlTypeFamily.DATETIME), + operandTypeChecker, SqlFunctionCategory.TIMEDATE); } @@ -87,14 +96,20 @@ private static RelDataType inferReturnType2(SqlOperatorBinding opBinding) { // This is either a time unit or a time frame: // - // * In "TIMESTAMPADD(YEAR, 2, x)" operand 0 is a SqlIntervalQualifier - // with startUnit = YEAR and timeFrameName = null. + // * In "TIMESTAMPDIFF(YEAR, timestamp1, timestamp2)" operand 0 is a SqlIntervalQualifier + // with startUnit = YEAR and timeFrameName = null. The same is true for BigQuery's + // TIMESTAMP_DIFF() however the SqlIntervalQualifier is operand 2 due to differing + // parameter orders. // - // * In "TIMESTAMPADD(MINUTE15, 2, x) operand 0 is a SqlIntervalQualifier - // with startUnit = EPOCH and timeFrameName = 'MINUTE15'. + // * In "TIMESTAMP_ADD(MINUTE15, timestamp1, timestamp2) operand 0 is a SqlIntervalQualifier + // with startUnit = EPOCH and timeFrameName = 'MINUTE15'. As above, for BigQuery's + // TIMESTAMP_DIFF() the SqlIntervalQualifier is found in operand 2 instead. // // If the latter, check that timeFrameName is valid. - validator.validateTimeFrame( - (SqlIntervalQualifier) call.getOperandList().get(0)); + if (call.operand(2) instanceof SqlIntervalQualifier) { + validator.validateTimeFrame((SqlIntervalQualifier) call.operand(2)); + } else { + validator.validateTimeFrame((SqlIntervalQualifier) call.operand(0)); + } } } diff --git a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java index 727ea5391038..c6c2dfa09806 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java @@ -167,6 +167,8 @@ private StandardConvertletTable() { registerOp(SqlLibraryOperators.TIMESTAMP_ADD2, new TimestampAddConvertlet()); + registerOp(SqlLibraryOperators.TIMESTAMP_DIFF3, + new TimestampDiffConvertlet()); registerOp(SqlLibraryOperators.NVL, StandardConvertletTable::convertNvl); registerOp(SqlLibraryOperators.DECODE, @@ -1898,13 +1900,24 @@ private static class TimestampDiffConvertlet implements SqlRexConvertlet { @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) { // TIMESTAMPDIFF(unit, t1, t2) // => (t2 - t1) UNIT + // TIMESTAMP_DIFF(t1, t2, unit) + // => (t1 - t2) UNIT + SqlIntervalQualifier qualifier; + final RexNode op1; + final RexNode op2; + if (call.operand(0).getKind() == SqlKind.INTERVAL_QUALIFIER) { + qualifier = call.operand(0); + op1 = cx.convertExpression(call.operand(1)); + op2 = cx.convertExpression(call.operand(2)); + } else { + qualifier = call.operand(2); + op1 = cx.convertExpression(call.operand(1)); + op2 = cx.convertExpression(call.operand(0)); + } final RexBuilder rexBuilder = cx.getRexBuilder(); - SqlIntervalQualifier qualifier = call.operand(0); final TimeFrame timeFrame = cx.getValidator().validateTimeFrame(qualifier); final TimeUnit unit = first(timeFrame.unit(), TimeUnit.EPOCH); - final RexNode op1 = cx.convertExpression(call.operand(1)); - final RexNode op2 = cx.convertExpression(call.operand(2)); if (unit == TimeUnit.EPOCH && qualifier.timeFrameName != null) { // Custom time frames have a different path. They are kept as names, and // then handled by Java functions. diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 423fd338c3a0..319b13ced807 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1017,6 +1017,7 @@ TIES, **TIMESTAMP**, TIMESTAMPADD, TIMESTAMPDIFF, +TIMESTAMP_DIFF, TIMESTAMP_TRUNC, **TIMEZONE_HOUR**, **TIMEZONE_MINUTE**, @@ -2655,7 +2656,8 @@ semantics. | m | STRCMP(string, string) | Returns 0 if both of the strings are same and returns -1 when the first argument is smaller than the second and 1 when the second one is smaller than the first one | b m o p | SUBSTR(string, position [, substringLength ]) | Returns a portion of *string*, beginning at character *position*, *substringLength* characters long. SUBSTR calculates lengths using characters as defined by the input character set | b o | TANH(numeric) | Returns the hyperbolic tangent of *numeric* -| b | TIMESTAMP_ADD(timestamp, interval int64 date_part) | Adds int64_expression units of date_part to the timestamp, independent of any time zone. +| b | TIMESTAMP_ADD(timestamp, interval) | Adds *interval* to *timestamp*, independent of any time zone +| b | TIMESTAMP_DIFF(timestamp, timestamp2, timeUnit) | Returns the whole number of *timeUnit* between *timestamp* and *timestamp2*. Equivalent to `TIMESTAMPDIFF(timeUnit, timestamp2, timestamp)` and `(timestamp - timestamp2) timeUnit` | 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 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 92cec17ff385..465110e9d6bd 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -7582,6 +7582,11 @@ private static void checkArrayConcatAggFuncFails(SqlOperatorFixture t) { f.checkScalar("timestampdiff(\"month4\", date '2016-02-24', " + "date '2016-02-23')", "0", "INTEGER NOT NULL"); + f.withLibrary(SqlLibrary.BIG_QUERY) + .setFor(SqlLibraryOperators.TIMESTAMP_DIFF3) + .checkScalar("timestamp_diff(timestamp '2008-12-25 15:30:00', " + + "timestamp '2008-12-25 16:30:00', \"minute15\")", + "-4", "INTEGER NOT NULL"); } @Test void testFloorFuncInterval() { @@ -7816,6 +7821,121 @@ private static void checkArrayConcatAggFuncFails(SqlOperatorFixture t) { f.checkNull("timestamp_add(CAST(NULL AS TIMESTAMP), interval 5 minute)"); } + /** Tests {@code TIMESTAMP_DIFF}, BigQuery's variant of the + * {@code TIMESTAMPDIFF} function, which differs in the ordering + * of the parameters and the ordering of the subtraction between + * the two timestamps. In {@code TIMESTAMPDIFF} it is (t2 - t1) + * while for {@code TIMESTAMP_DIFF} is is (t1 - t2). */ + @Test void testTimestampDiff3() { + final SqlOperatorFixture f0 = fixture() + .setFor(SqlLibraryOperators.TIMESTAMP_DIFF3); + f0.checkFails("^timestamp_diff(timestamp '2008-12-25 15:30:00', " + + "timestamp '2008-12-25 16:30:00', " + + "minute)^", + "No match found for function signature " + + "TIMESTAMP_DIFF\\(, , \\)", false); + + final SqlOperatorFixture f = fixture() + .withLibrary(SqlLibrary.BIG_QUERY) + .setFor(SqlLibraryOperators.TIMESTAMP_DIFF3); + HOUR_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2016-02-24 12:42:25', " + + "timestamp '2016-02-24 15:42:25', " + + s + ")", + "-3", "INTEGER NOT NULL")); + MICROSECOND_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2016-02-24 12:42:25', " + + "timestamp '2016-02-24 12:42:20', " + + s + ")", + "5000000", "INTEGER NOT NULL")); + YEAR_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2014-02-24 12:42:25', " + + "timestamp '2016-02-24 12:42:25', " + + s + ")", + "-2", "INTEGER NOT NULL")); + WEEK_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2014-02-24 12:42:25', " + + "timestamp '2016-02-24 12:42:25', " + + s + ")", + "-104", "INTEGER NOT NULL")); + WEEK_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2014-02-19 12:42:25', " + + "timestamp '2016-02-24 12:42:25', " + + s + ")", + "-105", "INTEGER NOT NULL")); + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2014-02-24 12:42:25', " + + "timestamp '2016-02-24 12:42:25', " + + s + ")", + "-24", "INTEGER NOT NULL")); + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2019-09-01 12:42:25', " + + "timestamp '2020-03-01 12:42:25', " + + s + ")", + "-6", "INTEGER NOT NULL")); + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2019-09-01 12:42:25', " + + "timestamp '2016-08-01 12:42:25', " + + s + ")", + "37", "INTEGER NOT NULL")); + QUARTER_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2014-02-24 12:42:25', " + + "timestamp '2016-02-24 12:42:25', " + + s + ")", + "-8", "INTEGER NOT NULL")); + f.checkScalar("timestamp_diff(timestamp '2014-02-24 12:42:25', " + + "timestamp '2614-02-24 12:42:25', " + + "CENTURY)", + "-6", "INTEGER NOT NULL"); + QUARTER_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(timestamp '2016-02-24 12:42:25', " + + "cast(null as timestamp), " + + s + ")", + isNullValue(), "INTEGER")); + + // timestamp_diff with date + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-03-15', " + + "date '2016-06-14', " + + s + ")", + "-2", "INTEGER NOT NULL")); + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2019-09-01', " + + "date '2020-03-01', " + + s + ")", + "-6", "INTEGER NOT NULL")); + MONTH_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2019-09-01', " + + "date '2016-08-01', " + + s + ")", + "37", "INTEGER NOT NULL")); + DAY_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-06-15', " + + "date '2016-06-14', " + + s + ")", + "1", "INTEGER NOT NULL")); + HOUR_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-06-15', " + + "date '2016-06-14', " + + s + ")", + "24", "INTEGER NOT NULL")); + HOUR_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-06-15', " + + "date '2016-06-15', " + + s + ")", + "0", "INTEGER NOT NULL")); + MINUTE_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-06-15', " + + "date '2016-06-14', " + + s + ")", + "1440", "INTEGER NOT NULL")); + DAY_VARIANTS.forEach(s -> + f.checkScalar("timestamp_diff(date '2016-06-15', " + + "cast(null as date), " + + s + ")", + isNullValue(), "INTEGER")); + } + @Test void testTimestampDiff() { final SqlOperatorFixture f = fixture(); f.setFor(SqlStdOperatorTable.TIMESTAMP_DIFF, VmName.EXPAND);