From 939c9a62b4905d2cfffcb4adaefd76a809aa3520 Mon Sep 17 00:00:00 2001 From: Jesus Camacho Rodriguez Date: Mon, 14 Aug 2017 17:46:42 -0700 Subject: [PATCH] [CALCITE-1947] Add time/timestamp with local time zone types to optimizer Close apache/calcite#519 --- .../adapter/enumerable/RexImpTable.java | 15 +- .../enumerable/RexToLixTranslator.java | 153 +++++++++++ .../calcite/jdbc/JavaTypeFactoryImpl.java | 2 + .../calcite/rel/metadata/RelMdSize.java | 4 + .../rel/rules/SortProjectTransposeRule.java | 3 +- .../rel/type/RelDataTypeSystemImpl.java | 8 + .../org/apache/calcite/rex/RexBuilder.java | 57 ++++ .../org/apache/calcite/rex/RexLiteral.java | 26 ++ .../org/apache/calcite/rex/RexSimplify.java | 1 + .../apache/calcite/runtime/SqlFunctions.java | 98 +++++++ .../org/apache/calcite/schema/Schemas.java | 4 +- .../calcite/sql/SqlJdbcDataTypeName.java | 2 + .../sql/type/SqlTypeAssignmentRules.java | 34 +++ .../calcite/sql/type/SqlTypeFamily.java | 6 +- .../apache/calcite/sql/type/SqlTypeName.java | 11 +- .../sql2rel/StandardConvertletTable.java | 26 ++ .../apache/calcite/util/BuiltInMethod.java | 28 +- .../org/apache/calcite/util/DateString.java | 2 +- .../calcite/util/DateTimeStringUtils.java | 88 ++++++ .../org/apache/calcite/util/TimeString.java | 6 +- .../calcite/util/TimeWithTimeZoneString.java | 188 +++++++++++++ .../apache/calcite/util/TimestampString.java | 52 +--- .../util/TimestampWithTimeZoneString.java | 194 ++++++++++++++ .../calcite/jdbc/CalciteRemoteDriverTest.java | 2 +- .../apache/calcite/rex/RexBuilderTest.java | 73 +++++ .../apache/calcite/test/CalciteAssert.java | 3 + .../apache/calcite/test/RexProgramTest.java | 128 ++++++++- .../adapter/druid/DruidConnectionImpl.java | 2 +- .../adapter/druid/DruidDateTimeUtils.java | 74 +++-- .../calcite/adapter/druid/DruidQuery.java | 19 +- .../calcite/adapter/druid/DruidRules.java | 6 +- .../adapter/druid/DruidTableFactory.java | 2 +- .../druid/TimeExtractionDimensionSpec.java | 14 +- .../adapter/druid/TimeExtractionFunction.java | 21 +- .../adapter/druid/DruidQueryFilterTest.java | 2 +- .../apache/calcite/test/DruidAdapterIT.java | 252 +++++++++--------- .../calcite/test/DruidDateRangeRulesTest.java | 4 +- site/_docs/reference.md | 9 +- 38 files changed, 1361 insertions(+), 258 deletions(-) create mode 100644 core/src/main/java/org/apache/calcite/util/DateTimeStringUtils.java create mode 100644 core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java create mode 100644 core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java 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 0c1f54213d5..0b3aa8470be 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 @@ -1655,7 +1655,14 @@ public Expression implement(RexToLixTranslator translator, RexCall call, case 2: final Type type; final Method floorMethod; + Expression operand = translatedOperands.get(0); switch (call.getType().getSqlTypeName()) { + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + operand = Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, translator.getRoot())); + // fall through case TIMESTAMP: type = long.class; floorMethod = timestampMethod; @@ -1671,19 +1678,19 @@ public Expression implement(RexToLixTranslator translator, RexCall call, case YEAR: case MONTH: return Expressions.call(floorMethod, tur, - call(translatedOperands, type, TimeUnit.DAY)); + call(operand, type, TimeUnit.DAY)); default: - return call(translatedOperands, type, timeUnitRange.startUnit); + return call(operand, type, timeUnitRange.startUnit); } default: throw new AssertionError(); } } - private Expression call(List translatedOperands, Type type, + private Expression call(Expression operand, Type type, TimeUnit timeUnit) { return Expressions.call(SqlFunctions.class, methodName, - Types.castIfNecessary(type, translatedOperands.get(0)), + Types.castIfNecessary(type, operand), Types.castIfNecessary(type, Expressions.constant(timeUnit.multiplier))); } diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java index 10e969fe9d2..270db82f514 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java @@ -245,6 +245,14 @@ Expression translateCast( Expressions.call(BuiltInMethod.FLOOR_DIV.method, operand, Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), int.class); + break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_DATE.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); } break; case TIME: @@ -254,6 +262,14 @@ Expression translateCast( convert = Expressions.call(BuiltInMethod.STRING_TO_TIME.method, operand); break; + case TIME_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_TIME.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; case TIMESTAMP: convert = Expressions.convert_( Expressions.call( @@ -261,6 +277,49 @@ Expression translateCast( operand, Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), int.class); + break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + } + break; + case TIME_WITH_LOCAL_TIME_ZONE: + switch (sourceType.getSqlTypeName()) { + case CHAR: + case VARCHAR: + convert = + Expressions.call(BuiltInMethod.STRING_TO_TIME_WITH_LOCAL_TIME_ZONE.method, operand); + break; + case TIME: + convert = Expressions.call( + BuiltInMethod.TIME_STRING_TO_TIME_WITH_LOCAL_TIME_ZONE.method, + RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.UNIX_TIME_TO_STRING.method, + operand)), + Expressions.call(BuiltInMethod.TIME_ZONE.method, root)); + break; + case TIMESTAMP: + convert = Expressions.call( + BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, + operand)), + Expressions.call(BuiltInMethod.TIME_ZONE.method, root)); + break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME_WITH_LOCAL_TIME_ZONE.method, + operand)); } break; case TIMESTAMP: @@ -285,6 +344,82 @@ Expression translateCast( Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), Expressions.convert_(operand, long.class)); break; + case TIME_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP.method, + Expressions.call( + BuiltInMethod.UNIX_DATE_TO_STRING.method, + Expressions.call(BuiltInMethod.CURRENT_DATE.method, root)), + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + } + break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + switch (sourceType.getSqlTypeName()) { + case CHAR: + case VARCHAR: + convert = + Expressions.call( + BuiltInMethod.STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + operand); + break; + case DATE: + convert = Expressions.call( + BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, + Expressions.multiply( + Expressions.convert_(operand, long.class), + Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)))), + Expressions.call(BuiltInMethod.TIME_ZONE.method, root)); + break; + case TIME: + convert = Expressions.call( + BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, + Expressions.add( + Expressions.multiply( + Expressions.convert_( + Expressions.call(BuiltInMethod.CURRENT_DATE.method, root), + long.class), + Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), + Expressions.convert_(operand, long.class)))), + Expressions.call(BuiltInMethod.TIME_ZONE.method, root)); + break; + case TIME_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + Expressions.call( + BuiltInMethod.UNIX_DATE_TO_STRING.method, + Expressions.call(BuiltInMethod.CURRENT_DATE.method, root)), + operand)); + break; + case TIMESTAMP: + convert = Expressions.call( + BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, + operand)), + Expressions.call(BuiltInMethod.TIME_ZONE.method, root)); } break; case BOOLEAN: @@ -315,6 +450,14 @@ Expression translateCast( BuiltInMethod.UNIX_TIME_TO_STRING.method, operand)); break; + case TIME_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_STRING.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; case TIMESTAMP: convert = RexImpTable.optimize2( operand, @@ -322,6 +465,14 @@ Expression translateCast( BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, operand)); break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + convert = RexImpTable.optimize2( + operand, + Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_STRING.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -621,6 +772,7 @@ public static Expression translateLiteral( Expressions.constant(bd.toString())); case DATE: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -628,6 +780,7 @@ public static Expression translateLiteral( javaClass = int.class; break; case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: diff --git a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java index 2e0ded2b239..8999f44b5b3 100644 --- a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java +++ b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java @@ -176,12 +176,14 @@ public Type getJavaClass(RelDataType type) { return String.class; case DATE: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case INTEGER: case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: return type.isNullable() ? Integer.class : int.class; case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case BIGINT: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java index bd1234796d6..085fd7ca2a5 100644 --- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java +++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java @@ -281,6 +281,7 @@ public Double averageTypeValueSize(RelDataType type) { case DECIMAL: case DATE: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -289,6 +290,7 @@ public Double averageTypeValueSize(RelDataType type) { case DOUBLE: case FLOAT: // sic case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: @@ -339,6 +341,7 @@ public double typeValueSize(RelDataType type, Comparable value) { case REAL: case DATE: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -346,6 +349,7 @@ public double typeValueSize(RelDataType type, Comparable value) { case BIGINT: case DOUBLE: case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SortProjectTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SortProjectTransposeRule.java index e7b44675e94..127e95ec470 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/SortProjectTransposeRule.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/SortProjectTransposeRule.java @@ -23,6 +23,7 @@ import org.apache.calcite.plan.RelOptUtil; import org.apache.calcite.rel.RelCollation; import org.apache.calcite.rel.RelCollationTraitDef; +import org.apache.calcite.rel.RelCollations; import org.apache.calcite.rel.RelFieldCollation; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.Project; @@ -102,7 +103,7 @@ public void onMatch(RelOptRuleCall call) { final RexCall cast = (RexCall) node; final RexCallBinding binding = RexCallBinding.create(cluster.getTypeFactory(), cast, - ImmutableList.of(RexUtil.apply(map, sort.getCollation()))); + ImmutableList.of(RelCollations.of(RexUtil.apply(map, fc)))); if (cast.getOperator().getMonotonicity(binding) == SqlMonotonicity.NOT_MONOTONIC) { return; } diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java index 3e0eebd8731..b7b8839fdb8 100644 --- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java +++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java @@ -97,9 +97,11 @@ public int getMaxScale(SqlTypeName typeName) { case DOUBLE: return 15; case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case DATE: return 0; // SQL99 part 2 section 6.1 syntax rule 30 case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: // farrago supports only 0 (see // SqlTypeName.getDefaultPrecision), but it should be 6 // (microseconds) per SQL99 part 2 section 6.1 syntax rule 30. @@ -120,7 +122,9 @@ public int getMaxScale(SqlTypeName typeName) { case BINARY: return 65536; case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return SqlTypeName.MAX_DATETIME_PRECISION; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: @@ -159,6 +163,8 @@ public int getMaxScale(SqlTypeName typeName) { return isPrefix ? "x'" : "'"; case TIMESTAMP: return isPrefix ? "TIMESTAMP '" : "'"; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return isPrefix ? "TIMESTAMP WITH LOCAL TIME ZONE '" : "'"; case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: @@ -176,6 +182,8 @@ public int getMaxScale(SqlTypeName typeName) { return isPrefix ? "INTERVAL '" : "' YEAR TO MONTH"; case TIME: return isPrefix ? "TIME '" : "'"; + case TIME_WITH_LOCAL_TIME_ZONE: + return isPrefix ? "TIME WITH LOCAL TIME ZONE '" : "'"; case DATE: return isPrefix ? "DATE '" : "'"; case ARRAY: diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java index adf7fee5c70..2144ab81a13 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -863,6 +863,14 @@ protected RexLiteral makeLiteral( } o = ((TimeString) o).round(p); break; + case TIME_WITH_LOCAL_TIME_ZONE: + assert o instanceof TimeString; + p = type.getPrecision(); + if (p == RelDataType.PRECISION_NOT_SPECIFIED) { + p = 0; + } + o = ((TimeString) o).round(p); + break; case TIMESTAMP: assert o instanceof TimestampString; p = type.getPrecision(); @@ -871,6 +879,13 @@ protected RexLiteral makeLiteral( } o = ((TimestampString) o).round(p); break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + assert o instanceof TimestampString; + p = type.getPrecision(); + if (p == RelDataType.PRECISION_NOT_SPECIFIED) { + p = 0; + } + o = ((TimestampString) o).round(p); } return new RexLiteral(o, type, typeName); } @@ -1084,6 +1099,17 @@ public RexLiteral makeTimeLiteral(TimeString time, int precision) { SqlTypeName.TIME); } + /** + * Creates a Time with local time-zone literal. + */ + public RexLiteral makeTimeWithLocalTimeZoneLiteral( + TimeString time, + int precision) { + return makeLiteral(Preconditions.checkNotNull(time), + typeFactory.createSqlType(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, precision), + SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); + } + /** @deprecated Use {@link #makeTimestampLiteral(TimestampString, int)}. */ @Deprecated // to be removed before 2.0 public RexLiteral makeTimestampLiteral(Calendar calendar, int precision) { @@ -1101,6 +1127,17 @@ public RexLiteral makeTimestampLiteral(TimestampString timestamp, SqlTypeName.TIMESTAMP); } + /** + * Creates a Timestamp with local time-zone literal. + */ + public RexLiteral makeTimestampWithLocalTimeZoneLiteral( + TimestampString timestamp, + int precision) { + return makeLiteral(Preconditions.checkNotNull(timestamp), + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, precision), + SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + } + /** * Creates a literal representing an interval type, for example * {@code YEAR TO MONTH} or {@code DOW}. @@ -1225,6 +1262,10 @@ private static Comparable zeroValue(RelDataType type) { case DATE: case TIMESTAMP: return DateTimeUtils.ZERO_CALENDAR; + case TIME_WITH_LOCAL_TIME_ZONE: + return new TimeString(0, 0, 0); + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return new TimestampString(0, 0, 0, 0, 0, 0); default: throw Util.unexpected(type.getSqlTypeName()); } @@ -1288,10 +1329,14 @@ public RexNode makeLiteral(Object value, RelDataType type, return (Boolean) value ? booleanTrue : booleanFalse; case TIME: return makeTimeLiteral((TimeString) value, type.getPrecision()); + case TIME_WITH_LOCAL_TIME_ZONE: + return makeTimeWithLocalTimeZoneLiteral((TimeString) value, type.getPrecision()); case DATE: return makeDateLiteral((DateString) value); case TIMESTAMP: return makeTimestampLiteral((TimestampString) value, type.getPrecision()); + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return makeTimestampWithLocalTimeZoneLiteral((TimestampString) value, type.getPrecision()); case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -1423,6 +1468,12 @@ private static Object clean(Object o, RelDataType type) { } else { return TimeString.fromMillisOfDay((Integer) o); } + case TIME_WITH_LOCAL_TIME_ZONE: + if (o instanceof TimeString) { + return o; + } else { + return TimeString.fromMillisOfDay((Integer) o); + } case DATE: if (o instanceof DateString) { return o; @@ -1445,6 +1496,12 @@ private static Object clean(Object o, RelDataType type) { } else { return TimestampString.fromMillisSinceEpoch((Long) o); } + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + if (o instanceof TimestampString) { + return o; + } else { + return TimestampString.fromMillisSinceEpoch((Long) o); + } default: return o; } diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java index 8c4c732778e..a5ed9188b3c 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java +++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java @@ -255,8 +255,12 @@ public static boolean valueMatchesType( return value instanceof DateString; case TIME: return value instanceof TimeString; + case TIME_WITH_LOCAL_TIME_ZONE: + return value instanceof TimeString; case TIMESTAMP: return value instanceof TimestampString; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return value instanceof TimestampString; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -498,10 +502,18 @@ private static void printAsJava( assert value instanceof TimeString; pw.print(value); break; + case TIME_WITH_LOCAL_TIME_ZONE: + assert value instanceof TimeString; + pw.print(value); + break; case TIMESTAMP: assert value instanceof TimestampString; pw.print(value); break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + assert value instanceof TimestampString; + pw.print(value); + break; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: @@ -724,9 +736,11 @@ public Object getValue2() { return getValueAs(String.class); case DECIMAL: case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return getValueAs(Long.class); case DATE: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: return getValueAs(Integer.class); default: return value; @@ -835,6 +849,12 @@ public T getValueAs(Class clazz) { return clazz.cast(((TimeString) value).toCalendar()); } break; + case TIME_WITH_LOCAL_TIME_ZONE: + if (clazz == Integer.class) { + // Milliseconds since 1970-01-01 00:00:00 + return clazz.cast(((TimeString) value).getMillisOfDay()); + } + break; case TIMESTAMP: if (clazz == Long.class) { // Milliseconds since 1970-01-01 00:00:00 @@ -844,6 +864,12 @@ public T getValueAs(Class clazz) { return clazz.cast(((TimestampString) value).toCalendar()); } break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + if (clazz == Long.class) { + // Milliseconds since 1970-01-01 00:00:00 + return clazz.cast(((TimestampString) value).getMillisSinceEpoch()); + } + break; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java index 67ac1b808ba..52c47950881 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java +++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java @@ -811,6 +811,7 @@ private RexNode simplifyCast(RexCall e) { case TIMESTAMP: return e; } + break; } final List reducedValues = new ArrayList<>(); executor.reduce(rexBuilder, ImmutableList.of(e), reducedValues); diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 6832ee4f44d..736034c6374 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -33,6 +33,8 @@ import org.apache.calcite.runtime.FlatLists.ComparableList; import org.apache.calcite.util.Bug; import org.apache.calcite.util.NumberUtil; +import org.apache.calcite.util.TimeWithTimeZoneString; +import org.apache.calcite.util.TimestampWithTimeZoneString; import java.math.BigDecimal; import java.math.BigInteger; @@ -1695,6 +1697,50 @@ public static java.sql.Time internalToTime(Integer v) { return v == null ? null : internalToTime(v.intValue()); } + public static Integer toTimeWithLocalTimeZone(String v) { + return v == null ? null : new TimeWithTimeZoneString(v) + .withTimeZone(DateTimeUtils.UTC_ZONE) + .getLocalTimeString() + .getMillisOfDay(); + } + + public static Integer toTimeWithLocalTimeZone(String v, TimeZone timeZone) { + return v == null ? null : new TimeWithTimeZoneString(v + " " + timeZone.getID()) + .withTimeZone(DateTimeUtils.UTC_ZONE) + .getLocalTimeString() + .getMillisOfDay(); + } + + public static int timeWithLocalTimeZoneToTime(int v, TimeZone timeZone) { + return TimeWithTimeZoneString.fromMillisOfDay(v) + .withTimeZone(timeZone) + .getLocalTimeString() + .getMillisOfDay(); + } + + public static long timeWithLocalTimeZoneToTimestamp(String date, int v, TimeZone timeZone) { + final TimeWithTimeZoneString tTZ = TimeWithTimeZoneString.fromMillisOfDay(v) + .withTimeZone(DateTimeUtils.UTC_ZONE); + return new TimestampWithTimeZoneString(date + " " + tTZ.toString()) + .withTimeZone(timeZone) + .getLocalTimestampString() + .getMillisSinceEpoch(); + } + + public static long timeWithLocalTimeZoneToTimestampWithLocalTimeZone(String date, int v) { + final TimeWithTimeZoneString tTZ = TimeWithTimeZoneString.fromMillisOfDay(v) + .withTimeZone(DateTimeUtils.UTC_ZONE); + return new TimestampWithTimeZoneString(date + " " + tTZ.toString()) + .getLocalTimestampString() + .getMillisSinceEpoch(); + } + + public static String timeWithLocalTimeZoneToString(int v, TimeZone timeZone) { + return TimeWithTimeZoneString.fromMillisOfDay(v) + .withTimeZone(timeZone) + .toString(); + } + /** Converts the internal representation of a SQL TIMESTAMP (long) to the Java * type used for UDF parameters ({@link java.sql.Timestamp}). */ public static java.sql.Timestamp internalToTimestamp(long v) { @@ -1705,6 +1751,53 @@ public static java.sql.Timestamp internalToTimestamp(Long v) { return v == null ? null : internalToTimestamp(v.longValue()); } + public static int timestampWithLocalTimeZoneToDate(long v, TimeZone timeZone) { + return TimestampWithTimeZoneString.fromMillisSinceEpoch(v) + .withTimeZone(timeZone) + .getLocalDateString() + .getDaysSinceEpoch(); + } + + public static int timestampWithLocalTimeZoneToTime(long v, TimeZone timeZone) { + return TimestampWithTimeZoneString.fromMillisSinceEpoch(v) + .withTimeZone(timeZone) + .getLocalTimeString() + .getMillisOfDay(); + } + + public static long timestampWithLocalTimeZoneToTimestamp(long v, TimeZone timeZone) { + return TimestampWithTimeZoneString.fromMillisSinceEpoch(v) + .withTimeZone(timeZone) + .getLocalTimestampString() + .getMillisSinceEpoch(); + } + + public static String timestampWithLocalTimeZoneToString(long v, TimeZone timeZone) { + return TimestampWithTimeZoneString.fromMillisSinceEpoch(v) + .withTimeZone(timeZone) + .toString(); + } + + public static int timestampWithLocalTimeZoneToTimeWithLocalTimeZone(long v) { + return TimestampWithTimeZoneString.fromMillisSinceEpoch(v) + .getLocalTimeString() + .getMillisOfDay(); + } + + public static Long toTimestampWithLocalTimeZone(String v) { + return v == null ? null : new TimestampWithTimeZoneString(v) + .withTimeZone(DateTimeUtils.UTC_ZONE) + .getLocalTimestampString() + .getMillisSinceEpoch(); + } + + public static Long toTimestampWithLocalTimeZone(String v, TimeZone timeZone) { + return v == null ? null : new TimestampWithTimeZoneString(v + " " + timeZone.getID()) + .withTimeZone(DateTimeUtils.UTC_ZONE) + .getLocalTimestampString() + .getMillisSinceEpoch(); + } + // Don't need shortValueOf etc. - Short.valueOf is sufficient. /** Helper for CAST(... AS VARCHAR(maxLength)). */ @@ -1867,6 +1960,11 @@ public static int localTime(DataContext root) { return (int) (localTimestamp(root) % DateTimeUtils.MILLIS_PER_DAY); } + @NonDeterministic + public static TimeZone timeZone(DataContext root) { + return (TimeZone) DataContext.Variable.TIME_ZONE.get(root); + } + /** SQL {@code TRANSLATE(string, search_chars, replacement_chars)} * function. */ public static String translate3(String s, String search, String replacement) { diff --git a/core/src/main/java/org/apache/calcite/schema/Schemas.java b/core/src/main/java/org/apache/calcite/schema/Schemas.java index edb833a82bf..6466d31531e 100644 --- a/core/src/main/java/org/apache/calcite/schema/Schemas.java +++ b/core/src/main/java/org/apache/calcite/schema/Schemas.java @@ -54,7 +54,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.TimeZone; /** * Utility functions for schemas. @@ -564,8 +563,7 @@ private static class DummyDataContext implements DataContext { DummyDataContext(CalciteConnection connection, SchemaPlus rootSchema) { this.connection = connection; this.rootSchema = rootSchema; - this.map = - ImmutableMap.of("timeZone", TimeZone.getDefault()); + this.map = ImmutableMap.of(); } public SchemaPlus getRootSchema() { diff --git a/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java b/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java index 90b05979c70..2f9d974c6ea 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java @@ -34,7 +34,9 @@ public enum SqlJdbcDataTypeName { SQL_VARCHAR(SqlTypeName.VARCHAR), SQL_DATE(SqlTypeName.DATE), SQL_TIME(SqlTypeName.TIME), + SQL_TIME_WITH_LOCAL_TIME_ZONE(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE), SQL_TIMESTAMP(SqlTypeName.TIMESTAMP), + SQL_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE), SQL_DECIMAL(SqlTypeName.DECIMAL), SQL_NUMERIC(SqlTypeName.DECIMAL), SQL_BOOLEAN(SqlTypeName.BOOLEAN), diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRules.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRules.java index f77d639034d..cd46c392a3d 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRules.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRules.java @@ -158,9 +158,17 @@ private SqlTypeAssignmentRules() { rule.add(SqlTypeName.TIMESTAMP); rules.put(SqlTypeName.TIME, rule); + // Time with local time-zone is assignable from ... + rules.put(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, + EnumSet.of(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)); + // Timestamp is assignable from ... rules.put(SqlTypeName.TIMESTAMP, EnumSet.of(SqlTypeName.TIMESTAMP)); + // Timestamp with local time-zone is assignable from ... + rules.put(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, + EnumSet.of(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)); + // Geometry is assignable from ... rules.put(SqlTypeName.GEOMETRY, EnumSet.of(SqlTypeName.GEOMETRY)); @@ -275,6 +283,7 @@ private SqlTypeAssignmentRules() { rule = new HashSet<>(); rule.add(SqlTypeName.DATE); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rule.add(SqlTypeName.CHAR); rule.add(SqlTypeName.VARCHAR); coerceRules.put(SqlTypeName.DATE, rule); @@ -282,19 +291,44 @@ private SqlTypeAssignmentRules() { // Time is castable from ... rule = new HashSet<>(); rule.add(SqlTypeName.TIME); + rule.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rule.add(SqlTypeName.CHAR); rule.add(SqlTypeName.VARCHAR); coerceRules.put(SqlTypeName.TIME, rule); + // Time with local time-zone is castable from ... + rule = new HashSet<>(); + rule.add(SqlTypeName.TIME); + rule.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); + rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + rule.add(SqlTypeName.CHAR); + rule.add(SqlTypeName.VARCHAR); + coerceRules.put(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, rule); + // Timestamp is castable from ... rule = new HashSet<>(); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rule.add(SqlTypeName.DATE); rule.add(SqlTypeName.TIME); + rule.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); rule.add(SqlTypeName.CHAR); rule.add(SqlTypeName.VARCHAR); coerceRules.put(SqlTypeName.TIMESTAMP, rule); + + // Timestamp with local time-zone is castable from ... + rule = new HashSet<>(); + rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + rule.add(SqlTypeName.DATE); + rule.add(SqlTypeName.TIME); + rule.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); + rule.add(SqlTypeName.CHAR); + rule.add(SqlTypeName.VARCHAR); + coerceRules.put(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, rule); } //~ Methods ---------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java index e88f96aa3f6..af1733b1967 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java @@ -99,7 +99,9 @@ public enum SqlTypeFamily implements RelDataTypeFamily { .put(Types.DATE, DATE) .put(Types.TIME, TIME) + .put(Types.TIME_WITH_TIMEZONE, TIME) .put(Types.TIMESTAMP, TIMESTAMP) + .put(Types.TIMESTAMP_WITH_TIMEZONE, TIMESTAMP) .put(Types.BOOLEAN, BOOLEAN) .put(ExtraSqlTypes.REF_CURSOR, CURSOR) @@ -130,9 +132,9 @@ public Collection getTypeNames() { case DATE: return ImmutableList.of(SqlTypeName.DATE); case TIME: - return ImmutableList.of(SqlTypeName.TIME); + return ImmutableList.of(SqlTypeName.TIME, SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); case TIMESTAMP: - return ImmutableList.of(SqlTypeName.TIMESTAMP); + return ImmutableList.of(SqlTypeName.TIMESTAMP, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); case BOOLEAN: return SqlTypeName.BOOLEAN_TYPES; case INTERVAL_YEAR_MONTH: diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java index fcae1ec074f..e56b9cf1eb2 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java @@ -63,8 +63,12 @@ public enum SqlTypeName { DATE(PrecScale.NO_NO, false, Types.DATE, SqlTypeFamily.DATE), TIME(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIME, SqlTypeFamily.TIME), + TIME_WITH_LOCAL_TIME_ZONE(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.OTHER, + SqlTypeFamily.TIME), TIMESTAMP(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIMESTAMP, SqlTypeFamily.TIMESTAMP), + TIMESTAMP_WITH_LOCAL_TIME_ZONE(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.OTHER, + SqlTypeFamily.TIMESTAMP), INTERVAL_YEAR(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.INTERVAL_YEAR_MONTH), INTERVAL_YEAR_MONTH(PrecScale.NO_NO, false, Types.OTHER, @@ -147,7 +151,7 @@ public enum SqlTypeName { INTERVAL_DAY, INTERVAL_DAY_HOUR, INTERVAL_DAY_MINUTE, INTERVAL_DAY_SECOND, INTERVAL_HOUR, INTERVAL_HOUR_MINUTE, INTERVAL_HOUR_SECOND, INTERVAL_MINUTE, INTERVAL_MINUTE_SECOND, - INTERVAL_SECOND, + INTERVAL_SECOND, TIME_WITH_LOCAL_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE, FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST); public static final List BOOLEAN_TYPES = @@ -178,7 +182,8 @@ public enum SqlTypeName { combine(CHAR_TYPES, BINARY_TYPES); public static final List DATETIME_TYPES = - ImmutableList.of(DATE, TIME, TIMESTAMP); + ImmutableList.of(DATE, TIME, TIME_WITH_LOCAL_TIME_ZONE, + TIMESTAMP, TIMESTAMP_WITH_LOCAL_TIME_ZONE); public static final Set YEAR_INTERVAL_TYPES = Sets.immutableEnumSet(SqlTypeName.INTERVAL_YEAR, @@ -740,7 +745,9 @@ public int getMinPrecision() { case VARBINARY: case BINARY: case TIME: + case TIME_WITH_LOCAL_TIME_ZONE: case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return 1; case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: 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 8940629c125..14bff940e89 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java @@ -678,6 +678,14 @@ public RexNode convertExtract( case INTERVAL_MINUTE_SECOND: case INTERVAL_SECOND: break; + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + RelDataType type = + cx.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP); + type = cx.getTypeFactory().createTypeWithNullability( + type, + exprs.get(1).getType().isNullable()); + res = rexBuilder.makeCast(type, res); + // fall through case TIMESTAMP: res = divide(rexBuilder, res, TimeUnit.DAY.multiplier); // fall through @@ -690,6 +698,14 @@ public RexNode convertExtract( break; case DECADE: switch (sqlTypeName) { + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + RelDataType type = + cx.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP); + type = cx.getTypeFactory().createTypeWithNullability( + type, + exprs.get(1).getType().isNullable()); + res = rexBuilder.makeCast(type, res); + // fall through case TIMESTAMP: res = divide(rexBuilder, res, TimeUnit.DAY.multiplier); // fall through @@ -709,6 +725,16 @@ public RexNode convertExtract( case TIMESTAMP: // convert to seconds return divide(rexBuilder, res, TimeUnit.SECOND.multiplier); + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + RelDataType type = + cx.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP); + type = cx.getTypeFactory().createTypeWithNullability( + type, + exprs.get(1).getType().isNullable()); + return divide( + rexBuilder, + rexBuilder.makeCast(type, res), + TimeUnit.SECOND.multiplier); case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: case INTERVAL_MONTH: diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 3265db56c9d..648f1b06098 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -297,8 +297,33 @@ public enum BuiltInMethod { INTERNAL_TO_TIMESTAMP(SqlFunctions.class, "internalToTimestamp", long.class), STRING_TO_DATE(DateTimeUtils.class, "dateStringToUnixDate", String.class), STRING_TO_TIME(DateTimeUtils.class, "timeStringToUnixDate", String.class), - STRING_TO_TIMESTAMP(DateTimeUtils.class, "timestampStringToUnixDate", + STRING_TO_TIMESTAMP(DateTimeUtils.class, "timestampStringToUnixDate", String.class), + STRING_TO_TIME_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, "toTimeWithLocalTimeZone", String.class), + TIME_STRING_TO_TIME_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, "toTimeWithLocalTimeZone", + String.class, TimeZone.class), + STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, "toTimestampWithLocalTimeZone", + String.class), + TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, + "toTimestampWithLocalTimeZone", String.class, TimeZone.class), + TIME_WITH_LOCAL_TIME_ZONE_TO_TIME(SqlFunctions.class, "timeWithLocalTimeZoneToTime", + int.class, TimeZone.class), + TIME_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP(SqlFunctions.class, "timeWithLocalTimeZoneToTimestamp", + String.class, int.class, TimeZone.class), + TIME_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, + "timeWithLocalTimeZoneToTimestampWithLocalTimeZone", String.class, int.class), + TIME_WITH_LOCAL_TIME_ZONE_TO_STRING(SqlFunctions.class, "timeWithLocalTimeZoneToString", + int.class, TimeZone.class), + TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_DATE(SqlFunctions.class, "timestampWithLocalTimeZoneToDate", + long.class, TimeZone.class), + TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME(SqlFunctions.class, "timestampWithLocalTimeZoneToTime", + long.class, TimeZone.class), + TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, + "timestampWithLocalTimeZoneToTimeWithLocalTimeZone", long.class), + TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP(SqlFunctions.class, + "timestampWithLocalTimeZoneToTimestamp", long.class, TimeZone.class), + TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_STRING(SqlFunctions.class, + "timestampWithLocalTimeZoneToString", long.class, TimeZone.class), UNIX_DATE_TO_STRING(DateTimeUtils.class, "unixDateToString", int.class), UNIX_TIME_TO_STRING(DateTimeUtils.class, "unixTimeToString", int.class), UNIX_TIMESTAMP_TO_STRING(DateTimeUtils.class, "unixTimestampToString", @@ -322,6 +347,7 @@ public enum BuiltInMethod { CURRENT_DATE(SqlFunctions.class, "currentDate", DataContext.class), LOCAL_TIMESTAMP(SqlFunctions.class, "localTimestamp", DataContext.class), LOCAL_TIME(SqlFunctions.class, "localTime", DataContext.class), + TIME_ZONE(SqlFunctions.class, "timeZone", DataContext.class), BOOLEAN_TO_STRING(SqlFunctions.class, "toString", boolean.class), JDBC_ARRAY_TO_LIST(SqlFunctions.class, "arrayToList", java.sql.Array.class), OBJECT_TO_STRING(Object.class, "toString"), diff --git a/core/src/main/java/org/apache/calcite/util/DateString.java b/core/src/main/java/org/apache/calcite/util/DateString.java index cea9df31d5d..5aaa1b8af8f 100644 --- a/core/src/main/java/org/apache/calcite/util/DateString.java +++ b/core/src/main/java/org/apache/calcite/util/DateString.java @@ -42,7 +42,7 @@ public DateString(String v) { /** Creates a DateString for year, month, day values. */ public DateString(int year, int month, int day) { - this(TimestampString.ymd(new StringBuilder(), year, month, day).toString()); + this(DateTimeStringUtils.ymd(new StringBuilder(), year, month, day).toString()); } @Override public String toString() { diff --git a/core/src/main/java/org/apache/calcite/util/DateTimeStringUtils.java b/core/src/main/java/org/apache/calcite/util/DateTimeStringUtils.java new file mode 100644 index 00000000000..c0d38df4182 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/DateTimeStringUtils.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.util; + +import java.util.TimeZone; + +/** + * Utility methods to manipulate String representation of DateTime values. + */ +public class DateTimeStringUtils { + + private DateTimeStringUtils() {} + + static String pad(int length, long v) { + StringBuilder s = new StringBuilder(Long.toString(v)); + while (s.length() < length) { + s.insert(0, "0"); + } + return s.toString(); + } + + static StringBuilder hms(StringBuilder b, int h, int m, int s) { + int2(b, h); + b.append(':'); + int2(b, m); + b.append(':'); + int2(b, s); + return b; + } + + static StringBuilder ymdhms(StringBuilder b, int year, int month, int day, + int h, int m, int s) { + ymd(b, year, month, day); + b.append(' '); + hms(b, h, m, s); + return b; + } + + static StringBuilder ymd(StringBuilder b, int year, int month, int day) { + int4(b, year); + b.append('-'); + int2(b, month); + b.append('-'); + int2(b, day); + return b; + } + + private static void int4(StringBuilder buf, int i) { + buf.append((char) ('0' + (i / 1000) % 10)); + buf.append((char) ('0' + (i / 100) % 10)); + buf.append((char) ('0' + (i / 10) % 10)); + buf.append((char) ('0' + i % 10)); + } + + private static void int2(StringBuilder buf, int i) { + buf.append((char) ('0' + (i / 10) % 10)); + buf.append((char) ('0' + i % 10)); + } + + static boolean isValidTimeZone(final String timeZone) { + if (timeZone.equals("GMT")) { + return true; + } else { + String id = TimeZone.getTimeZone(timeZone).getID(); + if (!id.equals("GMT")) { + return true; + } + } + return false; + } + +} + +// End DateTimeStringUtils.java diff --git a/core/src/main/java/org/apache/calcite/util/TimeString.java b/core/src/main/java/org/apache/calcite/util/TimeString.java index 75aa96b8320..ce03d87ca1b 100644 --- a/core/src/main/java/org/apache/calcite/util/TimeString.java +++ b/core/src/main/java/org/apache/calcite/util/TimeString.java @@ -44,7 +44,7 @@ public TimeString(String v) { /** Creates a TimeString for hour, minute, second and millisecond values. */ public TimeString(int h, int m, int s) { - this(TimestampString.hms(new StringBuilder(), h, m, s).toString()); + this(DateTimeStringUtils.hms(new StringBuilder(), h, m, s).toString()); } /** Sets the fraction field of a {@code TimeString} to a given number @@ -55,7 +55,7 @@ public TimeString(int h, int m, int s) { * yields {@code TIME '1970-01-01 02:03:04.056'}. */ public TimeString withMillis(int millis) { Preconditions.checkArgument(millis >= 0 && millis < 1000); - return withFraction(TimestampString.pad(3, millis)); + return withFraction(DateTimeStringUtils.pad(3, millis)); } /** Sets the fraction field of a {@code TimeString} to a given number @@ -66,7 +66,7 @@ public TimeString withMillis(int millis) { * yields {@code TIME '1970-01-01 02:03:04.000056789'}. */ public TimeString withNanos(int nanos) { Preconditions.checkArgument(nanos >= 0 && nanos < 1000000000); - return withFraction(TimestampString.pad(9, nanos)); + return withFraction(DateTimeStringUtils.pad(9, nanos)); } /** Sets the fraction field of a {@code TimeString}. diff --git a/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java b/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java new file mode 100644 index 00000000000..6547f065ae0 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.util; + +import org.apache.calcite.avatica.util.DateTimeUtils; + +import com.google.common.base.Preconditions; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Time with time-zone literal. + * + *

