Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<Expression> translatedOperands) {
Expression operand = translatedOperands.get(0);
return Expressions.call(Expressions.box(operand), "toString");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expressions.call(Primitive.ofBox(operand.getType()).getBoxClass(), "toString", operand)

would be a little better since it doesn't need to create a boxed object for non-nullable fields. But the performance difference should be very trivial

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Updated the implementation.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.opensearch.sql.util.MatcherUtils.verifySchema;

import java.io.IOException;
import java.util.Locale;
import org.json.JSONObject;
import org.junit.Test;
import org.opensearch.sql.common.antlr.SyntaxCheckException;
Expand Down Expand Up @@ -273,6 +274,24 @@ public void testCastNumericSTRING() throws IOException {
verifyDataRows(actual, rows(true));
}

@Test
public void testCastDecimal() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval s = cast(0.99 as string), f = cast(12.9 as float), "
+ "d = cast(100.00 as double), i = cast(2.9 as int) "
+ "| fields s, f, d, i",
TEST_INDEX_DATATYPE_NONNUMERIC));
verifySchema(
actual,
schema("s", "string"),
schema("f", "float"),
schema("d", "double"),
schema("i", "int"));
verifyDataRows(actual, rows("0.99", 12.9f, 100.0d, 2));
}

@Test
public void testCastDate() throws IOException {
JSONObject actual =
Expand Down Expand Up @@ -432,4 +451,17 @@ public void testCastToIP() throws IOException {
"IP address string 'invalid_ip' is not valid. Error details: invalid_ip IP Address error:"
+ " validation options do not allow you to specify a non-segmented single value");
}

@Test
public void testCastDoubleAsString() throws IOException {
JSONObject actual =
executeQuery(
String.format(
Locale.ROOT,
"source=%s | head 1 | eval d = cast(0 as double) | eval s = cast(d as string) |"
+ " fields s",
TEST_INDEX_STATE_COUNTRY));
verifySchema(actual, schema("s", "string"));
verifyDataRows(actual, rows("0.0"));
}
}
Loading