diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index b11d83e1a94b..833fab94da35 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -6181,6 +6181,7 @@ SqlNode BuiltinFunctionCall() : final SqlNode node; final SqlLiteral style; // mssql convert 'style' operand final SqlFunction f; + final SqlNode format; } { //~ FUNCTIONS WITH SPECIAL SYNTAX --------------------------------------- @@ -6197,6 +6198,7 @@ SqlNode BuiltinFunctionCall() : | e = IntervalQualifier() { args.add(e); } ) + [ format = StringLiteral() { args.add(format); } ] { return f.createCall(s.end(this), args); } 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 220d33f42505..e04971a92867 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 @@ -3262,11 +3262,21 @@ private static class CastImplementor extends AbstractRexCallImplementor { @Override Expression implementSafe(final RexToLixTranslator translator, final RexCall call, final List argValueList) { - assert call.getOperands().size() == 1; + assert call.getOperands().size() <= 2; final RelDataType sourceType = call.getOperands().get(0).getType(); - // Short-circuit if no cast is required RexNode arg = call.getOperands().get(0); + ConstantExpression formatExpr; + if (call.getOperands().size() > 1) { + RexLiteral format = (RexLiteral) call.getOperands().get(1); + formatExpr = + (ConstantExpression) RexToLixTranslator.translateLiteral(format, format.getType(), + translator.typeFactory, NullAs.NULL); + } else { + formatExpr = NULL_EXPR; + } + + // Short-circuit if no cast is required if (call.getType().equals(sourceType)) { // No cast required, omit cast return argValueList.get(0); @@ -3282,7 +3292,7 @@ private static class CastImplementor extends AbstractRexCallImplementor { nullifyType(translator.typeFactory, call.getType(), false); boolean safe = call.getKind() == SqlKind.SAFE_CAST; return translator.translateCast(sourceType, - targetType, argValueList.get(0), safe); + targetType, argValueList.get(0), safe, formatExpr); } private static RelDataType nullifyType(JavaTypeFactory typeFactory, 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 5389fdccf721..d533a8fbfa9a 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 @@ -289,8 +289,9 @@ Expression translateCast( RelDataType sourceType, RelDataType targetType, Expression operand, - boolean safe) { - Expression convert = getConvertExpression(sourceType, targetType, operand); + boolean safe, + ConstantExpression format) { + Expression convert = getConvertExpression(sourceType, targetType, operand, format); Expression convert2 = checkExpressionPadTruncate(convert, sourceType, targetType); Expression convert3 = expressionHandlingSafe(convert2, safe); return scaleValue(sourceType, targetType, convert3); @@ -299,7 +300,8 @@ Expression translateCast( private Expression getConvertExpression( RelDataType sourceType, RelDataType targetType, - Expression operand) { + Expression operand, + ConstantExpression format) { final Supplier defaultExpression = () -> EnumUtils.convert(operand, typeFactory.getJavaClass(targetType)); @@ -318,141 +320,20 @@ private Expression getConvertExpression( } case DATE: - return translateCastToDate(sourceType, operand, defaultExpression); + return translateCastToDate(sourceType, operand, format, defaultExpression); case TIME: - return translateCastToTime(sourceType, operand, defaultExpression); + return translateCastToTime(sourceType, operand, format, defaultExpression); case TIME_WITH_LOCAL_TIME_ZONE: - switch (sourceType.getSqlTypeName()) { - case CHAR: - case VARCHAR: - return Expressions.call(BuiltInMethod.STRING_TO_TIME_WITH_LOCAL_TIME_ZONE.method, - operand); - - case TIME: - return 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)); - - case TIMESTAMP: - return 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)); - - case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( - BuiltInMethod - .TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME_WITH_LOCAL_TIME_ZONE - .method, - operand)); - - default: - return defaultExpression.get(); - } + return translateCastToTimeWithLocalTimeZone(sourceType, operand, format, defaultExpression); case TIMESTAMP: - switch (sourceType.getSqlTypeName()) { - case CHAR: - case VARCHAR: - return Expressions.call(BuiltInMethod.STRING_TO_TIMESTAMP.method, - operand); - - case DATE: - return Expressions.multiply(Expressions.convert_(operand, long.class), - Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)); - - case TIME: - return 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)); - - case TIME_WITH_LOCAL_TIME_ZONE: - return 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))); - - case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( - BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP.method, - operand, - Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); - - default: - return defaultExpression.get(); - } + return translateCastToTimeStamp(sourceType, operand, format, defaultExpression); case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - switch (sourceType.getSqlTypeName()) { - case CHAR: - case VARCHAR: - return Expressions.call( - BuiltInMethod.STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, - operand); - - case DATE: - return 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)); - - case TIME: - return 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)); - - case TIME_WITH_LOCAL_TIME_ZONE: - return 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)); - - case TIMESTAMP: - return 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)); - - default: - return defaultExpression.get(); - } + return translateCastToTimeStampWithLocalTimeZone(sourceType, operand, format, + defaultExpression); case BOOLEAN: switch (sourceType.getSqlTypeName()) { @@ -469,34 +350,35 @@ private Expression getConvertExpression( final SqlIntervalQualifier interval = sourceType.getIntervalQualifier(); switch (sourceType.getSqlTypeName()) { + // If format string is supplied, return formatted date/time/timestamp case DATE: - return RexImpTable.optimize2(operand, - Expressions.call(BuiltInMethod.UNIX_DATE_TO_STRING.method, - operand)); + return Expressions.isConstantNull(format) ? RexImpTable.optimize2(operand, + Expressions.call(BuiltInMethod.UNIX_DATE_TO_STRING.method, operand)) + : Expressions.call(BuiltInMethod.FORMAT_DATE.method, DataContext.ROOT, format, operand); case TIME: - return RexImpTable.optimize2(operand, - Expressions.call(BuiltInMethod.UNIX_TIME_TO_STRING.method, - operand)); + return Expressions.isConstantNull(format) ? RexImpTable.optimize2(operand, + Expressions.call(BuiltInMethod.UNIX_TIME_TO_STRING.method, operand)) + : Expressions.call(BuiltInMethod.FORMAT_TIME.method, DataContext.ROOT, format, operand); case TIME_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( - BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_STRING.method, - operand, - Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + return Expressions.isConstantNull(format) ? RexImpTable.optimize2(operand, + Expressions.call(BuiltInMethod.TIME_WITH_LOCAL_TIME_ZONE_TO_STRING.method, operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))) + : Expressions.call(BuiltInMethod.FORMAT_TIME.method, DataContext.ROOT, format, operand); case TIMESTAMP: - return RexImpTable.optimize2(operand, - Expressions.call(BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, - operand)); + return Expressions.isConstantNull(format) ? RexImpTable.optimize2(operand, + Expressions.call(BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, operand)) + : Expressions.call(BuiltInMethod.FORMAT_TIMESTAMP.method, DataContext.ROOT, format, + operand); case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( - BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_STRING.method, - operand, - Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + return Expressions.isConstantNull(format) ? RexImpTable.optimize2(operand, + Expressions.call(BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_STRING.method, + operand, Expressions.call(BuiltInMethod.TIME_ZONE.method, root))) + : Expressions.call(BuiltInMethod.FORMAT_TIMESTAMP.method, DataContext.ROOT, format, + operand); case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: @@ -647,62 +529,263 @@ private static Expression checkExpressionPadTruncate( } } + private Expression translateCastToDate(RelDataType sourceType, + Expression operand, ConstantExpression format, + Supplier defaultExpression) { + + Expression translatedDate; + switch (sourceType.getSqlTypeName()) { + case CHAR: + case VARCHAR: + translatedDate = Expressions.call(BuiltInMethod.STRING_TO_DATE.method, operand); + break; + + case TIMESTAMP: + translatedDate = + Expressions.convert_( + Expressions.call(BuiltInMethod.FLOOR_DIV.method, + operand, Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), + int.class); + break; + + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + translatedDate = + RexImpTable.optimize2( + operand, Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_DATE.method, + operand, + Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; + + default: + return defaultExpression.get(); + } + + // If format string is supplied, return formatted date + return Expressions.isConstantNull(format) ? translatedDate + : Expressions.call(BuiltInMethod.FORMAT_DATE.method, DataContext.ROOT, format, + translatedDate); + } + private Expression translateCastToTime(RelDataType sourceType, - Expression operand, Supplier defaultExpression) { + Expression operand, ConstantExpression format, Supplier defaultExpression) { + + Expression translatedTime; switch (sourceType.getSqlTypeName()) { case CHAR: case VARCHAR: - return Expressions.call(BuiltInMethod.STRING_TO_TIME.method, operand); + translatedTime = Expressions.call(BuiltInMethod.STRING_TO_TIME.method, operand); + break; case TIME_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( + translatedTime = + 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: - return Expressions.convert_( - Expressions.call(BuiltInMethod.FLOOR_MOD.method, + translatedTime = + Expressions.convert_( + Expressions.call(BuiltInMethod.FLOOR_MOD.method, operand, Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), int.class); + break; case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - return RexImpTable.optimize2(operand, - Expressions.call( + translatedTime = + RexImpTable.optimize2( + operand, Expressions.call( BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME.method, operand, Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; default: return defaultExpression.get(); } + + // If format string is supplied, return formatted time + return Expressions.isConstantNull(format) ? translatedTime + : Expressions.call(BuiltInMethod.FORMAT_TIME.method, DataContext.ROOT, format, + translatedTime); } - private Expression translateCastToDate(RelDataType sourceType, - Expression operand, Supplier defaultExpression) { + private Expression translateCastToTimeWithLocalTimeZone(RelDataType sourceType, + Expression operand, ConstantExpression format, Supplier defaultExpression) { + + Expression translatedTime; switch (sourceType.getSqlTypeName()) { case CHAR: case VARCHAR: - return Expressions.call(BuiltInMethod.STRING_TO_DATE.method, operand); - + translatedTime = + Expressions.call(BuiltInMethod.STRING_TO_TIME_WITH_LOCAL_TIME_ZONE.method, operand); + break; + case TIME: + translatedTime = + 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: - return Expressions.convert_( - Expressions.call(BuiltInMethod.FLOOR_DIV.method, - operand, Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)), - int.class); + translatedTime = + 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: - return RexImpTable.optimize2(operand, - Expressions.call( - BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_DATE.method, + translatedTime = + RexImpTable.optimize2( + operand, Expressions.call( + BuiltInMethod + .TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIME_WITH_LOCAL_TIME_ZONE + .method, + operand)); + break; + + default: + return defaultExpression.get(); + } + + // If format string is supplied, return formatted time + return Expressions.isConstantNull(format) ? translatedTime + : Expressions.call(BuiltInMethod.FORMAT_TIME.method, DataContext.ROOT, format, + translatedTime); + } + + private Expression translateCastToTimeStamp(RelDataType sourceType, + Expression operand, ConstantExpression format, Supplier defaultExpression) { + + Expression translatedTimeStamp; + + switch (sourceType.getSqlTypeName()) { + case CHAR: + case VARCHAR: + translatedTimeStamp = + Expressions.call(BuiltInMethod.STRING_TO_TIMESTAMP.method, operand); + break; + + case DATE: + translatedTimeStamp = + Expressions.multiply(Expressions.convert_(operand, long.class), + Expressions.constant(DateTimeUtils.MILLIS_PER_DAY)); + break; + + case TIME: + translatedTimeStamp = + 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)); + break; + + case TIME_WITH_LOCAL_TIME_ZONE: + translatedTimeStamp = + 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: + translatedTimeStamp = + RexImpTable.optimize2( + operand, Expressions.call( + BuiltInMethod.TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_TIMESTAMP.method, operand, Expressions.call(BuiltInMethod.TIME_ZONE.method, root))); + break; + default: + return defaultExpression.get(); + } + // If format string is supplied, return formatted timestamp + return Expressions.isConstantNull(format) ? translatedTimeStamp + : Expressions.call(BuiltInMethod.FORMAT_TIMESTAMP.method, DataContext.ROOT, format, + translatedTimeStamp); + } + + private Expression translateCastToTimeStampWithLocalTimeZone(RelDataType sourceType, + Expression operand, ConstantExpression format, Supplier defaultExpression) { + + Expression translatedTimeStamp; + switch (sourceType.getSqlTypeName()) { + case CHAR: + case VARCHAR: + translatedTimeStamp = + Expressions.call(BuiltInMethod.STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, + operand); + break; + + case DATE: + translatedTimeStamp = + 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: + translatedTimeStamp = + 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: + translatedTimeStamp = + 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: + translatedTimeStamp = + 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; default: return defaultExpression.get(); } + + // If format string is supplied, return formatted timestamp + return Expressions.isConstantNull(format) ? translatedTimeStamp + : Expressions.call(BuiltInMethod.FORMAT_TIMESTAMP.method, DataContext.ROOT, format, + translatedTimeStamp); } /** 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 69149c167949..15acec572189 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -531,7 +531,7 @@ public RexNode makeNewInvocation( public RexNode makeCast( RelDataType type, RexNode exp) { - return makeCast(type, exp, false, false); + return makeCast(type, exp, false, false, constantNull); } @Deprecated // to be removed before 2.0 @@ -539,7 +539,7 @@ public RexNode makeCast( RelDataType type, RexNode exp, boolean matchNullability) { - return makeCast(type, exp, matchNullability, false); + return makeCast(type, exp, matchNullability, false, constantNull); } /** @@ -562,6 +562,32 @@ public RexNode makeCast( RexNode exp, boolean matchNullability, boolean safe) { + return makeCast(type, exp, matchNullability, safe, constantNull); + } + + + /** + * Creates a call to the CAST operator, expanding if possible, and optionally + * also preserving nullability, and optionally in safe mode. + * + *

