diff --git a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java index 49cb8ac1eec..d8bea5e3371 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java +++ b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java @@ -105,28 +105,29 @@ public RexNode makeCast( boolean safe, RexLiteral format) { final SqlTypeName sqlType = type.getSqlTypeName(); + RelDataType sourceType = exp.getType(); // Calcite bug which doesn't consider to cast literal to boolean if (exp instanceof RexLiteral && sqlType == SqlTypeName.BOOLEAN) { if (exp.equals(makeLiteral("1", typeFactory.createSqlType(SqlTypeName.CHAR, 1)))) { return makeLiteral(true, type); } else if (exp.equals(makeLiteral("0", typeFactory.createSqlType(SqlTypeName.CHAR, 1)))) { return makeLiteral(false, type); - } else if (SqlTypeUtil.isExactNumeric(exp.getType())) { + } else if (SqlTypeUtil.isExactNumeric(sourceType)) { return makeCall( type, SqlStdOperatorTable.NOT_EQUALS, - ImmutableList.of(exp, makeZeroLiteral(exp.getType()))); + ImmutableList.of(exp, makeZeroLiteral(sourceType))); // TODO https://github.com/opensearch-project/sql/issues/3443 // Current, we align the behaviour of Spark and Postgres, to align with OpenSearch V2, // enable following commented codes. // } else { // return makeCall(type, // SqlStdOperatorTable.NOT_EQUALS, - // ImmutableList.of(exp, makeZeroLiteral(exp.getType()))); + // ImmutableList.of(exp, makeZeroLiteral(sourceType))); } } else if (OpenSearchTypeFactory.isUserDefinedType(type)) { var udt = ((AbstractExprRelDataType>) type).getUdt(); - var argExprType = OpenSearchTypeFactory.convertRelDataTypeToExprType(exp.getType()); + var argExprType = OpenSearchTypeFactory.convertRelDataTypeToExprType(sourceType); return switch (udt) { case EXPR_DATE -> makeCall(type, PPLBuiltinOperators.DATE, List.of(exp)); case EXPR_TIME -> makeCall(type, PPLBuiltinOperators.TIME, List.of(exp)); @@ -150,6 +151,13 @@ public RexNode makeCast( String.format(Locale.ROOT, "Cannot cast from %s to %s", argExprType, udt.name())); }; } + // Use a custom operator when casting floating point or decimal number to a character type. + // This patch is necessary because in Calcite, 0.0F is cast to 0E0, decimal 0.x to x + else if ((SqlTypeUtil.isApproximateNumeric(sourceType) || SqlTypeUtil.isDecimal(sourceType)) + && SqlTypeUtil.isCharacter(type)) { + // NUMBER_TO_STRING uses java's built-in method to get the string representation of a number + return makeCall(type, PPLBuiltinOperators.NUMBER_TO_STRING, List.of(exp)); + } return super.makeCast(pos, type, exp, matchNullability, safe, format); } } diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java index 257f0c04668..7a274a830e2 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java @@ -78,6 +78,7 @@ import org.opensearch.sql.expression.function.udf.math.DivideFunction; import org.opensearch.sql.expression.function.udf.math.EulerFunction; import org.opensearch.sql.expression.function.udf.math.ModFunction; +import org.opensearch.sql.expression.function.udf.math.NumberToStringFunction; /** Defines functions and operators that are implemented only by PPL */ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { @@ -355,6 +356,8 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("query_string", false); public static final SqlOperator MULTI_MATCH = RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("multi_match", false); + public static final SqlOperator NUMBER_TO_STRING = + new NumberToStringFunction().toUDF("NUMBER_TO_STRING"); /** * Returns the PPL specific operator table, creating it if necessary. diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/math/NumberToStringFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/math/NumberToStringFunction.java new file mode 100644 index 00000000000..24488ea5746 --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/math/NumberToStringFunction.java @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.udf.math; + +import java.util.List; +import org.apache.calcite.adapter.enumerable.NotNullImplementor; +import org.apache.calcite.adapter.enumerable.NullPolicy; +import org.apache.calcite.adapter.enumerable.RexToLixTranslator; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.sql.type.SqlReturnTypeInference; +import org.opensearch.sql.calcite.utils.PPLOperandTypes; +import org.opensearch.sql.calcite.utils.PPLReturnTypes; +import org.opensearch.sql.expression.function.ImplementorUDF; +import org.opensearch.sql.expression.function.UDFOperandMetadata; + +/** + * A custom implementation of number to string cast. + * + *
This operator is necessary because Calcite's built-in CAST converts floating point 0.0 to 0E0,
+ * and converts decimal 0.123 to .123 when casting them to string.
+ */
+public class NumberToStringFunction extends ImplementorUDF {
+ public NumberToStringFunction() {
+ super(new NumberToStringImplementor(), NullPolicy.ANY);
+ }
+
+ @Override
+ public SqlReturnTypeInference getReturnTypeInference() {
+ return PPLReturnTypes.STRING_FORCE_NULLABLE;
+ }
+
+ @Override
+ public UDFOperandMetadata getOperandMetadata() {
+ return PPLOperandTypes.NUMERIC;
+ }
+
+ public static class NumberToStringImplementor implements NotNullImplementor {
+
+ @Override
+ public Expression implement(
+ RexToLixTranslator translator, RexCall call, List