Skip to content

Commit 324aae0

Browse files
committed
Fix float to string casting precision lost with custom FormatNumberFunction
This commit fixes float to string casting by replacing the use of SqlLibraryOperators.FORMAT_NUMBER with a custom FormatNumberFunction implementation. The new implementation converts the number to a BigDecimal before formatting to preserve precision and avoid issues like 6.2 becoming 6.199999809265137. Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>
1 parent 569035d commit 324aae0

3 files changed

Lines changed: 75 additions & 10 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.apache.calcite.rex.RexLiteral;
1616
import org.apache.calcite.rex.RexNode;
1717
import org.apache.calcite.sql.SqlIntervalQualifier;
18-
import org.apache.calcite.sql.fun.SqlLibraryOperators;
1918
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
2019
import org.apache.calcite.sql.parser.SqlParserPos;
2120
import org.apache.calcite.sql.type.SqlTypeName;
@@ -29,17 +28,9 @@
2928
import org.opensearch.sql.expression.function.PPLBuiltinOperators;
3029

3130
public class ExtendedRexBuilder extends RexBuilder {
32-
/**
33-
* Formats double values in PPL using a non-scientific notation, displaying up to 16 digits after
34-
* the decimal point. This formatting is consistent with PPL V2.
35-
*/
36-
private static final String DOUBLE_FORMAT = "0.0###############";
37-
38-
private final RexLiteral doubleFormat;
3931

4032
public ExtendedRexBuilder(RexBuilder rexBuilder) {
4133
super(rexBuilder.getTypeFactory());
42-
doubleFormat = makeLiteral(DOUBLE_FORMAT);
4334
}
4435

4536
public RexNode coalesce(RexNode... nodes) {
@@ -166,7 +157,9 @@ public RexNode makeCast(
166157
else if (SqlTypeUtil.isApproximateNumeric(exp.getType())
167158
&& SqlTypeUtil.isCharacter(type)
168159
&& format.getType().getSqlTypeName() == SqlTypeName.NULL) {
169-
return makeCall(type, SqlLibraryOperators.FORMAT_NUMBER, List.of(exp, doubleFormat));
160+
// Use a custom FORMAT_NUMBER which first convert the number to a BigDecimal, then
161+
// calls SqlFunctions.formatNumber
162+
return makeCall(type, PPLBuiltinOperators.FORMAT_NUMBER, List.of(exp));
170163
}
171164
return super.makeCast(pos, type, exp, matchNullability, safe, format);
172165
}

core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import org.opensearch.sql.expression.function.udf.math.ConvFunction;
7878
import org.opensearch.sql.expression.function.udf.math.DivideFunction;
7979
import org.opensearch.sql.expression.function.udf.math.EulerFunction;
80+
import org.opensearch.sql.expression.function.udf.math.FormatNumberFunction;
8081
import org.opensearch.sql.expression.function.udf.math.ModFunction;
8182

8283
/** Defines functions and operators that are implemented only by PPL */
@@ -355,6 +356,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
355356
RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("query_string", false);
356357
public static final SqlOperator MULTI_MATCH =
357358
RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("multi_match", false);
359+
public static final SqlOperator FORMAT_NUMBER = new FormatNumberFunction().toUDF("FORMAT_NUMBER");
358360

359361
/**
360362
* Returns the PPL specific operator table, creating it if necessary.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.function.udf.math;
7+
8+
import java.math.BigDecimal;
9+
import java.util.List;
10+
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
11+
import org.apache.calcite.adapter.enumerable.NullPolicy;
12+
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
13+
import org.apache.calcite.linq4j.tree.Expression;
14+
import org.apache.calcite.linq4j.tree.Expressions;
15+
import org.apache.calcite.rex.RexCall;
16+
import org.apache.calcite.runtime.SqlFunctions;
17+
import org.apache.calcite.sql.type.SqlReturnTypeInference;
18+
import org.opensearch.sql.calcite.utils.PPLOperandTypes;
19+
import org.opensearch.sql.calcite.utils.PPLReturnTypes;
20+
import org.opensearch.sql.expression.function.ImplementorUDF;
21+
import org.opensearch.sql.expression.function.UDFOperandMetadata;
22+
23+
/**
24+
* A custom implementation of FormatNumber in replace of SqlLibraryOperators.FORMAT_NUMBER
25+
*
26+
* <p>The operators SqlLibraryOperators.FORMAT_NUMBER will convert a float to double with code
27+
* equivalent to {@code ((Number) float).doubleValue()}, which will lead to a loss in precision.
28+
* E.g. 6.2 becomes 6.199999809265137. This operator fix the problem by converting the number to a
29+
* BigDecimal before formatting it.
30+
*/
31+
public class FormatNumberFunction extends ImplementorUDF {
32+
33+
/**
34+
* Formats double values in PPL using a non-scientific notation, displaying up to 16 digits after
35+
* the decimal point. This formatting is consistent with PPL V2.
36+
*/
37+
private static final String DOUBLE_FORMAT = "0.0###############";
38+
39+
public FormatNumberFunction() {
40+
super(new FormatNumberImplementor(), NullPolicy.ANY);
41+
}
42+
43+
@Override
44+
public SqlReturnTypeInference getReturnTypeInference() {
45+
return PPLReturnTypes.STRING_FORCE_NULLABLE;
46+
}
47+
48+
@Override
49+
public UDFOperandMetadata getOperandMetadata() {
50+
return PPLOperandTypes.NUMERIC;
51+
}
52+
53+
public static class FormatNumberImplementor implements NotNullImplementor {
54+
55+
@Override
56+
public Expression implement(
57+
RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) {
58+
Expression operand = translatedOperands.get(0);
59+
Expression decimal =
60+
Expressions.call(
61+
FormatNumberImplementor.class, "convertNumber", Expressions.box(operand));
62+
return Expressions.call(
63+
SqlFunctions.class, "formatNumber", decimal, Expressions.constant(DOUBLE_FORMAT));
64+
}
65+
66+
public static BigDecimal convertNumber(Number number) {
67+
return new BigDecimal(number.toString());
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)