Immutable, internally represented as a string (in ISO format), + * and can support unlimited precision (milliseconds, nanoseconds). + */ +public class TimeWithTimeZoneString implements Comparable { + + final TimeString localTime; + final TimeZone timeZone; + final String v; + + /** Creates a TimeWithTimeZoneString. */ + public TimeWithTimeZoneString(TimeString localTime, TimeZone timeZone) { + this.localTime = localTime; + this.timeZone = timeZone; + this.v = localTime.toString() + " " + timeZone.getID(); + } + + /** Creates a TimeWithTimeZoneString. */ + public TimeWithTimeZoneString(String v) { + this.localTime = new TimeString(v.substring(0, 8)); + String timeZoneString = v.substring(9); + Preconditions.checkArgument(DateTimeStringUtils.isValidTimeZone(timeZoneString)); + this.timeZone = TimeZone.getTimeZone(timeZoneString); + this.v = v; + } + + /** Creates a TimeWithTimeZoneString for hour, minute, second and millisecond values + * in the given time-zone. */ + public TimeWithTimeZoneString(int h, int m, int s, String timeZone) { + this(DateTimeStringUtils.hms(new StringBuilder(), h, m, s).toString() + " " + timeZone); + } + + /** Sets the fraction field of a {@code TimeWithTimeZoneString} to a given number + * of milliseconds. Nukes the value set via {@link #withNanos}. + * + *

For example, + * {@code new TimeWithTimeZoneString(1970, 1, 1, 2, 3, 4, "UTC").withMillis(56)} + * yields {@code TIME WITH LOCAL TIME ZONE '1970-01-01 02:03:04.056 UTC'}. */ + public TimeWithTimeZoneString withMillis(int millis) { + Preconditions.checkArgument(millis >= 0 && millis < 1000); + return withFraction(DateTimeStringUtils.pad(3, millis)); + } + + /** Sets the fraction field of a {@code TimeString} to a given number + * of nanoseconds. Nukes the value set via {@link #withMillis(int)}. + * + *

For example, + * {@code new TimeWithTimeZoneString(1970, 1, 1, 2, 3, 4, "UTC").withNanos(56789)} + * yields {@code TIME WITH LOCAL TIME ZONE '1970-01-01 02:03:04.000056789 UTC'}. */ + public TimeWithTimeZoneString withNanos(int nanos) { + Preconditions.checkArgument(nanos >= 0 && nanos < 1000000000); + return withFraction(DateTimeStringUtils.pad(9, nanos)); + } + + /** Sets the fraction field of a {@code TimeWithTimeZoneString}. + * The precision is determined by the number of leading zeros. + * Trailing zeros are stripped. + * + *

For example, + * {@code new TimeWithTimeZoneString(1970, 1, 1, 2, 3, 4, "UTC").withFraction("00506000")} + * yields {@code TIME WITH LOCAL TIME ZONE '1970-01-01 02:03:04.00506 UTC'}. */ + public TimeWithTimeZoneString withFraction(String fraction) { + String v = this.v; + int i = v.indexOf('.'); + if (i >= 0) { + v = v.substring(0, i); + } else { + v = v.substring(0, 8); + } + while (fraction.endsWith("0")) { + fraction = fraction.substring(0, fraction.length() - 1); + } + if (fraction.length() > 0) { + v = v + "." + fraction; + } + v = v + this.v.substring(8); // time-zone + return new TimeWithTimeZoneString(v); + } + + public TimeWithTimeZoneString withTimeZone(TimeZone timeZone) { + if (this.timeZone.equals(timeZone)) { + return this; + } + String localTimeString = localTime.toString(); + String v; + String fraction; + int i = localTimeString.indexOf('.'); + if (i >= 0) { + v = localTimeString.substring(0, i); + fraction = localTimeString.substring(i + 1); + } else { + v = localTimeString; + fraction = null; + } + final DateTimeUtils.PrecisionTime pt = + DateTimeUtils.parsePrecisionDateTimeLiteral(v, + new SimpleDateFormat(DateTimeUtils.TIME_FORMAT_STRING, Locale.ROOT), + this.timeZone, -1); + pt.getCalendar().setTimeZone(timeZone); + if (fraction != null) { + return new TimeWithTimeZoneString( + pt.getCalendar().get(Calendar.HOUR_OF_DAY), + pt.getCalendar().get(Calendar.MINUTE), + pt.getCalendar().get(Calendar.SECOND), + timeZone.getID()) + .withFraction(fraction); + } + return new TimeWithTimeZoneString( + pt.getCalendar().get(Calendar.HOUR_OF_DAY), + pt.getCalendar().get(Calendar.MINUTE), + pt.getCalendar().get(Calendar.SECOND), + timeZone.getID()); + } + + @Override public String toString() { + return v; + } + + @Override public boolean equals(Object o) { + // The value is in canonical form (no trailing zeros). + return o == this + || o instanceof TimeWithTimeZoneString + && ((TimeWithTimeZoneString) o).v.equals(v); + } + + @Override public int hashCode() { + return v.hashCode(); + } + + @Override public int compareTo(TimeWithTimeZoneString o) { + return v.compareTo(o.v); + } + + public TimeWithTimeZoneString round(int precision) { + Preconditions.checkArgument(precision >= 0); + return new TimeWithTimeZoneString( + localTime.round(precision), timeZone); + } + + public static TimeWithTimeZoneString fromMillisOfDay(int i) { + return new TimeWithTimeZoneString( + DateTimeUtils.unixTimeToString(i) + " " + DateTimeUtils.UTC_ZONE.getID()) + .withMillis((int) DateTimeUtils.floorMod(i, 1000)); + } + + /** Converts this TimeWithTimeZoneString to a string, truncated or padded with + * zeroes to a given precision. */ + public String toString(int precision) { + Preconditions.checkArgument(precision >= 0); + return localTime.toString(precision) + " " + timeZone.getID(); + } + + public TimeString getLocalTimeString() { + return localTime; + } + +} + +// End TimeWithTimeZoneString.java diff --git a/core/src/main/java/org/apache/calcite/util/TimestampString.java b/core/src/main/java/org/apache/calcite/util/TimestampString.java index 4e392f0b4ab..604d5a4ac3d 100644 --- a/core/src/main/java/org/apache/calcite/util/TimestampString.java +++ b/core/src/main/java/org/apache/calcite/util/TimestampString.java @@ -47,7 +47,7 @@ public TimestampString(String v) { /** Creates a TimestampString for year, month, day, hour, minute, second, * millisecond values. */ public TimestampString(int year, int month, int day, int h, int m, int s) { - this(ymdhms(new StringBuilder(), year, month, day, h, m, s).toString()); + this(DateTimeStringUtils.ymdhms(new StringBuilder(), year, month, day, h, m, s).toString()); } /** Sets the fraction field of a {@code TimestampString} to a given number @@ -58,7 +58,7 @@ public TimestampString(int year, int month, int day, int h, int m, int s) { * yields {@code TIMESTAMP '1970-01-01 02:03:04.056'}. */ public TimestampString withMillis(int millis) { Preconditions.checkArgument(millis >= 0 && millis < 1000); - return withFraction(pad(3, millis)); + return withFraction(DateTimeStringUtils.pad(3, millis)); } /** Sets the fraction field of a {@code TimestampString} to a given number @@ -69,7 +69,7 @@ public TimestampString withMillis(int millis) { * yields {@code TIMESTAMP '1970-01-01 02:03:04.000056789'}. */ public TimestampString withNanos(int nanos) { Preconditions.checkArgument(nanos >= 0 && nanos < 1000000000); - return withFraction(pad(9, nanos)); + return withFraction(DateTimeStringUtils.pad(9, nanos)); } /** Sets the fraction field of a {@code TimestampString}. @@ -113,44 +113,6 @@ public TimestampString withFraction(String fraction) { return v.compareTo(o.v); } - static StringBuilder hms(StringBuilder b, int h, int m, int s) { - int2(b, h); - b.append(':'); - int2(b, m); - b.append(':'); - int2(b, s); - return b; - } - - static StringBuilder ymdhms(StringBuilder b, int year, int month, int day, - int h, int m, int s) { - ymd(b, year, month, day); - b.append(' '); - hms(b, h, m, s); - return b; - } - - static StringBuilder ymd(StringBuilder b, int year, int month, int day) { - int4(b, year); - b.append('-'); - int2(b, month); - b.append('-'); - int2(b, day); - return b; - } - - private static void int4(StringBuilder buf, int i) { - buf.append((char) ('0' + (i / 1000) % 10)); - buf.append((char) ('0' + (i / 100) % 10)); - buf.append((char) ('0' + (i / 10) % 10)); - buf.append((char) ('0' + i % 10)); - } - - private static void int2(StringBuilder buf, int i) { - buf.append((char) ('0' + (i / 10) % 10)); - buf.append((char) ('0' + i % 10)); - } - /** Creates a TimestampString from a Calendar. */ public static TimestampString fromCalendarFields(Calendar calendar) { return new TimestampString( @@ -214,14 +176,6 @@ public static TimestampString fromMillisSinceEpoch(long millis) { .withMillis((int) DateTimeUtils.floorMod(millis, 1000)); } - static String pad(int length, long v) { - StringBuilder s = new StringBuilder(Long.toString(v)); - while (s.length() < length) { - s.insert(0, "0"); - } - return s.toString(); - } - public Calendar toCalendar() { return Util.calendar(getMillisSinceEpoch()); } diff --git a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java new file mode 100644 index 00000000000..3f327621925 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.util; + +import org.apache.calcite.avatica.util.DateTimeUtils; + +import com.google.common.base.Preconditions; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Timestamp with time-zone literal. + * + *

Immutable, internally represented as a string (in ISO format), + * and can support unlimited precision (milliseconds, nanoseconds). + */ +public class TimestampWithTimeZoneString + implements Comparable { + + final TimestampString localDateTime; + final TimeZone timeZone; + final String v; + + /** Creates a TimestampWithTimeZoneString. */ + public TimestampWithTimeZoneString(TimestampString localDateTime, TimeZone timeZone) { + this.localDateTime = localDateTime; + this.timeZone = timeZone; + this.v = localDateTime.toString() + " " + timeZone.getID(); + } + + /** Creates a TimestampWithTimeZoneString. */ + public TimestampWithTimeZoneString(String v) { + this.localDateTime = new TimestampString(v.substring(0, v.indexOf(' ', 11))); + String timeZoneString = v.substring(v.indexOf(' ', 11) + 1); + Preconditions.checkArgument(DateTimeStringUtils.isValidTimeZone(timeZoneString)); + this.timeZone = TimeZone.getTimeZone(timeZoneString); + this.v = v; + } + + /** Creates a TimestampWithTimeZoneString for year, month, day, hour, minute, second, + * millisecond values in the given time-zone. */ + public TimestampWithTimeZoneString(int year, int month, int day, int h, int m, int s, + String timeZone) { + this(DateTimeStringUtils.ymdhms(new StringBuilder(), year, month, day, h, m, s).toString() + + " " + timeZone); + } + + /** Sets the fraction field of a {@code TimestampWithTimeZoneString} to a given number + * of milliseconds. Nukes the value set via {@link #withNanos}. + * + *

For example, + * {@code new TimestampWithTimeZoneString(1970, 1, 1, 2, 3, 4, "GMT").withMillis(56)} + * yields {@code TIMESTAMP WITH LOCAL TIME ZONE '1970-01-01 02:03:04.056 GMT'}. */ + public TimestampWithTimeZoneString withMillis(int millis) { + Preconditions.checkArgument(millis >= 0 && millis < 1000); + return withFraction(DateTimeStringUtils.pad(3, millis)); + } + + /** Sets the fraction field of a {@code TimestampWithTimeZoneString} to a given number + * of nanoseconds. Nukes the value set via {@link #withMillis(int)}. + * + *

For example, + * {@code new TimestampWithTimeZoneString(1970, 1, 1, 2, 3, 4, "GMT").withNanos(56789)} + * yields {@code TIMESTAMP WITH LOCAL TIME ZONE '1970-01-01 02:03:04.000056789 GMT'}. */ + public TimestampWithTimeZoneString withNanos(int nanos) { + Preconditions.checkArgument(nanos >= 0 && nanos < 1000000000); + return withFraction(DateTimeStringUtils.pad(9, nanos)); + } + + /** Sets the fraction field of a {@code TimestampString}. + * The precision is determined by the number of leading zeros. + * Trailing zeros are stripped. + * + *

For example, {@code + * new TimestampWithTimeZoneString(1970, 1, 1, 2, 3, 4, "GMT").withFraction("00506000")} + * yields {@code TIMESTAMP WITH LOCAL TIME ZONE '1970-01-01 02:03:04.00506 GMT'}. */ + public TimestampWithTimeZoneString withFraction(String fraction) { + return new TimestampWithTimeZoneString( + localDateTime.withFraction(fraction), timeZone); + } + + public TimestampWithTimeZoneString withTimeZone(TimeZone timeZone) { + if (this.timeZone.equals(timeZone)) { + return this; + } + String localDateTimeString = localDateTime.toString(); + String v; + String fraction; + int i = localDateTimeString.indexOf('.'); + if (i >= 0) { + v = localDateTimeString.substring(0, i); + fraction = localDateTimeString.substring(i + 1); + } else { + v = localDateTimeString; + fraction = null; + } + final DateTimeUtils.PrecisionTime pt = + DateTimeUtils.parsePrecisionDateTimeLiteral(v, + new SimpleDateFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING, Locale.ROOT), + this.timeZone, -1); + pt.getCalendar().setTimeZone(timeZone); + if (fraction != null) { + return new TimestampWithTimeZoneString( + pt.getCalendar().get(Calendar.YEAR), + pt.getCalendar().get(Calendar.MONTH) + 1, + pt.getCalendar().get(Calendar.DAY_OF_MONTH), + pt.getCalendar().get(Calendar.HOUR_OF_DAY), + pt.getCalendar().get(Calendar.MINUTE), + pt.getCalendar().get(Calendar.SECOND), + timeZone.getID()) + .withFraction(fraction); + } + return new TimestampWithTimeZoneString( + pt.getCalendar().get(Calendar.YEAR), + pt.getCalendar().get(Calendar.MONTH) + 1, + pt.getCalendar().get(Calendar.DAY_OF_MONTH), + pt.getCalendar().get(Calendar.HOUR_OF_DAY), + pt.getCalendar().get(Calendar.MINUTE), + pt.getCalendar().get(Calendar.SECOND), + timeZone.getID()); + } + + @Override public String toString() { + return v; + } + + @Override public boolean equals(Object o) { + // The value is in canonical form (no trailing zeros). + return o == this + || o instanceof TimestampWithTimeZoneString + && ((TimestampWithTimeZoneString) o).v.equals(v); + } + + @Override public int hashCode() { + return v.hashCode(); + } + + @Override public int compareTo(TimestampWithTimeZoneString o) { + return v.compareTo(o.v); + } + + public TimestampWithTimeZoneString round(int precision) { + Preconditions.checkArgument(precision >= 0); + return new TimestampWithTimeZoneString( + localDateTime.round(precision), timeZone); + } + + /** Creates a TimestampWithTimeZoneString that is a given number of milliseconds since + * the epoch UTC. */ + public static TimestampWithTimeZoneString fromMillisSinceEpoch(long millis) { + return new TimestampWithTimeZoneString( + DateTimeUtils.unixTimestampToString(millis) + " " + DateTimeUtils.UTC_ZONE.getID()) + .withMillis((int) DateTimeUtils.floorMod(millis, 1000)); + } + + /** Converts this TimestampWithTimeZoneString to a string, truncated or padded with + * zeroes to a given precision. */ + public String toString(int precision) { + Preconditions.checkArgument(precision >= 0); + return localDateTime.toString(precision) + " " + timeZone.getID(); + } + + public DateString getLocalDateString() { + return new DateString(localDateTime.toString().substring(0, 10)); + } + + public TimeString getLocalTimeString() { + return new TimeString(localDateTime.toString().substring(11)); + } + + public TimestampString getLocalTimestampString() { + return localDateTime; + } + +} + +// End TimestampWithTimeZoneString.java diff --git a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java index 9dd3019ecbb..1e6f1a3f953 100644 --- a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java +++ b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java @@ -274,7 +274,7 @@ public AvaticaJsonHandler createHandler(Service service) { @Test public void testRemoteTypeInfo() throws Exception { CalciteAssert.hr().with(REMOTE_CONNECTION_FACTORY) .metaData(GET_TYPEINFO) - .returns(CalciteAssert.checkResultCount(is(43))); + .returns(CalciteAssert.checkResultCount(is(45))); } @Test public void testRemoteTableTypes() throws Exception { diff --git a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java index 0fd99f807f4..f9a61fc2e25 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java +++ b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java @@ -24,11 +24,13 @@ import org.apache.calcite.util.DateString; import org.apache.calcite.util.TimeString; import org.apache.calcite.util.TimestampString; +import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.Util; import org.junit.Test; import java.util.Calendar; +import java.util.TimeZone; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.core.Is.is; @@ -176,6 +178,77 @@ private void checkTimestamp(RexNode node) { assertThat(literal.getValueAs(TimestampString.class), notNullValue()); } + /** Tests + * {@link RexBuilder#makeTimestampWithLocalTimeZoneLiteral(TimestampWithTimeZoneString, int)}. */ + @Test public void testTimestampWithLocalTimeZoneLiteral() { + final RelDataTypeFactory typeFactory = + new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT); + final RelDataType timestampType = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + final RelDataType timestampType3 = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 3); + final RelDataType timestampType9 = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 9); + final RelDataType timestampType18 = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 18); + final RexBuilder builder = new RexBuilder(typeFactory); + + // The new way + final TimestampWithTimeZoneString ts = new TimestampWithTimeZoneString( + 1969, 7, 21, 2, 56, 15, TimeZone.getTimeZone("PST").getID()); + checkTimestampWithLocalTimeZone( + builder.makeLiteral(ts.getLocalTimestampString(), timestampType, false)); + + // Now with milliseconds + final TimestampWithTimeZoneString ts2 = ts.withMillis(56); + assertThat(ts2.toString(), is("1969-07-21 02:56:15.056 PST")); + final RexNode literal2 = builder.makeLiteral( + ts2.getLocalTimestampString(), timestampType3, false); + assertThat(((RexLiteral) literal2).getValue().toString(), is("1969-07-21 02:56:15.056")); + + // Now with nanoseconds + final TimestampWithTimeZoneString ts3 = ts.withNanos(56); + final RexNode literal3 = builder.makeLiteral( + ts3.getLocalTimestampString(), timestampType9, false); + assertThat(((RexLiteral) literal3).getValueAs(TimestampString.class) + .toString(), is("1969-07-21 02:56:15")); + final TimestampWithTimeZoneString ts3b = ts.withNanos(2345678); + final RexNode literal3b = builder.makeLiteral( + ts3b.getLocalTimestampString(), timestampType9, false); + assertThat(((RexLiteral) literal3b).getValueAs(TimestampString.class) + .toString(), is("1969-07-21 02:56:15.002")); + + // Now with a very long fraction + final TimestampWithTimeZoneString ts4 = ts.withFraction("102030405060708090102"); + final RexNode literal4 = builder.makeLiteral( + ts4.getLocalTimestampString(), timestampType18, false); + assertThat(((RexLiteral) literal4).getValueAs(TimestampString.class) + .toString(), is("1969-07-21 02:56:15.102")); + + // toString + assertThat(ts2.round(1).toString(), is("1969-07-21 02:56:15 PST")); + assertThat(ts2.round(2).toString(), is("1969-07-21 02:56:15.05 PST")); + assertThat(ts2.round(3).toString(), is("1969-07-21 02:56:15.056 PST")); + assertThat(ts2.round(4).toString(), is("1969-07-21 02:56:15.056 PST")); + + assertThat(ts2.toString(6), is("1969-07-21 02:56:15.056000 PST")); + assertThat(ts2.toString(1), is("1969-07-21 02:56:15.0 PST")); + assertThat(ts2.toString(0), is("1969-07-21 02:56:15 PST")); + + assertThat(ts2.round(0).toString(), is("1969-07-21 02:56:15 PST")); + assertThat(ts2.round(0).toString(0), is("1969-07-21 02:56:15 PST")); + assertThat(ts2.round(0).toString(1), is("1969-07-21 02:56:15.0 PST")); + assertThat(ts2.round(0).toString(2), is("1969-07-21 02:56:15.00 PST")); + } + + private void checkTimestampWithLocalTimeZone(RexNode node) { + assertThat(node.toString(), is("1969-07-21 02:56:15")); + RexLiteral literal = (RexLiteral) node; + assertThat(literal.getValue() instanceof TimestampString, is(true)); + assertThat(literal.getValue2() instanceof Long, is(true)); + assertThat(literal.getValue3() instanceof Long, is(true)); + } + /** Tests {@link RexBuilder#makeTimeLiteral(TimeString, int)}. */ @Test public void testTimeLiteral() { final RelDataTypeFactory typeFactory = diff --git a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java b/core/src/test/java/org/apache/calcite/test/CalciteAssert.java index 6db94350919..3e6b4143aef 100644 --- a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java +++ b/core/src/test/java/org/apache/calcite/test/CalciteAssert.java @@ -548,6 +548,9 @@ static void assertQuery( calciteConnection.getProperties().setProperty( CalciteConnectionProperty.CREATE_MATERIALIZATIONS.camelName(), Boolean.toString(materializationsEnabled)); + calciteConnection.getProperties().setProperty( + CalciteConnectionProperty.TIME_ZONE.camelName(), + DateTimeUtils.UTC_ZONE.getID()); } for (Pair hook : hooks) { closer.add(hook.left.addThread(hook.right)); diff --git a/core/src/test/java/org/apache/calcite/test/RexProgramTest.java b/core/src/test/java/org/apache/calcite/test/RexProgramTest.java index 7cbcfa6da9a..ccecc987f03 100644 --- a/core/src/test/java/org/apache/calcite/test/RexProgramTest.java +++ b/core/src/test/java/org/apache/calcite/test/RexProgramTest.java @@ -16,9 +16,11 @@ */ package org.apache.calcite.test; +import org.apache.calcite.DataContext; import org.apache.calcite.adapter.java.JavaTypeFactory; import org.apache.calcite.avatica.util.ByteString; import org.apache.calcite.jdbc.JavaTypeFactoryImpl; +import org.apache.calcite.linq4j.QueryProvider; import org.apache.calcite.plan.RelOptUtil; import org.apache.calcite.plan.Strong; import org.apache.calcite.rel.type.RelDataType; @@ -27,6 +29,8 @@ import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexCall; import org.apache.calcite.rex.RexDynamicParam; +import org.apache.calcite.rex.RexExecutor; +import org.apache.calcite.rex.RexExecutorImpl; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexLocalRef; @@ -35,6 +39,7 @@ import org.apache.calcite.rex.RexProgramBuilder; import org.apache.calcite.rex.RexSimplify; import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlSpecialOperator; @@ -48,6 +53,7 @@ import org.apache.calcite.util.TestUtil; import org.apache.calcite.util.TimeString; import org.apache.calcite.util.TimestampString; +import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.Util; import com.google.common.collect.ImmutableList; @@ -65,6 +71,7 @@ import java.util.Calendar; import java.util.List; import java.util.Map; +import java.util.TimeZone; import java.util.TreeMap; import static org.hamcrest.CoreMatchers.equalTo; @@ -100,7 +107,9 @@ public RexProgramTest() { public void setUp() { typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT); rexBuilder = new RexBuilder(typeFactory); - simplify = new RexSimplify(rexBuilder, false, RexUtil.EXECUTOR); + RexExecutor executor = + new RexExecutorImpl(new DummyTestDataContext()); + simplify = new RexSimplify(rexBuilder, false, executor); trueLiteral = rexBuilder.makeLiteral(true); falseLiteral = rexBuilder.makeLiteral(false); final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER); @@ -108,6 +117,34 @@ public void setUp() { unknownLiteral = rexBuilder.makeNullLiteral(trueLiteral.getType()); } + /** Dummy data context for test. */ + private static class DummyTestDataContext implements DataContext { + private final ImmutableMap map; + + DummyTestDataContext() { + this.map = + ImmutableMap.of( + Variable.TIME_ZONE.camelName, TimeZone.getTimeZone("America/Los_Angeles"), + Variable.CURRENT_TIMESTAMP.camelName, new Long(1311120000000L)); + } + + public SchemaPlus getRootSchema() { + return null; + } + + public JavaTypeFactory getTypeFactory() { + return null; + } + + public QueryProvider getQueryProvider() { + return null; + } + + public Object get(String name) { + return map.get(name); + } + } + private void checkCnf(RexNode node, String expected) { assertThat(RexUtil.toCnf(rexBuilder, node).toString(), equalTo(expected)); } @@ -1491,6 +1528,95 @@ private void checkExponentialCnf(int n) { "1970-01-01 00:00:00"); // different from Hive } + @Test public void testSimplifyCastLiteral3() { + // Default TimeZone is "America/Los_Angeles" (DummyDataContext) + final RexLiteral literalDate = rexBuilder.makeDateLiteral(new DateString("2011-07-20")); + final RexLiteral literalTime = rexBuilder.makeTimeLiteral(new TimeString("12:34:56"), 0); + final RexLiteral literalTimestamp = rexBuilder.makeTimestampLiteral( + new TimestampString("2011-07-20 12:34:56"), 0); + final RexLiteral literalTimeLTZ = + rexBuilder.makeTimeWithLocalTimeZoneLiteral( + new TimeString(1, 23, 45), 0); + final RexLiteral timeLTZChar1 = rexBuilder.makeLiteral("12:34:45 America/Los_Angeles"); + final RexLiteral timeLTZChar2 = rexBuilder.makeLiteral("12:34:45 UTC"); + final RexLiteral timeLTZChar3 = rexBuilder.makeLiteral("12:34:45 GMT+01"); + final RexLiteral timestampLTZChar1 = rexBuilder.makeLiteral("2011-07-20 12:34:56 Asia/Tokyo"); + final RexLiteral timestampLTZChar2 = rexBuilder.makeLiteral("2011-07-20 12:34:56 GMT+01"); + final RexLiteral timestampLTZChar3 = rexBuilder.makeLiteral("2011-07-20 12:34:56 UTC"); + final RexLiteral literalTimestampLTZ = + rexBuilder.makeTimestampWithLocalTimeZoneLiteral( + new TimestampString(2011, 7, 20, 8, 23, 45), 0); + + final RelDataType dateType = + typeFactory.createSqlType(SqlTypeName.DATE); + final RelDataType timeType = + typeFactory.createSqlType(SqlTypeName.TIME); + final RelDataType timestampType = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP); + final RelDataType timeLTZType = + typeFactory.createSqlType(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE); + final RelDataType timestampLTZType = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + final RelDataType varCharType = + typeFactory.createSqlType(SqlTypeName.VARCHAR, 40); + + checkSimplify(cast(timeLTZChar1, timeLTZType), "20:34:45"); + checkSimplify(cast(timeLTZChar2, timeLTZType), "12:34:45"); + checkSimplify(cast(timeLTZChar3, timeLTZType), "11:34:45"); + checkSimplify(cast(literalTimeLTZ, timeLTZType), "01:23:45"); + checkSimplify(cast(timestampLTZChar1, timestampLTZType), + "2011-07-20 03:34:56"); + checkSimplify(cast(timestampLTZChar2, timestampLTZType), + "2011-07-20 11:34:56"); + checkSimplify(cast(timestampLTZChar3, timestampLTZType), + "2011-07-20 12:34:56"); + checkSimplify(cast(literalTimestampLTZ, timestampLTZType), + "2011-07-20 08:23:45"); + checkSimplify(cast(literalDate, timestampLTZType), + "2011-07-20 07:00:00"); + checkSimplify(cast(literalTime, timestampLTZType), + "2011-07-20 19:34:56"); + checkSimplify(cast(literalTimestamp, timestampLTZType), + "2011-07-20 19:34:56"); + checkSimplify(cast(literalTimestamp, dateType), + "2011-07-20"); + checkSimplify(cast(literalTimestampLTZ, dateType), + "2011-07-20"); + checkSimplify(cast(literalTimestampLTZ, timeType), + "01:23:45"); + checkSimplify(cast(literalTimestampLTZ, timestampType), + "2011-07-20 01:23:45"); + checkSimplify(cast(literalTimeLTZ, timeType), + "17:23:45"); + checkSimplify(cast(literalTime, timeLTZType), + "20:34:56"); + checkSimplify(cast(literalTimestampLTZ, timeLTZType), + "08:23:45"); + checkSimplify(cast(literalTimeLTZ, varCharType), + "'17:23:45 America/Los_Angeles'"); + checkSimplify(cast(literalTimestampLTZ, varCharType), + "'2011-07-20 01:23:45 America/Los_Angeles'"); + checkSimplify(cast(literalTimeLTZ, timestampType), + "2011-07-19 18:23:45"); + checkSimplify(cast(literalTimeLTZ, timestampLTZType), + "2011-07-20 01:23:45"); + } + + @Test public void testCompareTimestampWithTimeZone() { + final TimestampWithTimeZoneString timestampLTZChar1 = + new TimestampWithTimeZoneString("2011-07-20 10:34:56 America/Los_Angeles"); + final TimestampWithTimeZoneString timestampLTZChar2 = + new TimestampWithTimeZoneString("2011-07-20 19:34:56 Europe/Rome"); + final TimestampWithTimeZoneString timestampLTZChar3 = + new TimestampWithTimeZoneString("2011-07-20 01:34:56 Asia/Tokyo"); + final TimestampWithTimeZoneString timestampLTZChar4 = + new TimestampWithTimeZoneString("2011-07-20 10:34:56 America/Los_Angeles"); + + assertThat(timestampLTZChar1.equals(timestampLTZChar2), is(false)); + assertThat(timestampLTZChar1.equals(timestampLTZChar3), is(false)); + assertThat(timestampLTZChar1.equals(timestampLTZChar4), is(true)); + } + @Test public void testSimplifyLiterals() { final RexLiteral literalAbc = rexBuilder.makeLiteral("abc"); final RexLiteral literalDef = rexBuilder.makeLiteral("def"); diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidConnectionImpl.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidConnectionImpl.java index 3be8d203602..1951396939e 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidConnectionImpl.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidConnectionImpl.java @@ -512,7 +512,7 @@ void metadata(String dataSourceName, String timestampColumnName, JsonSegmentMetadata.class); final List list = mapper.readValue(in, listType); in.close(); - fieldBuilder.put(timestampColumnName, SqlTypeName.TIMESTAMP); + fieldBuilder.put(timestampColumnName, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); for (JsonSegmentMetadata o : list) { for (Map.Entry entry : o.columns.entrySet()) { if (entry.getKey().equals(DruidTable.DEFAULT_TIMESTAMP_COLUMN)) { diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java index 03288824c28..fb693530e9a 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidDateTimeUtils.java @@ -16,6 +16,7 @@ */ package org.apache.calcite.adapter.druid; +import org.apache.calcite.avatica.util.DateTimeUtils; import org.apache.calcite.avatica.util.TimeUnitRange; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rex.RexCall; @@ -26,6 +27,7 @@ import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.util.DateString; import org.apache.calcite.util.TimestampString; +import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.Util; import org.apache.calcite.util.trace.CalciteTrace; @@ -40,6 +42,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.TimeZone; /** * Utilities for generating intervals from RexNode. @@ -57,9 +60,9 @@ private DruidDateTimeUtils() { * expression. Assumes that all the predicates in the input * reference a single column: the timestamp column. */ - public static List createInterval(RelDataType type, - RexNode e) { - final List> ranges = extractRanges(e, false); + public static List createInterval(RexNode e, String timeZone) { + final List> ranges = + extractRanges(e, TimeZone.getTimeZone(timeZone), false); if (ranges == null) { // We did not succeed, bail out return null; @@ -71,10 +74,12 @@ public static List createInterval(RelDataType type, if (LOGGER.isDebugEnabled()) { LOGGER.debug("Inferred ranges on interval : " + condensedRanges); } - return toInterval(ImmutableList.copyOf(condensedRanges.asRanges())); + return toInterval( + ImmutableList.copyOf(condensedRanges.asRanges())); } - protected static List toInterval(List> ranges) { + protected static List toInterval( + List> ranges) { List intervals = Lists.transform(ranges, new Function, LocalInterval>() { public LocalInterval apply(Range range) { @@ -105,7 +110,7 @@ public LocalInterval apply(Range range) { } protected static List> extractRanges(RexNode node, - boolean withNot) { + TimeZone timeZone, boolean withNot) { switch (node.getKind()) { case EQUALS: case LESS_THAN: @@ -114,16 +119,17 @@ protected static List> extractRanges(RexNode node, case GREATER_THAN_OR_EQUAL: case BETWEEN: case IN: - return leafToRanges((RexCall) node, withNot); + return leafToRanges((RexCall) node, timeZone, withNot); case NOT: - return extractRanges(((RexCall) node).getOperands().get(0), !withNot); + return extractRanges(((RexCall) node).getOperands().get(0), timeZone, !withNot); case OR: { RexCall call = (RexCall) node; List> intervals = Lists.newArrayList(); for (RexNode child : call.getOperands()) { - List> extracted = extractRanges(child, withNot); + List> extracted = + extractRanges(child, timeZone, withNot); if (extracted != null) { intervals.addAll(extracted); } @@ -135,7 +141,8 @@ protected static List> extractRanges(RexNode node, RexCall call = (RexCall) node; List> ranges = new ArrayList<>(); for (RexNode child : call.getOperands()) { - List> extractedRanges = extractRanges(child, false); + List> extractedRanges = + extractRanges(child, timeZone, false); if (extractedRanges == null || extractedRanges.isEmpty()) { // We could not extract, we bail out return null; @@ -163,7 +170,7 @@ protected static List> extractRanges(RexNode node, } protected static List> leafToRanges(RexCall call, - boolean withNot) { + TimeZone timeZone, boolean withNot) { switch (call.getKind()) { case EQUALS: case LESS_THAN: @@ -173,11 +180,11 @@ protected static List> leafToRanges(RexCall call, { final TimestampString value; if (call.getOperands().get(0) instanceof RexInputRef - && literalValue(call.getOperands().get(1)) != null) { - value = literalValue(call.getOperands().get(1)); + && literalValue(call.getOperands().get(1), timeZone) != null) { + value = literalValue(call.getOperands().get(1), timeZone); } else if (call.getOperands().get(1) instanceof RexInputRef - && literalValue(call.getOperands().get(0)) != null) { - value = literalValue(call.getOperands().get(0)); + && literalValue(call.getOperands().get(0), timeZone) != null) { + value = literalValue(call.getOperands().get(0), timeZone); } else { return null; } @@ -201,10 +208,10 @@ && literalValue(call.getOperands().get(0)) != null) { { final TimestampString value1; final TimestampString value2; - if (literalValue(call.getOperands().get(2)) != null - && literalValue(call.getOperands().get(3)) != null) { - value1 = literalValue(call.getOperands().get(2)); - value2 = literalValue(call.getOperands().get(3)); + if (literalValue(call.getOperands().get(2), timeZone) != null + && literalValue(call.getOperands().get(3), timeZone) != null) { + value1 = literalValue(call.getOperands().get(2), timeZone); + value2 = literalValue(call.getOperands().get(3), timeZone); } else { return null; } @@ -219,9 +226,10 @@ && literalValue(call.getOperands().get(3)) != null) { } case IN: { - ImmutableList.Builder> ranges = ImmutableList.builder(); + ImmutableList.Builder> ranges = + ImmutableList.builder(); for (RexNode operand : Util.skip(call.operands)) { - final TimestampString element = literalValue(operand); + final TimestampString element = literalValue(operand, timeZone); if (element == null) { return null; } @@ -239,16 +247,24 @@ && literalValue(call.getOperands().get(3)) != null) { } } - private static TimestampString literalValue(RexNode node) { + private static TimestampString literalValue(RexNode node, TimeZone timeZone) { switch (node.getKind()) { case LITERAL: switch (((RexLiteral) node).getTypeName()) { - case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return ((RexLiteral) node).getValueAs(TimestampString.class); + case TIMESTAMP: + // Cast timestamp to timestamp with local time zone + final TimestampString t = ((RexLiteral) node).getValueAs(TimestampString.class); + return new TimestampWithTimeZoneString(t.toString() + " " + timeZone.getID()) + .withTimeZone(DateTimeUtils.UTC_ZONE).getLocalTimestampString(); case DATE: - // For uniformity, treat dates as timestamps + // Cast date to timestamp with local time zone final DateString d = ((RexLiteral) node).getValueAs(DateString.class); - return TimestampString.fromMillisSinceEpoch(d.getMillisSinceEpoch()); + return new TimestampWithTimeZoneString( + TimestampString.fromMillisSinceEpoch( + d.getMillisSinceEpoch()).toString() + " " + timeZone.getID()) + .withTimeZone(DateTimeUtils.UTC_ZONE).getLocalTimestampString(); } break; case CAST: @@ -262,11 +278,13 @@ private static TimestampString literalValue(RexNode node) { final RelDataType callType = call.getType(); final RelDataType operandType = operand.getType(); if (operand.getKind() == SqlKind.LITERAL - && callType.getSqlTypeName() == SqlTypeName.TIMESTAMP + && callType.getSqlTypeName() == operandType.getSqlTypeName() + && (callType.getSqlTypeName() == SqlTypeName.DATE + || callType.getSqlTypeName() == SqlTypeName.TIMESTAMP + || callType.getSqlTypeName() == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) && callType.isNullable() - && operandType.getSqlTypeName() == SqlTypeName.TIMESTAMP && !operandType.isNullable()) { - return literalValue(operand); + return literalValue(operand, timeZone); } } return null; diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java index b12ad9a6599..b4a069b7fa0 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java @@ -512,7 +512,7 @@ protected QuerySpec getQuery(RelDataType rowType, RexNode filter, List ImmutableBitSet numericCollationIndexes, Integer fetch, Project postProject) { final CalciteConnectionConfig config = getConnectionConfig(); QueryType queryType = QueryType.SELECT; - final Translator translator = new Translator(druidTable, rowType); + final Translator translator = new Translator(druidTable, rowType, config.timeZone()); List fieldNames = rowType.getFieldNames(); Set usedFieldNames = Sets.newHashSet(fieldNames); @@ -564,7 +564,7 @@ protected QuerySpec getQuery(RelDataType rowType, RexNode filter, List String extractColumnName = SqlValidatorUtil.uniquify(EXTRACT_COLUMN_NAME_PREFIX, usedFieldNames, SqlValidatorUtil.EXPR_SUGGESTER); timeExtractionDimensionSpec = TimeExtractionDimensionSpec.makeFullTimeExtract( - extractColumnName); + extractColumnName, config.timeZone()); dimensions.add(timeExtractionDimensionSpec); builder.add(extractColumnName); assert timePositionIdx == -1; @@ -587,7 +587,7 @@ protected QuerySpec getQuery(RelDataType rowType, RexNode filter, List + "_" + funcGranularity.value, usedFieldNames, SqlValidatorUtil.EXPR_SUGGESTER); timeExtractionDimensionSpec = TimeExtractionDimensionSpec.makeTimeExtract( - funcGranularity, extractColumnName); + funcGranularity, extractColumnName, config.timeZone()); dimensions.add(timeExtractionDimensionSpec); builder.add(extractColumnName); break; @@ -600,7 +600,7 @@ protected QuerySpec getQuery(RelDataType rowType, RexNode filter, List SqlValidatorUtil.EXPR_SUGGESTER); dimensions.add( TimeExtractionDimensionSpec.makeTimeFloor(funcGranularity, - extractColumnName)); + extractColumnName, config.timeZone())); finalGranularity = Granularity.ALL; builder.add(extractColumnName); } else { @@ -632,7 +632,7 @@ protected QuerySpec getQuery(RelDataType rowType, RexNode filter, List String extractColumnName = SqlValidatorUtil.uniquify(EXTRACT_COLUMN_NAME_PREFIX, usedFieldNames, SqlValidatorUtil.EXPR_SUGGESTER); timeExtractionDimensionSpec = TimeExtractionDimensionSpec.makeFullTimeExtract( - extractColumnName); + extractColumnName, config.timeZone()); dimensions.add(timeExtractionDimensionSpec); builder.add(extractColumnName); assert timePositionIdx == -1; @@ -1083,8 +1083,9 @@ protected static class Translator { final List metrics = new ArrayList<>(); final DruidTable druidTable; final RelDataType rowType; + final String timeZone; - Translator(DruidTable druidTable, RelDataType rowType) { + Translator(DruidTable druidTable, RelDataType rowType, String timeZone) { this.druidTable = druidTable; this.rowType = rowType; for (RelDataTypeField f : rowType.getFieldList()) { @@ -1096,6 +1097,7 @@ protected static class Translator { dimensions.add(fieldName); } } + this.timeZone = timeZone; } protected void clearFieldNameLists() { @@ -1169,7 +1171,8 @@ private JsonFilter translateFilter(RexNode e) { // in case no extraction the field will be omitted from the serialization ExtractionFunction extractionFunction = null; if (granularity != null) { - extractionFunction = TimeExtractionFunction.createExtractFromGranularity(granularity); + extractionFunction = + TimeExtractionFunction.createExtractFromGranularity(granularity, timeZone); } String dimName = tr(e, posRef); if (dimName.equals(DruidConnectionImpl.DEFAULT_RESPONSE_TIMESTAMP_COLUMN)) { @@ -1279,7 +1282,7 @@ private static boolean containsLimit(QuerySpec querySpec) { private ColumnMetaData.Rep getPrimitive(RelDataTypeField field) { switch (field.getType().getSqlTypeName()) { - case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: return ColumnMetaData.Rep.JAVA_SQL_TIMESTAMP; case BIGINT: return ColumnMetaData.Rep.LONG; diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java index 343f03e621d..562e5685464 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java @@ -230,8 +230,8 @@ public void onMatch(RelOptRuleCall call) { List intervals = null; if (!triple.getLeft().isEmpty()) { intervals = DruidDateTimeUtils.createInterval( - query.getRowType().getFieldList().get(timestampFieldIdx).getType(), - RexUtil.composeConjunction(rexBuilder, triple.getLeft(), false)); + RexUtil.composeConjunction(rexBuilder, triple.getLeft(), false), + cluster.getPlanner().getContext().unwrap(CalciteConnectionConfig.class).timeZone()); if (intervals == null || intervals.isEmpty()) { // Case we have an filter with extract that can not be written as interval push down triple.getMiddle().addAll(triple.getLeft()); @@ -579,7 +579,7 @@ public boolean checkPostAggregatorExist(RexNode rexNode) { case MINUS: case DIVIDE: case TIMES: - case CAST: + //case CAST: return true; default: return false; diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTableFactory.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTableFactory.java index d636ce86003..d34e00052b6 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTableFactory.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidTableFactory.java @@ -59,7 +59,7 @@ public Table create(SchemaPlus schema, String name, Map operand, } else { timestampColumnName = DruidTable.DEFAULT_TIMESTAMP_COLUMN; } - fieldBuilder.put(timestampColumnName, SqlTypeName.TIMESTAMP); + fieldBuilder.put(timestampColumnName, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); final Object dimensionsRaw = operand.get("dimensions"); if (dimensionsRaw instanceof List) { // noinspection unchecked diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionDimensionSpec.java b/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionDimensionSpec.java index 656ee7703b0..7ef19a6c5ce 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionDimensionSpec.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionDimensionSpec.java @@ -34,9 +34,10 @@ public TimeExtractionDimensionSpec( * * @return the time extraction DimensionSpec instance */ - public static TimeExtractionDimensionSpec makeFullTimeExtract(String outputName) { + public static TimeExtractionDimensionSpec makeFullTimeExtract( + String outputName, String timeZone) { return new TimeExtractionDimensionSpec( - TimeExtractionFunction.createDefault(), outputName); + TimeExtractionFunction.createDefault(timeZone), outputName); } /** @@ -51,9 +52,9 @@ public static TimeExtractionDimensionSpec makeFullTimeExtract(String outputName) * is not supported */ public static TimeExtractionDimensionSpec makeTimeExtract( - Granularity granularity, String outputName) { + Granularity granularity, String outputName, String timeZone) { return new TimeExtractionDimensionSpec( - TimeExtractionFunction.createExtractFromGranularity(granularity), outputName); + TimeExtractionFunction.createExtractFromGranularity(granularity, timeZone), outputName); } /** @@ -64,8 +65,9 @@ public static TimeExtractionDimensionSpec makeTimeExtract( * @return floor time extraction DimensionSpec instance. */ public static TimeExtractionDimensionSpec makeTimeFloor(Granularity granularity, - String outputName) { - ExtractionFunction fn = TimeExtractionFunction.createFloorFromGranularity(granularity); + String outputName, String timeZone) { + ExtractionFunction fn = + TimeExtractionFunction.createFloorFromGranularity(granularity, timeZone); return new TimeExtractionDimensionSpec(fn, outputName); } } diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionFunction.java b/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionFunction.java index 22733bef916..b1f8870b782 100644 --- a/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionFunction.java +++ b/druid/src/main/java/org/apache/calcite/adapter/druid/TimeExtractionFunction.java @@ -75,8 +75,8 @@ public TimeExtractionFunction(String format, String granularity, String timeZone * * @return the time extraction function */ - public static TimeExtractionFunction createDefault() { - return new TimeExtractionFunction(ISO_TIME_FORMAT, null, "UTC", null); + public static TimeExtractionFunction createDefault(String timeZone) { + return new TimeExtractionFunction(ISO_TIME_FORMAT, null, timeZone, null); } /** @@ -87,16 +87,18 @@ public static TimeExtractionFunction createDefault() { * @return the time extraction function corresponding to the granularity input unit * {@link TimeExtractionFunction#VALID_TIME_EXTRACT} for supported granularity */ - public static TimeExtractionFunction createExtractFromGranularity(Granularity granularity) { + public static TimeExtractionFunction createExtractFromGranularity( + Granularity granularity, String timeZone) { switch (granularity) { case DAY: - return new TimeExtractionFunction("d", null, "UTC", Locale.getDefault().toLanguageTag()); + return new TimeExtractionFunction("d", null, timeZone, Locale.getDefault().toLanguageTag()); case MONTH: - return new TimeExtractionFunction("M", null, "UTC", Locale.getDefault().toLanguageTag()); + return new TimeExtractionFunction("M", null, timeZone, Locale.getDefault().toLanguageTag()); case YEAR: - return new TimeExtractionFunction("yyyy", null, "UTC", Locale.getDefault().toLanguageTag()); + return new TimeExtractionFunction("yyyy", null, timeZone, + Locale.getDefault().toLanguageTag()); case WEEK: - return new TimeExtractionFunction("w", null, "UTC", Locale.getDefault().toLanguageTag()); + return new TimeExtractionFunction("w", null, timeZone, Locale.getDefault().toLanguageTag()); default: throw new IllegalArgumentException("Granularity [" + granularity + "] is not supported"); } @@ -108,8 +110,9 @@ public static TimeExtractionFunction createExtractFromGranularity(Granularity gr * @param granularity granularity to apply to the column * @return the time extraction function or null if granularity is not supported */ - public static TimeExtractionFunction createFloorFromGranularity(Granularity granularity) { - return new TimeExtractionFunction(ISO_TIME_FORMAT, granularity.value, "UTC", Locale + public static TimeExtractionFunction createFloorFromGranularity( + Granularity granularity, String timeZone) { + return new TimeExtractionFunction(ISO_TIME_FORMAT, granularity.value, timeZone, Locale .getDefault().toLanguageTag()); } diff --git a/druid/src/test/java/org/apache/calcite/adapter/druid/DruidQueryFilterTest.java b/druid/src/test/java/org/apache/calcite/adapter/druid/DruidQueryFilterTest.java index b2e8635ee4a..e8e42be6bb4 100644 --- a/druid/src/test/java/org/apache/calcite/adapter/druid/DruidQueryFilterTest.java +++ b/druid/src/test/java/org/apache/calcite/adapter/druid/DruidQueryFilterTest.java @@ -121,7 +121,7 @@ static class Fixture { .add("dimensionName", varcharType) .build(); final DruidQuery.Translator translatorStringKind = - new DruidQuery.Translator(druidTable, varcharRowType); + new DruidQuery.Translator(druidTable, varcharRowType, "UTC"); } } diff --git a/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java b/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java index e88aaed2b81..1a0d3d3988b 100644 --- a/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java +++ b/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java @@ -235,12 +235,11 @@ private CalciteAssert.AssertQuery sql(String sql) { @Test public void testSelectTimestampColumnNoTables2() { // Since columns are not explicitly declared, we use the default time // column in the query. - final String sql = "select \"__time\"\n" + final String sql = "select cast(\"__time\" as timestamp) as \"__time\"\n" + "from \"wikiticker\"\n" + "limit 1\n"; - final String explain = "PLAN=" - + "EnumerableInterpreter\n" - + " DruidQuery(table=[[wiki, wikiticker]], intervals=[[1900-01-01T00:00:00.000/3000-01-01T00:00:00.000]], projects=[[$0]], fetch=[1])\n"; + final String explain = + "DruidQuery(table=[[wiki, wikiticker]], intervals=[[1900-01-01T00:00:00.000/3000-01-01T00:00:00.000]], projects=[[$0]], fetch=[1])\n"; final String druidQuery = "{'queryType':'select'," + "'dataSource':'wikiticker','descending':false," + "'intervals':['1900-01-01T00:00:00.000/3000-01-01T00:00:00.000']," @@ -255,12 +254,12 @@ private CalciteAssert.AssertQuery sql(String sql) { @Test public void testSelectTimestampColumnNoTables3() { // Since columns are not explicitly declared, we use the default time // column in the query. - final String sql = "select floor(\"__time\" to DAY) as \"day\", sum(\"added\")\n" + final String sql = + "select cast(floor(\"__time\" to DAY) as timestamp) as \"day\", sum(\"added\")\n" + "from \"wikiticker\"\n" + "group by floor(\"__time\" to DAY)"; - final String explain = "PLAN=" - + "EnumerableInterpreter\n" - + " DruidQuery(table=[[wiki, wikiticker]], intervals=[[1900-01-01T00:00:00.000/3000-01-01T00:00:00.000]], projects=[[FLOOR($0, FLAG(DAY)), $1]], groups=[{0}], aggs=[[SUM($1)]])\n"; + final String explain = + "DruidQuery(table=[[wiki, wikiticker]], intervals=[[1900-01-01T00:00:00.000/3000-01-01T00:00:00.000]], projects=[[FLOOR($0, FLAG(DAY)), $1]], groups=[{0}], aggs=[[SUM($1)]])\n"; final String druidQuery = "{'queryType':'timeseries'," + "'dataSource':'wikiticker','descending':false,'granularity':'day'," + "'aggregations':[{'type':'longSum','name':'EXPR$1','fieldName':'added'}]," @@ -276,12 +275,12 @@ private CalciteAssert.AssertQuery sql(String sql) { // Since columns are not explicitly declared, we use the default time // column in the query. final String sql = "select sum(\"added\") as \"s\", \"page\", " - + "floor(\"__time\" to DAY) as \"day\"\n" + + "cast(floor(\"__time\" to DAY) as timestamp) as \"day\"\n" + "from \"wikiticker\"\n" + "group by \"page\", floor(\"__time\" to DAY)\n" + "order by \"s\" desc"; final String explain = "PLAN=EnumerableInterpreter\n" - + " BindableProject(s=[$2], page=[$0], day=[$1])\n" + + " BindableProject(s=[$2], page=[$0], day=[CAST($1):TIMESTAMP(0)])\n" + " DruidQuery(table=[[wiki, wikiticker]], " + "intervals=[[1900-01-01T00:00:00.000/3000-01-01T00:00:00.000]], projects=[[$17, FLOOR" + "($0, FLAG(DAY)), $1]], groups=[{0, 1}], aggs=[[SUM($2)]], sort0=[2], dir0=[DESC])"; @@ -296,7 +295,8 @@ private CalciteAssert.AssertQuery sql(String sql) { } @Test public void testSkipEmptyBuckets() { - final String sql = "select floor(\"__time\" to SECOND) as \"second\", sum(\"added\")\n" + final String sql = + "select cast(floor(\"__time\" to SECOND) as timestamp) as \"second\", sum(\"added\")\n" + "from \"wikiticker\"\n" + "where \"page\" = 'Jeremy Corbyn'\n" + "group by floor(\"__time\" to SECOND)"; @@ -334,12 +334,10 @@ private CalciteAssert.AssertQuery checkSelectDistinctWiki(URL url, String tableN * Druid adapter: Send timestamp literals to Druid as local time, not * UTC. */ @Test public void testFilterTime() { - final String sql = "select \"__time\"\n" + final String sql = "select cast(\"__time\" as timestamp) as \"__time\"\n" + "from \"wikiticker\"\n" - + "where \"__time\" < '2015-10-12 00:00:00'"; - final String explain = "PLAN=" - + "EnumerableInterpreter\n" - + " DruidQuery(table=[[wiki, wikiticker]], " + + "where \"__time\" < '2015-10-12 00:00:00 UTC'"; + final String explain = "\n DruidQuery(table=[[wiki, wikiticker]], " + "intervals=[[1900-01-01T00:00:00.000/2015-10-12T00:00:00.000]], " + "projects=[[$0]])\n"; final String druidQuery = "{'queryType':'select'," @@ -357,12 +355,14 @@ private CalciteAssert.AssertQuery checkSelectDistinctWiki(URL url, String tableN } @Test public void testFilterTimeDistinct() { - final String sql = "select distinct \"__time\"\n" + final String sql = "select CAST(\"c1\" AS timestamp) as \"__time\" from\n" + + "(select distinct \"__time\" as \"c1\"\n" + "from \"wikiticker\"\n" - + "where \"__time\" < '2015-10-12 00:00:00'"; + + "where \"__time\" < '2015-10-12 00:00:00 UTC')"; final String explain = "PLAN=" + "EnumerableInterpreter\n" - + " DruidQuery(table=[[wiki, wikiticker]], " + + " BindableProject(__time=[CAST($0):TIMESTAMP(0)])\n" + + " DruidQuery(table=[[wiki, wikiticker]], " + "intervals=[[1900-01-01T00:00:00.000/2015-10-12T00:00:00.000]], " + "groups=[{0}], aggs=[[]])\n"; final String subDruidQuery = "{'queryType':'groupBy','dataSource':'wikiticker'," @@ -371,10 +371,10 @@ private CalciteAssert.AssertQuery checkSelectDistinctWiki(URL url, String tableN + "'extractionFn':{'type':'timeFormat'"; sql(sql, WIKI_AUTO2) .limit(2) - .returnsUnordered("__time=2015-09-12 00:46:58", - "__time=2015-09-12 00:47:00") .explainContains(explain) - .queryContains(druidChecker(subDruidQuery)); + .queryContains(druidChecker(subDruidQuery)) + .returnsUnordered("__time=2015-09-12 00:46:58", + "__time=2015-09-12 00:47:00"); } @Test public void testMetadataColumns() throws Exception { @@ -390,10 +390,11 @@ public Void apply(Connection c) { while (r.next()) { map.put(r.getString("TYPE_NAME"), true); } + System.out.println(map); // 1 timestamp, 2 float measure, 1 int measure, 88 dimensions assertThat(map.keySet().size(), is(4)); assertThat(map.values().size(), is(92)); - assertThat(map.get("TIMESTAMP(0)").size(), is(1)); + assertThat(map.get("TIMESTAMP_WITH_LOCAL_TIME_ZONE(0)").size(), is(1)); assertThat(map.get("DOUBLE").size(), is(2)); assertThat(map.get("BIGINT").size(), is(1)); assertThat(map.get(VARCHAR_TYPE).size(), is(88)); @@ -689,18 +690,14 @@ private void checkGroupBySingleSortLimit(boolean approx) { *

Before CALCITE-1578 was fixed, this would use a "topN" query but return * the wrong results. */ @Test public void testGroupByDaySortDescLimit() { - final String sql = "select \"brand_name\", floor(\"timestamp\" to DAY) as d," + final String sql = "select \"brand_name\"," + + " cast(floor(\"timestamp\" to DAY) as timestamp) as d," + " sum(\"unit_sales\") as s\n" + "from \"foodmart\"\n" + "group by \"brand_name\", floor(\"timestamp\" to DAY)\n" + "order by s desc limit 30"; - final String druidQuery = "{'queryType':'groupBy','dataSource':'foodmart'," - + "'granularity':'day','dimensions':[{'type':'default','dimension':'brand_name'}]," - + "'limitSpec':{'type':'default'}," - + "'aggregations':[{'type':'longSum','name':'S','fieldName':'unit_sales'}]," - + "'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000']}"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$2, FLOOR" + "($0, FLAG(DAY)), $89]], groups=[{0, 1}], aggs=[[SUM($2)]], sort0=[2], dir0=[DESC], " + "fetch=[30])"; @@ -725,7 +722,8 @@ private void checkGroupBySingleSortLimit(boolean approx) { * wrongly try to use a {@code limitSpec} to sort and filter. (A "topN" query * was not possible because the sort was {@code ASC}.) */ @Test public void testGroupByDaySortLimit() { - final String sql = "select \"brand_name\", floor(\"timestamp\" to DAY) as d," + final String sql = "select \"brand_name\"," + + " cast(floor(\"timestamp\" to DAY) as timestamp) as d," + " sum(\"unit_sales\") as s\n" + "from \"foodmart\"\n" + "group by \"brand_name\", floor(\"timestamp\" to DAY)\n" @@ -739,8 +737,7 @@ private void checkGroupBySingleSortLimit(boolean approx) { + "'dimensionOrder':'numeric'}]},'aggregations':[{'type':'longSum'," + "'name':'S','fieldName':'unit_sales'}]," + "'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000']}"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = "DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$2, FLOOR" + "($0, FLAG(DAY)), $89]], groups=[{0, 1}], aggs=[[SUM($2)]], sort0=[2], dir0=[DESC], " + "fetch=[30])"; @@ -757,7 +754,8 @@ private void checkGroupBySingleSortLimit(boolean approx) { * [CALCITE-1580] * Druid adapter: Wrong semantics for ordering within groupBy queries. */ @Test public void testGroupByDaySortDimension() { - final String sql = "select \"brand_name\", floor(\"timestamp\" to DAY) as d," + final String sql = + "select \"brand_name\", cast(floor(\"timestamp\" to DAY) as timestamp) as d," + " sum(\"unit_sales\") as s\n" + "from \"foodmart\"\n" + "group by \"brand_name\", floor(\"timestamp\" to DAY)\n" @@ -766,8 +764,7 @@ private void checkGroupBySingleSortLimit(boolean approx) { + "'granularity':'all','dimensions':[{'type':'default'," + "'dimension':'brand_name'},{'type':'extraction','dimension':'__time'," + "'outputName':'floor_day','extractionFn':{'type':'timeFormat'"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$2, FLOOR" + "($0, FLAG(DAY)), $89]], groups=[{0, 1}], aggs=[[SUM($2)]], sort0=[0], dir0=[ASC])"; sql(sql) @@ -790,8 +787,7 @@ private void checkGroupBySingleSortLimit(boolean approx) { + "'filter':{'type':'and','fields':[" + "{'type':'bound','dimension':'product_id','lower':'1500','lowerStrict':false,'ordering':'lexicographic'}," + "{'type':'bound','dimension':'product_id','upper':'1502','upperStrict':false,'ordering':'lexicographic'}]}," - + "'dimensions':['product_name','state_province','product_id']," - + "'metrics':[],'granularity':'all'," + + "'dimensions':['product_name','state_province','product_id'],'metrics':[],'granularity':'all'," + "'pagingSpec':{'threshold':16384,'fromNext':true},'context':{'druid.query.fetch':false}}"; sql(sql) .limit(4) @@ -819,13 +815,13 @@ public Void apply(ResultSet resultSet) { final String sql = "select \"product_name\" from \"foodmart\"\n" + "where \"product_id\" BETWEEN 1500 AND 1502\n" + "order by \"state_province\" desc, \"product_id\""; - final String druidQuery = "{'queryType':'select','dataSource':'foodmart','descending':false," - + "'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000'],'filter':{'type':" - + "'and','fields':[{'type':'bound','dimension':'product_id','lower':'1500'," - + "'lowerStrict':false,'ordering':'numeric'},{'type':'bound','dimension':'product_id'," - + "'upper':'1502','upperStrict':false,'ordering':'numeric'}]},'dimensions':" - + "['product_name','state_province','product_id'],'metrics':[],'granularity':'all','pagingSpec':" - + "{'threshold':16384,'fromNext':true},'context':{'druid.query.fetch':false}}"; + final String druidQuery = "{'queryType':'select','dataSource':'foodmart'," + + "'descending':false,'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000']," + + "'filter':{'type':'and','fields':[" + + "{'type':'bound','dimension':'product_id','lower':'1500','lowerStrict':false,'ordering':'numeric'}," + + "{'type':'bound','dimension':'product_id','upper':'1502','upperStrict':false,'ordering':'numeric'}]}," + + "'dimensions':['product_name','state_province','product_id'],'metrics':[],'granularity':'all'," + + "'pagingSpec':{'threshold':16384,'fromNext':true},'context':{'druid.query.fetch':false}}"; sql(sql) .limit(4) .returns( @@ -854,8 +850,7 @@ public Void apply(ResultSet resultSet) { final String druidQuery = "{'queryType':'select','dataSource':'foodmart'," + "'descending':false,'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000']," + "'filter':{'type':'selector','dimension':'product_id','value':'-1'}," - + "'dimensions':['product_name']," - + "'metrics':[],'granularity':'all'," + + "'dimensions':['product_name'],'metrics':[],'granularity':'all'," + "'pagingSpec':{'threshold':16384,'fromNext':true},'context':{'druid.query.fetch':false}}"; sql(sql) .limit(4) @@ -871,8 +866,7 @@ public Void apply(ResultSet resultSet) { + "order by \"state_province\" desc, \"product_id\""; final String druidQuery = "{'queryType':'select','dataSource':'foodmart'," + "'descending':false,'intervals':['1900-01-09T00:00:00.000/2992-01-10T00:00:00.000']," - + "'dimensions':['product_id','product_name','state_province']," - + "'metrics':[],'granularity':'all'," + + "'dimensions':['product_id','product_name','state_province'],'metrics':[],'granularity':'all'," + "'pagingSpec':{'threshold':16384,'fromNext':true},'context':{'druid.query.fetch':false}}"; sql(sql) .limit(4) @@ -959,7 +953,8 @@ public Void apply(ResultSet resultSet) { * "topN" because we have a global limit, and that requires * {@code granularity: all}. */ @Test public void testGroupByTimeAndOneColumnNotProjectedWithLimit() { - final String sql = "select count(*) as \"c\", floor(\"timestamp\" to MONTH) as \"month\"\n" + final String sql = "select count(*) as \"c\"," + + " cast(floor(\"timestamp\" to MONTH) as timestamp) as \"month\"\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH), \"state_province\"\n" + "order by \"c\" desc limit 3"; @@ -972,7 +967,7 @@ public Void apply(ResultSet resultSet) { @Test public void testGroupByTimeAndOneMetricNotProjected() { final String sql = - "select count(*) as \"c\", floor(\"timestamp\" to MONTH) as \"month\", floor" + "select count(*) as \"c\", cast(floor(\"timestamp\" to MONTH) as timestamp) as \"month\", floor" + "(\"store_sales\") as sales\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH), \"state_province\", floor" @@ -986,7 +981,7 @@ public Void apply(ResultSet resultSet) { @Test public void testGroupByTimeAndOneColumnNotProjected() { final String sql = "select count(*) as \"c\",\n" - + " floor(\"timestamp\" to MONTH) as \"month\"\n" + + " cast(floor(\"timestamp\" to MONTH) as timestamp) as \"month\"\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH), \"state_province\"\n" + "having count(*) > 3500"; @@ -1066,46 +1061,45 @@ public Void apply(ResultSet resultSet) { * [CALCITE-1577] * Druid adapter: Incorrect result - limit on timestamp disappears. */ @Test public void testGroupByMonthGranularitySort() { - final String sql = "select floor(\"timestamp\" to MONTH) as m,\n" - + " sum(\"unit_sales\") as s,\n" + final String sql = "select sum(\"unit_sales\") as s,\n" + " count(\"store_sqft\") as c\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH)\n" + "order by floor(\"timestamp\" to MONTH) ASC"; final String explain = "PLAN=EnumerableInterpreter\n" - + " BindableSort(sort0=[$0], dir0=[ASC])\n" - + " BindableAggregate(group=[{0}], S=[SUM($1)], C=[COUNT($2)])\n" - + " BindableProject(M=[FLOOR($0, FLAG(MONTH))], unit_sales=[$2], store_sqft=[$1])\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + + " BindableSort(sort0=[$2], dir0=[ASC])\n" + + " BindableProject(S=[$1], C=[$2], EXPR$2=[$0])\n" + + " BindableAggregate(group=[{0}], S=[SUM($1)], C=[COUNT($2)])\n" + + " BindableProject($f0=[FLOOR($0, FLAG(MONTH))], unit_sales=[$2], store_sqft=[$1])\n" + + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$0, $71, $89]])"; sql(sql) - .returnsOrdered("M=1997-01-01 00:00:00; S=21628; C=5957", - "M=1997-02-01 00:00:00; S=20957; C=5842", - "M=1997-03-01 00:00:00; S=23706; C=6528", - "M=1997-04-01 00:00:00; S=20179; C=5523", - "M=1997-05-01 00:00:00; S=21081; C=5793", - "M=1997-06-01 00:00:00; S=21350; C=5863", - "M=1997-07-01 00:00:00; S=23763; C=6762", - "M=1997-08-01 00:00:00; S=21697; C=5915", - "M=1997-09-01 00:00:00; S=20388; C=5591", - "M=1997-10-01 00:00:00; S=19958; C=5606", - "M=1997-11-01 00:00:00; S=25270; C=7026", - "M=1997-12-01 00:00:00; S=26796; C=7338") - .explainContains(explain); + .explainContains(explain) + .returnsOrdered("S=21628; C=5957", + "S=20957; C=5842", + "S=23706; C=6528", + "S=20179; C=5523", + "S=21081; C=5793", + "S=21350; C=5863", + "S=23763; C=6762", + "S=21697; C=5915", + "S=20388; C=5591", + "S=19958; C=5606", + "S=25270; C=7026", + "S=26796; C=7338"); } @Test public void testGroupByMonthGranularitySortLimit() { - final String sql = "select floor(\"timestamp\" to MONTH) as m,\n" + final String sql = "select cast(floor(\"timestamp\" to MONTH) as timestamp) as m,\n" + " sum(\"unit_sales\") as s,\n" + " count(\"store_sqft\") as c\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH)\n" + "order by floor(\"timestamp\" to MONTH) limit 3"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " BindableSort(sort0=[$0], dir0=[ASC], fetch=[3])\n" - + " BindableAggregate(group=[{0}], S=[SUM($1)], C=[COUNT($2)])\n" - + " BindableProject(M=[FLOOR($0, FLAG(MONTH))], unit_sales=[$2], store_sqft=[$1])\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = "BindableSort(sort0=[$0], dir0=[ASC], fetch=[3])\n" + + " BindableAggregate(group=[{0}], S=[SUM($1)], C=[COUNT($2)])\n" + + " BindableProject($f0=[FLOOR($0, FLAG(MONTH))], unit_sales=[$2], store_sqft=[$1])\n" + + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$0, $71, $89]])"; sql(sql) .returnsOrdered("M=1997-01-01 00:00:00; S=21628; C=5957", @@ -1122,16 +1116,16 @@ public Void apply(ResultSet resultSet) { String druidQuery = "{'queryType':'select','dataSource':'foodmart'"; sql(sql) .limit(3) - .returnsUnordered("S=1244; C=391", "S=550; C=112", "S=580; C=171") - .queryContains(druidChecker(druidQuery)); + .queryContains(druidChecker(druidQuery)) + .returnsUnordered("S=1244; C=391", "S=550; C=112", "S=580; C=171"); } @Test public void testGroupByMonthGranularityFiltered() { final String sql = "select sum(\"unit_sales\") as s,\n" + " count(\"store_sqft\") as c\n" + "from \"foodmart\"\n" - + "where \"timestamp\" >= '1996-01-01 00:00:00' and " - + " \"timestamp\" < '1998-01-01 00:00:00'\n" + + "where \"timestamp\" >= '1996-01-01 00:00:00 UTC' and " + + " \"timestamp\" < '1998-01-01 00:00:00 UTC'\n" + "group by floor(\"timestamp\" to MONTH)"; String druidQuery = "{'queryType':'select','dataSource':'foodmart'"; sql(sql) @@ -1179,8 +1173,8 @@ public Void apply(ResultSet resultSet) { + "max(\"unit_sales\") as m,\n" + "\"state_province\" as p\n" + "from \"foodmart\"\n" - + "where \"timestamp\" >= '1997-01-01 00:00:00' and " - + " \"timestamp\" < '1997-09-01 00:00:00'\n" + + "where \"timestamp\" >= '1997-01-01 00:00:00 UTC' and " + + " \"timestamp\" < '1997-09-01 00:00:00 UTC'\n" + "group by \"state_province\", floor(\"timestamp\" to DAY)\n" + "order by s desc limit 6"; final String explain = "PLAN=EnumerableInterpreter\n" @@ -1383,12 +1377,11 @@ public Void apply(ResultSet resultSet) { + "from \"foodmart\"\n" + "where extract(year from \"timestamp\") = 1997\n" + "and extract(month from \"timestamp\") in (4, 6)\n"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = "DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[AND(=" - + "(EXTRACT_DATE(FLAG(YEAR), /INT(Reinterpret($0), 86400000)), 1997), OR(=(EXTRACT_DATE" - + "(FLAG(MONTH), /INT(Reinterpret($0), 86400000)), 4), =(EXTRACT_DATE(FLAG(MONTH), /INT" - + "(Reinterpret($0), 86400000)), 6)))], groups=[{}], aggs=[[COUNT()]])"; + + "(EXTRACT_DATE(FLAG(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), 1997), OR(=(EXTRACT_DATE" + + "(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), 4), =(EXTRACT_DATE(FLAG(MONTH), " + + "/INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), 6)))], groups=[{}], aggs=[[COUNT()]])"; sql(sql) .explainContains(explain) .returnsUnordered("C=13500"); @@ -1430,17 +1423,17 @@ public Void apply(ResultSet resultSet) { @Test public void testFieldBasedCostColumnPruning() { // A query where filter cannot be pushed to Druid but // the project can still be pushed in order to prune extra columns. - String sql = "select \"countryName\", floor(\"time\" to DAY),\n" + String sql = "select \"countryName\", floor(CAST(\"time\" AS TIMESTAMP) to DAY),\n" + " cast(count(*) as integer) as c\n" + "from \"wiki\"\n" - + "where floor(\"time\" to DAY) >= '1997-01-01 00:00:00'\n" - + "and floor(\"time\" to DAY) < '1997-09-01 00:00:00'\n" - + "group by \"countryName\", floor(\"time\" TO DAY)\n" + + "where floor(\"time\" to DAY) >= '1997-01-01 00:00:00 UTC'\n" + + "and floor(\"time\" to DAY) < '1997-09-01 00:00:00 UTC'\n" + + "group by \"countryName\", floor(CAST(\"time\" AS TIMESTAMP) TO DAY)\n" + "order by c limit 5"; String plan = "BindableProject(countryName=[$0], EXPR$1=[$1], C=[CAST($2):INTEGER NOT NULL])\n" + " BindableSort(sort0=[$2], dir0=[ASC], fetch=[5])\n" + " BindableAggregate(group=[{0, 1}], agg#0=[COUNT()])\n" - + " BindableProject(countryName=[$1], EXPR$1=[FLOOR($0, FLAG(DAY))])\n" + + " BindableProject(countryName=[$1], EXPR$1=[FLOOR(CAST($0):TIMESTAMP(0), FLAG(DAY))])\n" + " BindableFilter(condition=[AND(>=(FLOOR($0, FLAG(DAY)), 1997-01-01 00:00:00), <(FLOOR($0, FLAG(DAY)), 1997-09-01 00:00:00))])\n" + " DruidQuery(table=[[wiki, wiki]], intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$0, $5]])"; // NOTE: Druid query only has countryName as the dimension @@ -1459,10 +1452,11 @@ public Void apply(ResultSet resultSet) { } @Test public void testGroupByMetricAndExtractTime() { - final String sql = "SELECT count(*), floor(\"timestamp\" to DAY), \"store_sales\" " - + "FROM \"foodmart\"\n" - + "GROUP BY \"store_sales\", floor(\"timestamp\" to DAY)\n ORDER BY \"store_sales\" DESC\n" - + "LIMIT 10\n"; + final String sql = + "SELECT count(*), cast(floor(\"timestamp\" to DAY) as timestamp), \"store_sales\" " + + "FROM \"foodmart\"\n" + + "GROUP BY \"store_sales\", floor(\"timestamp\" to DAY)\n ORDER BY \"store_sales\" DESC\n" + + "LIMIT 10\n"; sql(sql).queryContains(druidChecker("{\"queryType\":\"select\"")); } @@ -1475,10 +1469,11 @@ public Void apply(ResultSet resultSet) { } @Test public void testPushAggregateOnTime() { - String sql = "select \"product_id\", \"timestamp\" as \"time\" from \"foodmart\" " + String sql = "select \"product_id\", cast(\"timestamp\" as timestamp) as \"time\" " + + "from \"foodmart\" " + "where \"product_id\" = 1016 " - + "and \"timestamp\" < cast('1997-01-03' as timestamp) " - + "and \"timestamp\" > cast('1990-01-01' as timestamp) " + + "and \"timestamp\" < '1997-01-03 00:00:00 UTC' " + + "and \"timestamp\" > '1990-01-01 00:00:00 UTC' " + "group by \"timestamp\", \"product_id\" "; String druidQuery = "{'queryType':'groupBy','dataSource':'foodmart'," + "'granularity':'all','dimensions':[{'type':'extraction'," @@ -1592,9 +1587,9 @@ public Void apply(ResultSet resultSet) { .explainContains("PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1997-01-01T00:00:00.001/1997-01-20T00:00:00.000]], filter=[=($1, 1016)" - + "], projects=[[EXTRACT_DATE(FLAG(DAY), /INT(Reinterpret($0), 86400000)), " - + "EXTRACT_DATE(FLAG(MONTH), /INT(Reinterpret($0), 86400000)), EXTRACT_DATE(FLAG" - + "(YEAR), /INT(Reinterpret($0), 86400000)), $1]], groups=[{0, 1, 2, 3}], aggs=[[]])\n") + + "], projects=[[EXTRACT_DATE(FLAG(DAY), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), " + + "EXTRACT_DATE(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), EXTRACT_DATE(FLAG" + + "(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), $1]], groups=[{0, 1, 2, 3}], aggs=[[]])\n") .returnsUnordered("day=2; month=1; year=1997; product_id=1016", "day=10; month=1; year=1997; product_id=1016", "day=13; month=1; year=1997; product_id=1016", @@ -1626,9 +1621,9 @@ public Void apply(ResultSet resultSet) { .explainContains("PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1997-01-01T00:00:00.001/1997-01-20T00:00:00.000]], filter=[=($1, 1016)" - + "], projects=[[EXTRACT_DATE(FLAG(DAY), /INT(Reinterpret($0), 86400000)), " - + "EXTRACT_DATE(FLAG(MONTH), /INT(Reinterpret($0), 86400000)), EXTRACT_DATE(FLAG" - + "(YEAR), /INT(Reinterpret($0), 86400000)), $1]], groups=[{0, 1, 2, 3}], aggs=[[]])\n") + + "], projects=[[EXTRACT_DATE(FLAG(DAY), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), " + + "EXTRACT_DATE(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), EXTRACT_DATE(FLAG" + + "(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), $1]], groups=[{0, 1, 2, 3}], aggs=[[]])\n") .returnsUnordered("EXPR$0=2; EXPR$1=1; EXPR$2=1997; product_id=1016", "EXPR$0=10; EXPR$1=1; EXPR$2=1997; product_id=1016", "EXPR$0=13; EXPR$1=1; EXPR$2=1997; product_id=1016", @@ -1653,7 +1648,7 @@ public Void apply(ResultSet resultSet) { .explainContains("PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1997-01-01T00:00:00.001/1997-01-20T00:00:00.000]], filter=[=($1, 1016)], " - + "projects=[[EXTRACT_DATE(FLAG(DAY), /INT(Reinterpret($0), 86400000)), $1]], " + + "projects=[[EXTRACT_DATE(FLAG(DAY), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), $1]], " + "groups=[{0, 1}], aggs=[[]])\n") .returnsUnordered("EXPR$0=2; dayOfMonth=1016", "EXPR$0=10; dayOfMonth=1016", "EXPR$0=13; dayOfMonth=1016", "EXPR$0=16; dayOfMonth=1016"); @@ -1682,7 +1677,7 @@ public Void apply(ResultSet resultSet) { + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[AND(>=(CAST" + "($11):BIGINT, 8), <=(CAST($11):BIGINT, 10), <(CAST($10):BIGINT, 15), =(EXTRACT_DATE" - + "(FLAG(YEAR), /INT(Reinterpret($0), 86400000)), 1997))], groups=[{}], " + + "(FLAG(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), 1997))], groups=[{}], " + "aggs=[[SUM($90)]])") .queryContains(druidChecker(druidQuery)) .returnsUnordered("EXPR$0=75364.09998679161"); @@ -1830,33 +1825,33 @@ public Void apply(ResultSet resultSet) { .explainContains("PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[>=(CAST($1)" - + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(MONTH), /INT(Reinterpret($0), " + + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), " + "86400000)), $1, $89]], groups=[{0, 1}], aggs=[[SUM($2)]], sort0=[0], sort1=[2], " + "sort2=[1], dir0=[ASC], dir1=[ASC], dir2=[ASC])"); } @Test public void testGroupByFloorTimeWithoutLimit() { - final String sql = "select floor(\"timestamp\" to MONTH) as \"month\"\n" + final String sql = "select cast(floor(\"timestamp\" to MONTH) as timestamp) as \"month\"\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH)\n" + "order by \"month\" DESC"; sql(sql) - .explainContains("PLAN=EnumerableInterpreter\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + .explainContains("DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[FLOOR($0, " + "FLAG(MONTH))]], groups=[{0}], aggs=[[]], sort0=[0], dir0=[DESC])") .queryContains(druidChecker("'queryType':'timeseries'", "'descending':true")); } @Test public void testGroupByFloorTimeWithLimit() { - final String sql = "select floor(\"timestamp\" to MONTH) as \"floor_month\"\n" + final String sql = + "select cast(floor(\"timestamp\" to MONTH) as timestamp) as \"floor_month\"\n" + "from \"foodmart\"\n" + "group by floor(\"timestamp\" to MONTH)\n" + "order by \"floor_month\" DESC LIMIT 3"; - final String explain = "PLAN=EnumerableInterpreter\n" - + " BindableSort(sort0=[$0], dir0=[DESC], fetch=[3])\n" - + " DruidQuery(table=[[foodmart, foodmart]], " + final String explain = + " BindableSort(sort0=[$0], dir0=[DESC], fetch=[3])\n" + + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], " + "projects=[[FLOOR($0, FLAG(MONTH))]], groups=[{0}], aggs=[[]], " + "sort0=[0], dir0=[DESC])"; @@ -1876,8 +1871,8 @@ public Void apply(ResultSet resultSet) { final String expectedPlan = "PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[>=(CAST($1)" - + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(YEAR), /INT(Reinterpret($0), 86400000))," - + " EXTRACT_DATE(FLAG(MONTH), /INT(Reinterpret($0), 86400000)), $1, $89]], groups=[{0, 1," + + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000))," + + " EXTRACT_DATE(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), $1, $89]], groups=[{0, 1," + " 2}], aggs=[[SUM($3)]], sort0=[0], sort1=[1], sort2=[3], sort3=[2], dir0=[DESC], " + "dir1=[ASC], dir2=[DESC], dir3=[ASC], fetch=[3])"; final String expectedDruidQuery = "{'queryType':'groupBy','dataSource':'foodmart'," @@ -1912,8 +1907,8 @@ public Void apply(ResultSet resultSet) { final String expectedPlan = "PLAN=EnumerableInterpreter\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[>=(CAST($1)" - + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(YEAR), /INT(Reinterpret($0), 86400000))," - + " EXTRACT_DATE(FLAG(MONTH), /INT(Reinterpret($0), 86400000)), $1, $89]], groups=[{0, 1," + + ":BIGINT, 1558)], projects=[[EXTRACT_DATE(FLAG(YEAR), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000))," + + " EXTRACT_DATE(FLAG(MONTH), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000)), $1, $89]], groups=[{0, 1," + " 2}], aggs=[[SUM($3)]], sort0=[3], sort1=[1], sort2=[2], dir0=[DESC], dir1=[DESC], " + "dir2=[ASC], fetch=[3])"; final String expectedDruidQuery = "{'queryType':'groupBy','dataSource':'foodmart'," @@ -1939,12 +1934,13 @@ public Void apply(ResultSet resultSet) { } @Test public void testGroupByTimeSortOverMetrics() { - final String sqlQuery = "SELECT count(*) as c , SUM(\"unit_sales\") as s, floor(\"timestamp\"" - + " to month) FROM \"foodmart\" group by floor(\"timestamp\" to month) order by s DESC"; + final String sqlQuery = "SELECT count(*) as c , SUM(\"unit_sales\") as s," + + " cast(floor(\"timestamp\" to month) as timestamp)" + + " FROM \"foodmart\" group by floor(\"timestamp\" to month) order by s DESC"; sql(sqlQuery) .explainContains("PLAN=EnumerableInterpreter\n" + " BindableSort(sort0=[$1], dir0=[DESC])\n" - + " BindableProject(C=[$1], S=[$2], EXPR$2=[$0])\n" + + " BindableProject(C=[$1], S=[$2], EXPR$2=[CAST($0):TIMESTAMP(0)])\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[FLOOR($0, " + "FLAG(MONTH)), $89]], groups=[{0}], aggs=[[COUNT(), SUM($1)]])") @@ -1964,8 +1960,8 @@ public Void apply(ResultSet resultSet) { } @Test public void testNumericOrderingOfOrderByOperatorFullTime() { - final String sqlQuery = "SELECT \"timestamp\", count(*) as c, SUM(\"unit_sales\") " - + "as s FROM " + final String sqlQuery = "SELECT cast(\"timestamp\" as timestamp) as \"timestamp\"," + + " count(*) as c, SUM(\"unit_sales\") as s FROM " + "\"foodmart\" group by \"timestamp\" order by \"timestamp\" DESC, c DESC, s LIMIT 5"; final String druidSubQuery = "'limitSpec':{'type':'default','limit':5," + "'columns':[{'dimension':'extract','direction':'descending'," @@ -2044,7 +2040,7 @@ public Void apply(ResultSet resultSet) { + "\"product_id\" = 1558 group by extract(CENTURY from \"timestamp\")"; final String plan = "PLAN=EnumerableInterpreter\n" + " BindableAggregate(group=[{0}])\n" - + " BindableProject(EXPR$0=[EXTRACT_DATE(FLAG(CENTURY), /INT(Reinterpret($0), 86400000))])\n" + + " BindableProject(EXPR$0=[EXTRACT_DATE(FLAG(CENTURY), /INT(CAST(Reinterpret($0)):TIMESTAMP(0), 86400000))])\n" + " DruidQuery(table=[[foodmart, foodmart]], " + "intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], filter=[=($1, 1558)], " + "projects=[[$0]])"; @@ -2491,7 +2487,7 @@ public RelNode apply(RelBuilder b) { @Test public void testOrderByOnMetricsInSelectDruidQuery() { final String sqlQuery = "select \"store_sales\" as a, \"store_cost\" as b, \"store_sales\" - " + "\"store_cost\" as c from \"foodmart\" where \"timestamp\" " - + ">= '1997-01-01 00:00:00' and \"timestamp\" < '1997-09-01 00:00:00' order by c " + + ">= '1997-01-01 00:00:00 UTC' and \"timestamp\" < '1997-09-01 00:00:00 UTC' order by c " + "limit 5"; String postAggString = "'queryType':'select'"; final String plan = "PLAN=EnumerableInterpreter\n" diff --git a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java index 74ce10c934e..0e1948daa86 100644 --- a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java +++ b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java @@ -158,7 +158,7 @@ private void checkDateRangeNoSimplify(Fixture f, RexNode e, operandRanges)); } final List intervals = - DruidDateTimeUtils.createInterval(f.timeStampDataType, e); + DruidDateTimeUtils.createInterval(e, "UTC"); assertThat(intervals, notNullValue()); assertThat(intervals.toString(), intervalMatcher); } @@ -178,7 +178,7 @@ private void checkDateRange(Fixture f, RexNode e, Matcher intervalMatche } final RexNode e2 = f.simplify.simplify(e); List intervals = - DruidDateTimeUtils.createInterval(f.timeStampDataType, e2); + DruidDateTimeUtils.createInterval(e2, "UTC"); if (intervals == null) { throw new AssertionError("null interval"); } diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 209d39a2833..2bc70fd6c0b 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -978,6 +978,7 @@ name will have been converted to upper case also. | DATE | Date | Example: DATE '1969-07-20' | TIME | Time of day | Example: TIME '20:17:40' | TIMESTAMP [ WITHOUT TIME ZONE ] | Date and time | Example: TIMESTAMP '1969-07-20 20:17:40' +| TIMESTAMP WITH LOCAL TIME ZONE | Date and time with local time zone | Example: TIMESTAMP '1969-07-20 20:17:40 America/Los Angeles' | TIMESTAMP WITH TIME ZONE | Date and time with time zone | Example: TIMESTAMP '1969-07-20 20:17:40 America/Los Angeles' | INTERVAL timeUnit [ TO timeUnit ] | Date time interval | Examples: INTERVAL '1-5' YEAR TO MONTH, INTERVAL '45' DAY, INTERVAL '1 2:34:56.789' DAY TO SECOND | GEOMETRY | Geometry | Examples: ST_GeomFromText('POINT (30 10)') @@ -991,9 +992,11 @@ timeUnit: Note: -* DATE, TIME and TIMESTAMP have no time zone. There is not even an implicit - time zone, such as UTC (as in Java) or the local time zone. It is left to - the user or application to supply a time zone. +* DATE, TIME and TIMESTAMP have no time zone. For those types, there is not + even an implicit time zone, such as UTC (as in Java) or the local time zone. + It is left to the user or application to supply a time zone. In turn, + TIMESTAMP WITH LOCAL TIME ZONE does not store the time zone internally, but + it will rely on the supplied time zone to provide correct semantics. * GEOMETRY is allowed only in certain [conformance levels]({{ site.apiRoot }}/org/apache/calcite/sql/validate/SqlConformance.html#allowGeometry--).