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 b77fe0283095..97118c10201f 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -35,11 +35,14 @@ import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlSpecialOperator; +import org.apache.calcite.sql.SqlTimeLiteral; +import org.apache.calcite.sql.SqlTimestampLiteral; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.fun.SqlCountAggFunction; import org.apache.calcite.sql.fun.SqlLibraryOperators; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.parser.SqlParserUtil; import org.apache.calcite.sql.type.ArraySqlType; import org.apache.calcite.sql.type.IntervalSqlType; import org.apache.calcite.sql.type.MapSqlType; @@ -805,6 +808,12 @@ public RexNode makeCast( && SqlTypeUtil.isExactNumeric(type)) { return makeCastBooleanToExact(type, exp); } + final RexNode literalCast = + makeCastForTemporalLiteral( + pos, type, literal, matchNullability, safe, format); + if (literalCast != null) { + return literalCast; + } if (canRemoveCastFromLiteral(type, value, typeName)) { switch (typeName) { case INTERVAL_YEAR: @@ -877,6 +886,106 @@ public RexNode makeCast( return makeAbstractCast(pos, type, exp, safe, format); } + private @Nullable RexNode makeCastForTemporalLiteral( + SqlParserPos pos, + RelDataType type, + RexLiteral literal, + boolean matchNullability, + boolean safe, + RexLiteral format) { + if (!format.isNull()) { + return null; + } + if (SqlTypeUtil.isCharacter(literal.getType())) { + return makeCastFromCharacterLiteralToTemporal( + pos, type, literal, matchNullability, safe, format); + } + if (SqlTypeUtil.isCharacter(type)) { + return makeCastFromTemporalLiteralToCharacter(type, literal); + } + return null; + } + + private @Nullable RexNode makeCastFromCharacterLiteralToTemporal( + SqlParserPos pos, + RelDataType type, + RexLiteral literal, + boolean matchNullability, + boolean safe, + RexLiteral format) { + final NlsString nlsString = literal.getValueAs(NlsString.class); + if (nlsString == null) { + return null; + } + final String value = nlsString.getValue().trim(); + final RexNode temporalLiteral; + try { + switch (type.getSqlTypeName()) { + case TIME: + final SqlTimeLiteral timeLiteral = + SqlParserUtil.parseTimeLiteral(value, pos); + if (timeLiteral.getPrec() <= 0 + || !timeLiteral.toFormattedString().equals(value)) { + return null; + } + final TimeString time = + requireNonNull(timeLiteral.getValueAs(TimeString.class), + "timeLiteral.getValueAs(TimeString.class)"); + temporalLiteral = makeTimeLiteral(time, type.getPrecision()); + break; + case TIMESTAMP: + final SqlTimestampLiteral timestampLiteral = + SqlParserUtil.parseTimestampLiteral(value, pos); + if (timestampLiteral.getPrec() <= 0 + || !timestampLiteral.toFormattedString().equals(value)) { + return null; + } + final TimestampString timestamp = + requireNonNull(timestampLiteral.getValueAs(TimestampString.class), + "timestampLiteral.getValueAs(TimestampString.class)"); + temporalLiteral = makeTimestampLiteral(timestamp, type.getPrecision()); + break; + default: + return null; + } + } catch (RuntimeException e) { + return safe ? makeNullLiteral(type) : null; + } + if (type.isNullable() + && !temporalLiteral.getType().isNullable() + && matchNullability) { + return makeAbstractCast(pos, type, temporalLiteral, safe, format); + } + return temporalLiteral; + } + + private @Nullable RexNode makeCastFromTemporalLiteralToCharacter( + RelDataType type, + RexLiteral literal) { + final String value; + switch (literal.getType().getSqlTypeName()) { + case TIME: + final TimeString time = literal.getValueAs(TimeString.class); + if (time == null) { + return null; + } + value = time.toString(literal.getType().getPrecision()); + break; + case TIMESTAMP: + final TimestampString timestamp = literal.getValueAs(TimestampString.class); + if (timestamp == null || literal.getType().getPrecision() <= 3) { + return null; + } + value = timestamp.toString(literal.getType().getPrecision()); + break; + default: + return null; + } + return SqlTypeUtil.comparePrecision(type.getPrecision(), value.length()) >= 0 + ? makeLiteral(value, type, true) + : null; + } + /** Returns the lowest granularity unit for the given unit. * YEAR and MONTH intervals are stored as months; * HOUR, MINUTE, SECOND intervals are stored as milliseconds. */ diff --git a/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java b/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java index 3a894d0fbad1..aef5b92e785e 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java +++ b/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java @@ -43,6 +43,7 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; /** @@ -136,14 +137,32 @@ public static RexExecutable getExecutable(RexBuilder rexBuilder, List e @Override public void reduce(RexBuilder rexBuilder, List constExps, List reducedValues) { assert reducedValues.isEmpty(); + final List exps = new ArrayList<>(); + final List ordinals = new ArrayList<>(); + for (int i = 0; i < constExps.size(); i++) { + final RexNode constExp = constExps.get(i); + if (!(constExp instanceof RexLiteral)) { + ordinals.add(i); + exps.add(constExp); + } + } + if (exps.isEmpty()) { + reducedValues.addAll(constExps); + return; + } try { - String code = compile(rexBuilder, constExps, (list, index, storageType) -> { + String code = compile(rexBuilder, exps, (list, index, storageType) -> { throw new UnsupportedOperationException(); }); - final RexExecutable executable = new RexExecutable(code, constExps); + final RexExecutable executable = new RexExecutable(code, exps); executable.setDataContext(dataContext); - executable.reduce(rexBuilder, constExps, reducedValues); + final List values = new ArrayList<>(exps.size()); + executable.reduce(rexBuilder, exps, values); + reducedValues.addAll(constExps); + for (int i = 0; i < ordinals.size(); i++) { + reducedValues.set(ordinals.get(i), values.get(i)); + } } catch (RuntimeException ex) { // Something went wrong during constant reduction (for example, // we may have attempted a division by zero). diff --git a/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java b/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java index a98cffc0007a..f867609c20e2 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java +++ b/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java @@ -15,11 +15,14 @@ * limitations under the License. */ package org.apache.calcite.rex; + import org.apache.calcite.DataContext; import org.apache.calcite.DataContexts; import org.apache.calcite.avatica.util.ByteString; +import org.apache.calcite.jdbc.JavaTypeFactoryImpl; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.rel.type.RelDataTypeSystemImpl; import org.apache.calcite.sql.SqlBinaryOperator; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; @@ -35,6 +38,7 @@ import org.apache.calcite.util.DateString; import org.apache.calcite.util.NlsString; import org.apache.calcite.util.TestUtil; +import org.apache.calcite.util.TimeString; import org.apache.calcite.util.TimestampString; import org.apache.calcite.util.Util; @@ -160,6 +164,171 @@ protected void check(final Action action) { }); } + @Test void testReduceTimeCastWithMicroseconds() { + checkHighPrecision((rexBuilder, executor) -> { + final RexNode cast = + rexBuilder.makeCast( + rexBuilder.getTypeFactory().createSqlType(SqlTypeName.TIME, 6), + rexBuilder.makeLiteral("12:34:56.123456")); + + final RexNode reduced = reduce(rexBuilder, executor, cast).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat( + ((RexLiteral) reduced).getValueAs(TimeString.class).toString(6), + equalTo("12:34:56.123456")); + }); + } + + @Test void testReduceTimestampCastWithMicroseconds() { + checkHighPrecision((rexBuilder, executor) -> { + final RexNode cast = + rexBuilder.makeCast( + rexBuilder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP, 6), + rexBuilder.makeLiteral("2020-01-01 12:34:56.123456")); + + final RexNode reduced = reduce(rexBuilder, executor, cast).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat( + ((RexLiteral) reduced).getValueAs(TimestampString.class).toString(6), + equalTo("2020-01-01 12:34:56.123456")); + }); + } + + @Test void testReduceLiteralWithMicroseconds() { + checkHighPrecision((rexBuilder, executor) -> { + final RexLiteral timeLiteral = + rexBuilder.makeTimeLiteral(new TimeString("12:34:56.123456"), 6); + final RexLiteral timestampLiteral = + rexBuilder.makeTimestampLiteral( + new TimestampString("2020-01-01 12:34:56.123456"), 6); + final RexNode expression = + rexBuilder.makeCall(SqlStdOperatorTable.PLUS, + rexBuilder.makeExactLiteral(BigDecimal.TEN), + rexBuilder.makeExactLiteral(BigDecimal.ONE)); + + final List reducedValues = + reduce(rexBuilder, executor, timeLiteral, expression, timestampLiteral); + + assertThat(reducedValues, hasSize(3)); + assertThat( + ((RexLiteral) reducedValues.get(0)).getValueAs(TimeString.class).toString(6), + equalTo("12:34:56.123456")); + assertThat(((RexLiteral) reducedValues.get(1)).getValue2(), equalTo(11L)); + assertThat( + ((RexLiteral) reducedValues.get(2)).getValueAs(TimestampString.class) + .toString(6), + equalTo("2020-01-01 12:34:56.123456")); + }); + } + + @Test void testReduceTimeToVarcharWithMicroseconds() { + checkHighPrecision((rexBuilder, executor) -> { + final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory(); + final RexNode castToTime = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIME, 6), + rexBuilder.makeLiteral("12:34:56.123456")); + final RexNode castToVarchar = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR, 30), + castToTime); + + final RexNode reduced = reduce(rexBuilder, executor, castToVarchar).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat(((RexLiteral) reduced).getValueAs(String.class), + equalTo("12:34:56.123456")); + }); + } + + @Test void testReduceTimeToVarcharWithMilliseconds() { + check((rexBuilder, executor) -> { + final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory(); + final RexNode castToTime = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIME, 3), + rexBuilder.makeLiteral("12:34:56.987654")); + final RexNode castToVarchar = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR, 30), + castToTime); + + final RexNode reduced = reduce(rexBuilder, executor, castToVarchar).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat(((RexLiteral) reduced).getValueAs(String.class), + equalTo("12:34:56.987")); + }); + } + + @Test void testReduceTimeToVarcharWithZeroMilliseconds() { + check((rexBuilder, executor) -> { + final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory(); + final RexNode castToTime = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIME, 3), + rexBuilder.makeLiteral("12:34:56.000456")); + final RexNode castToVarchar = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR, 30), + castToTime); + + final RexNode reduced = reduce(rexBuilder, executor, castToVarchar).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat(((RexLiteral) reduced).getValueAs(String.class), + equalTo("12:34:56.000")); + }); + } + + @Test void testReduceTimestampToVarcharWithMicroseconds() { + checkHighPrecision((rexBuilder, executor) -> { + final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory(); + final RexNode castToTimestamp = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIMESTAMP, 6), + rexBuilder.makeLiteral("2020-01-01 12:34:56.000456")); + final RexNode castToVarchar = + rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR, 30), + castToTimestamp); + + final RexNode reduced = reduce(rexBuilder, executor, castToVarchar).get(0); + + assertThat(reduced, instanceOf(RexLiteral.class)); + assertThat(((RexLiteral) reduced).getValueAs(String.class), + equalTo("2020-01-01 12:34:56.000456")); + }); + } + + private static void checkHighPrecision(Action action) { + action.check(new RexBuilder(highPrecisionTemporalTypeFactory()), executor()); + } + + private static List reduce(RexBuilder rexBuilder, + RexExecutorImpl executor, RexNode... nodes) { + final List reducedValues = new ArrayList<>(); + executor.reduce(rexBuilder, ImmutableList.copyOf(nodes), reducedValues); + return reducedValues; + } + + private static RelDataTypeFactory highPrecisionTemporalTypeFactory() { + return new JavaTypeFactoryImpl( + new RelDataTypeSystemImpl() { + @Override public int getMaxPrecision(SqlTypeName typeName) { + switch (typeName) { + case TIME: + case TIMESTAMP: + return 6; + default: + return super.getMaxPrecision(typeName); + } + } + }); + } + + private static RexExecutorImpl executor() { + return new RexExecutorImpl( + DataContexts.of( + ImmutableMap.of( + DataContext.Variable.TIME_ZONE.camelName, TimeZone.getTimeZone("GMT"), + DataContext.Variable.LOCALE.camelName, Locale.US))); + } + private void checkConstant(final Object operand, final Function function) { check((rexBuilder, executor) -> {