Skip to content

Commit 96e48e2

Browse files
PPL tostring() implementation issue #4492 (#4497) (#4747)
--------- (cherry picked from commit 6783c89) Signed-off-by: Asif Bashar <[email protected]> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 86a0118 commit 96e48e2

File tree

9 files changed

+592
-0
lines changed

9 files changed

+592
-0
lines changed

core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ private PPLOperandTypes() {}
114114
SqlTypeFamily.INTEGER,
115115
SqlTypeFamily.INTEGER));
116116

117+
public static final UDFOperandMetadata NUMERIC_STRING_OR_STRING_STRING =
118+
UDFOperandMetadata.wrap(
119+
(CompositeOperandTypeChecker)
120+
(OperandTypes.family(SqlTypeFamily.NUMERIC, SqlTypeFamily.STRING))
121+
.or(OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING)));
122+
117123
public static final UDFOperandMetadata NUMERIC_NUMERIC_OPTIONAL_NUMERIC =
118124
UDFOperandMetadata.wrap(
119125
(CompositeOperandTypeChecker)

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
@@ -67,6 +67,7 @@
6767
import org.opensearch.sql.expression.function.udf.RexExtractMultiFunction;
6868
import org.opensearch.sql.expression.function.udf.RexOffsetFunction;
6969
import org.opensearch.sql.expression.function.udf.SpanFunction;
70+
import org.opensearch.sql.expression.function.udf.ToStringFunction;
7071
import org.opensearch.sql.expression.function.udf.condition.EarliestFunction;
7172
import org.opensearch.sql.expression.function.udf.condition.EnhancedCoalesceFunction;
7273
import org.opensearch.sql.expression.function.udf.condition.LatestFunction;
@@ -411,6 +412,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
411412
RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("multi_match", false);
412413
public static final SqlOperator NUMBER_TO_STRING =
413414
new NumberToStringFunction().toUDF("NUMBER_TO_STRING");
415+
public static final SqlOperator TOSTRING = new ToStringFunction().toUDF("TOSTRING");
414416
public static final SqlOperator WIDTH_BUCKET =
415417
new org.opensearch.sql.expression.function.udf.binning.WidthBucketFunction()
416418
.toUDF("WIDTH_BUCKET");

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIMESTAMPDIFF;
212212
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIME_FORMAT;
213213
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIME_TO_SEC;
214+
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TOSTRING;
214215
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TO_DAYS;
215216
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TO_SECONDS;
216217
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TRANSFORM;
@@ -953,6 +954,13 @@ void populate() {
953954
registerOperator(WEEKOFYEAR, PPLBuiltinOperators.WEEK);
954955

955956
registerOperator(INTERNAL_PATTERN_PARSER, PPLBuiltinOperators.PATTERN_PARSER);
957+
registerOperator(TOSTRING, PPLBuiltinOperators.TOSTRING);
958+
register(
959+
TOSTRING,
960+
(FunctionImp1)
961+
(builder, source) ->
962+
builder.makeCast(TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR, true), source),
963+
PPLTypeChecker.family(SqlTypeFamily.ANY));
956964

957965
// Register MVJOIN to use Calcite's ARRAY_JOIN
958966
register(
@@ -1121,6 +1129,7 @@ void populate() {
11211129
SqlTypeFamily.INTEGER,
11221130
SqlTypeFamily.INTEGER)),
11231131
false));
1132+
11241133
register(
11251134
LOG,
11261135
(FunctionImp2)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.function.udf;
7+
8+
import java.math.BigDecimal;
9+
import java.math.BigInteger;
10+
import java.text.NumberFormat;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
14+
import org.apache.calcite.adapter.enumerable.NullPolicy;
15+
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
16+
import org.apache.calcite.linq4j.function.Strict;
17+
import org.apache.calcite.linq4j.tree.Expression;
18+
import org.apache.calcite.linq4j.tree.Expressions;
19+
import org.apache.calcite.rex.RexCall;
20+
import org.apache.calcite.runtime.SqlFunctions;
21+
import org.apache.calcite.sql.*;
22+
import org.apache.calcite.sql.type.SqlReturnTypeInference;
23+
import org.opensearch.sql.calcite.utils.PPLOperandTypes;
24+
import org.opensearch.sql.calcite.utils.PPLReturnTypes;
25+
import org.opensearch.sql.expression.function.ImplementorUDF;
26+
import org.opensearch.sql.expression.function.UDFOperandMetadata;
27+
28+
/**
29+
* A custom implementation of number/boolean to string .
30+
*
31+
* <p>This operator is necessary because tostring has following requirements "binary" Converts a
32+
* number to a binary value. "hex" Converts the number to a hexadecimal value. "commas" Formats the
33+
* number with commas. If the number includes a decimal, the function rounds the number to nearest
34+
* two decimal places. "duration" Converts the value in seconds to the readable time format
35+
* HH:MM:SS. if not format parameter provided, then consider value as boolean
36+
*/
37+
public class ToStringFunction extends ImplementorUDF {
38+
public ToStringFunction() {
39+
super(
40+
new org.opensearch.sql.expression.function.udf.ToStringFunction.ToStringImplementor(),
41+
NullPolicy.ANY);
42+
}
43+
44+
public static final String DURATION_FORMAT = "duration";
45+
public static final String DURATION_MILLIS_FORMAT = "duration_millis";
46+
public static final String HEX_FORMAT = "hex";
47+
public static final String COMMAS_FORMAT = "commas";
48+
public static final String BINARY_FORMAT = "binary";
49+
public static final SqlFunctions.DateFormatFunction dateTimeFormatter =
50+
new SqlFunctions.DateFormatFunction();
51+
public static final String FORMAT_24_HOUR = "%H:%M:%S";
52+
53+
@Override
54+
public SqlReturnTypeInference getReturnTypeInference() {
55+
return PPLReturnTypes.STRING_FORCE_NULLABLE;
56+
}
57+
58+
@Override
59+
public UDFOperandMetadata getOperandMetadata() {
60+
return PPLOperandTypes.NUMERIC_STRING_OR_STRING_STRING;
61+
}
62+
63+
public static class ToStringImplementor implements NotNullImplementor {
64+
65+
@Override
66+
public Expression implement(
67+
RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) {
68+
Expression fieldValue = translatedOperands.get(0);
69+
Expression format = translatedOperands.get(1);
70+
return Expressions.call(ToStringFunction.class, "toString", fieldValue, format);
71+
}
72+
}
73+
74+
@Strict
75+
public static String toString(BigDecimal num, String format) {
76+
if (format.equals(DURATION_FORMAT)) {
77+
78+
return dateTimeFormatter.formatTime(FORMAT_24_HOUR, num.toBigInteger().intValue() * 1000);
79+
80+
} else if (format.equals(DURATION_MILLIS_FORMAT)) {
81+
82+
return dateTimeFormatter.formatTime(FORMAT_24_HOUR, num.toBigInteger().intValue());
83+
84+
} else if (format.equals(HEX_FORMAT)) {
85+
return num.toBigInteger().toString(16);
86+
} else if (format.equals(COMMAS_FORMAT)) {
87+
NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault());
88+
nf.setMinimumFractionDigits(0);
89+
nf.setMaximumFractionDigits(2);
90+
return nf.format(num);
91+
92+
} else if (format.equals(BINARY_FORMAT)) {
93+
BigInteger integerPart = num.toBigInteger(); // 42
94+
return integerPart.toString(2);
95+
}
96+
return num.toString();
97+
}
98+
99+
@Strict
100+
public static String toString(double num, String format) {
101+
return toString(BigDecimal.valueOf(num), format);
102+
}
103+
104+
@Strict
105+
public static String toString(int num, String format) {
106+
return toString(BigDecimal.valueOf(num), format);
107+
}
108+
109+
@Strict
110+
public static String toString(String str, String format) {
111+
try {
112+
BigDecimal bd = new BigDecimal(str);
113+
return toString(bd, format);
114+
} catch (Exception e) {
115+
return null;
116+
}
117+
}
118+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.function.udf;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
import java.math.BigDecimal;
11+
import java.util.Locale;
12+
import org.junit.jupiter.api.Test;
13+
14+
public class ToStringFunctionTest {
15+
16+
private final ToStringFunction function = new ToStringFunction();
17+
18+
@Test
19+
void testBigDecimalToStringDurationFormat() {
20+
BigDecimal num = new BigDecimal("3661"); // 1 hour 1 minute 1 second
21+
String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
22+
assertEquals("01:01:01", result);
23+
}
24+
25+
@Test
26+
void testBigDecimalToStringHexFormat() {
27+
BigDecimal num = new BigDecimal("255");
28+
String result = ToStringFunction.toString(num, ToStringFunction.HEX_FORMAT);
29+
assertEquals("ff", result);
30+
}
31+
32+
@Test
33+
void testBigDecimalToStringCommasFormat() {
34+
Locale.setDefault(Locale.US); // Ensure predictable comma placement
35+
BigDecimal num = new BigDecimal("1234567.891");
36+
String result = ToStringFunction.toString(num, ToStringFunction.COMMAS_FORMAT);
37+
assertTrue(result.contains(","));
38+
}
39+
40+
@Test
41+
void testBigDecimalToStringBinaryFormat() {
42+
BigDecimal num = new BigDecimal("10");
43+
String result = ToStringFunction.toString(num, ToStringFunction.BINARY_FORMAT);
44+
assertEquals("1010", result);
45+
}
46+
47+
@Test
48+
void testBigDecimalToStringDefault() {
49+
BigDecimal num = new BigDecimal("123.45");
50+
assertEquals("123.45", ToStringFunction.toString(num, "unknown"));
51+
}
52+
53+
@Test
54+
void testDoubleToStringDurationFormat() {
55+
double num = 3661.4;
56+
String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
57+
assertEquals("01:01:01", result);
58+
}
59+
60+
@Test
61+
void testDoubleToStringHexFormat() {
62+
double num = 10.5;
63+
String result = ToStringFunction.toString(num, ToStringFunction.HEX_FORMAT);
64+
assertTrue(result.equals("a"));
65+
}
66+
67+
@Test
68+
void testDoubleToStringCommasFormat() {
69+
Locale.setDefault(Locale.US);
70+
double num = 12345.678;
71+
String result = ToStringFunction.toString(num, ToStringFunction.COMMAS_FORMAT);
72+
assertTrue(result.contains(","));
73+
}
74+
75+
@Test
76+
void testDoubleToStringBinaryFormat() {
77+
double num = 10.0;
78+
String result = ToStringFunction.toString(num, ToStringFunction.BINARY_FORMAT);
79+
assertNotNull(result);
80+
assertFalse(result.isEmpty());
81+
}
82+
83+
@Test
84+
void testDoubleToStringDefault() {
85+
assertEquals("10.5", ToStringFunction.toString(10.5, "unknown"));
86+
}
87+
88+
@Test
89+
void testIntToStringDurationFormat() {
90+
int num = 3661;
91+
String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
92+
assertEquals("01:01:01", result);
93+
}
94+
95+
@Test
96+
void testIntToStringHexFormat() {
97+
assertEquals("ff", ToStringFunction.toString(255, ToStringFunction.HEX_FORMAT));
98+
}
99+
100+
@Test
101+
void testIntToStringCommasFormat() {
102+
Locale.setDefault(Locale.US);
103+
String result = ToStringFunction.toString(1234567, ToStringFunction.COMMAS_FORMAT);
104+
assertTrue(result.contains(","));
105+
}
106+
107+
@Test
108+
void testIntToStringBinaryFormat() {
109+
assertEquals("1010", ToStringFunction.toString(10, ToStringFunction.BINARY_FORMAT));
110+
}
111+
112+
@Test
113+
void testIntToStringDefault() {
114+
assertEquals("123", ToStringFunction.toString(123, "unknown"));
115+
}
116+
117+
@Test
118+
void testStringNumericToStringIntFormat() {
119+
String result = ToStringFunction.toString("42", ToStringFunction.HEX_FORMAT);
120+
assertEquals("2a", result);
121+
}
122+
123+
@Test
124+
void testStringNumericToStringDoubleFormat() {
125+
String result = ToStringFunction.toString("42.5", ToStringFunction.COMMAS_FORMAT);
126+
assertTrue(result.contains("42"));
127+
}
128+
129+
@Test
130+
void testStringLargeNumberAsDouble() {
131+
String largeNum = "1234567890123";
132+
String result = ToStringFunction.toString(largeNum, ToStringFunction.BINARY_FORMAT);
133+
assertNotNull(result);
134+
}
135+
}

0 commit comments

Comments
 (0)