Tries to expand the cast, and therefore the result may be something + * other than a {@link RexCall} to the CAST operator, such as a + * {@link RexLiteral}. + * + * @param type Type to cast to + * @param exp Expression being cast + * @param matchNullability Whether to ensure the result has the same + * nullability as {@code type} + * @param safe Whether to return NULL if cast fails + * @param format Type Format to cast into + * @return Call to CAST operator + */ + public RexNode makeCast( + RelDataType type, + RexNode exp, + boolean matchNullability, + boolean safe, + RexLiteral format) { final SqlTypeName sqlType = type.getSqlTypeName(); if (exp instanceof RexLiteral) { RexLiteral literal = (RexLiteral) exp; @@ -628,7 +654,7 @@ public RexNode makeCast( if (type.isNullable() && !literal2.getType().isNullable() && matchNullability) { - return makeAbstractCast(type, literal2, safe); + return makeAbstractCast(type, literal2, safe, format); } return literal2; } @@ -642,7 +668,7 @@ public RexNode makeCast( && SqlTypeUtil.isExactNumeric(type)) { return makeCastBooleanToExact(type, exp); } - return makeAbstractCast(type, exp, safe); + return makeAbstractCast(type, exp, safe, format); } /** Returns the lowest granularity unit for the given unit. @@ -832,6 +858,25 @@ public RexNode makeAbstractCast(RelDataType type, RexNode exp, boolean safe) { return new RexCall(type, operator, ImmutableList.of(exp)); } + /** + * Creates a call to CAST or SAFE_CAST operator with a FORMAT clause. + * + * @param type Type to cast to + * @param exp Expression being cast + * @param safe Whether to return NULL if cast fails + * @param format Conversion format for target type + * @return Call to CAST operator + */ + public RexNode makeAbstractCast(RelDataType type, RexNode exp, boolean safe, RexLiteral format) { + SqlOperator operator = + safe ? SqlLibraryOperators.SAFE_CAST + : SqlStdOperatorTable.CAST; + if (format.isNull()) { + return new RexCall(type, operator, ImmutableList.of(exp)); + } + return new RexCall(type, operator, ImmutableList.of(exp, format)); + } + /** * Makes a reinterpret cast. * diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java index 81da37c7e706..e6e1c257806d 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java @@ -26,17 +26,20 @@ import org.apache.calcite.sql.SqlFunctionCategory; import org.apache.calcite.sql.SqlIntervalQualifier; import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlLiteral; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlOperandCountRange; import org.apache.calcite.sql.SqlOperatorBinding; import org.apache.calcite.sql.SqlSyntax; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.SqlWriter; +import org.apache.calcite.sql.parser.SqlParserPos; import org.apache.calcite.sql.type.InferTypes; import org.apache.calcite.sql.type.SqlOperandCountRanges; import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeMappingRule; +import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.sql.validate.SqlMonotonicity; import org.apache.calcite.sql.validate.SqlValidator; @@ -109,7 +112,7 @@ public SqlCastFunction(String name, SqlKind kind) { static SqlReturnTypeInference returnTypeInference(boolean safe) { return opBinding -> { - assert opBinding.getOperandCount() == 2; + assert opBinding.getOperandCount() <= 3; final RelDataType ret = deriveType(opBinding.getTypeFactory(), opBinding.getOperandType(0), opBinding.getOperandType(1), safe); @@ -191,12 +194,12 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory } @Override public String getSignatureTemplate(final int operandsCount) { - assert operandsCount == 2; - return "{0}({1} AS {2})"; + assert operandsCount <= 3; + return "{0}({1} AS {2} [FORMAT {3}])"; } @Override public SqlOperandCountRange getOperandCountRange() { - return SqlOperandCountRanges.of(2); + return SqlOperandCountRanges.between(2, 3); } /** @@ -209,6 +212,9 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory boolean throwOnFailure) { final SqlNode left = callBinding.operand(0); final SqlNode right = callBinding.operand(1); + final SqlLiteral format = callBinding.getOperandCount() > 2 + ? (SqlLiteral) callBinding.operand(2) : SqlLiteral.createNull(SqlParserPos.ZERO); + if (SqlUtil.isNullLiteral(left, false) || left instanceof SqlDynamicParam) { return true; @@ -239,6 +245,12 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory } return false; } + + // Validate format argument is string type if included + if (!SqlUtil.isNullLiteral(format, false) + && !SqlLiteral.valueMatchesType(format.getValue(), SqlTypeName.CHAR)) { + return false; + } return true; } @@ -251,7 +263,7 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory SqlCall call, int leftPrec, int rightPrec) { - assert call.operandCount() == 2; + assert call.operandCount() <= 3; final SqlWriter.Frame frame = writer.startFunCall(getName()); call.operand(0).unparse(writer, 0, 0); writer.sep("AS"); @@ -259,6 +271,10 @@ private static RelDataType createTypeWithNullabilityFromExpr(RelDataTypeFactory writer.sep("INTERVAL"); } call.operand(1).unparse(writer, 0, 0); + if (call.getOperandList().size() > 2) { + writer.sep("FORMAT"); + call.operand(2).unparse(writer, 0, 0); + } writer.endFunCall(frame); } 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 ccd946c437f8..e8f7ded9a94d 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java @@ -684,7 +684,13 @@ protected RexNode convertCast( final boolean safe = kind == SqlKind.SAFE_CAST; final SqlNode left = call.operand(0); final SqlNode right = call.operand(1); + final SqlLiteral format = call.getOperandList().size() > 2 + ? call.operand(2) : SqlLiteral.createNull(SqlParserPos.ZERO); + final RexBuilder rexBuilder = cx.getRexBuilder(); + final RexNode arg = cx.convertExpression(left); + final RexLiteral formatArg = (RexLiteral) cx.convertLiteral(format); + if (right instanceof SqlIntervalQualifier) { final SqlIntervalQualifier intervalQualifier = (SqlIntervalQualifier) right; @@ -716,14 +722,13 @@ protected RexNode convertCast( return castToValidatedType(call, value, validator, rexBuilder, safe); } - final RexNode arg = cx.convertExpression(left); final SqlDataTypeSpec dataType = (SqlDataTypeSpec) right; RelDataType type = SqlCastFunction.deriveType(cx.getTypeFactory(), arg.getType(), dataType.deriveType(validator), safe); if (SqlUtil.isNullLiteral(left, false)) { validator.setValidatedNodeType(left, type); - return cx.convertExpression(left); + return arg; } if (null != dataType.getCollectionsTypeName()) { RelDataType argComponentType = arg.getType().getComponentType(); @@ -752,7 +757,7 @@ protected RexNode convertCast( type = typeFactory.createTypeWithNullability(type, isn); } } - return rexBuilder.makeCast(type, arg, safe, safe); + return rexBuilder.makeCast(type, arg, safe, safe, formatArg); } protected RexNode convertFloorCeil(SqlRexContext cx, SqlCall call) {