diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java b/core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java
index 12a9a297542..e2bdc2f02e3 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java
@@ -112,6 +112,12 @@ private PPLOperandTypes() {}
SqlTypeFamily.INTEGER,
SqlTypeFamily.INTEGER));
+ public static final UDFOperandMetadata NUMERIC_STRING_OR_STRING_STRING =
+ UDFOperandMetadata.wrap(
+ (CompositeOperandTypeChecker)
+ (OperandTypes.family(SqlTypeFamily.NUMERIC, SqlTypeFamily.STRING))
+ .or(OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING)));
+
public static final UDFOperandMetadata NUMERIC_NUMERIC_OPTIONAL_NUMERIC =
UDFOperandMetadata.wrap(
(CompositeOperandTypeChecker)
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 4e1aadfc847..e7bf337d24d 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
@@ -67,6 +67,7 @@
import org.opensearch.sql.expression.function.udf.RexExtractMultiFunction;
import org.opensearch.sql.expression.function.udf.RexOffsetFunction;
import org.opensearch.sql.expression.function.udf.SpanFunction;
+import org.opensearch.sql.expression.function.udf.ToStringFunction;
import org.opensearch.sql.expression.function.udf.condition.EarliestFunction;
import org.opensearch.sql.expression.function.udf.condition.EnhancedCoalesceFunction;
import org.opensearch.sql.expression.function.udf.condition.LatestFunction;
@@ -413,6 +414,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
RELEVANCE_QUERY_FUNCTION_INSTANCE.toUDF("multi_match", false);
public static final SqlOperator NUMBER_TO_STRING =
new NumberToStringFunction().toUDF("NUMBER_TO_STRING");
+ public static final SqlOperator TOSTRING = new ToStringFunction().toUDF("TOSTRING");
public static final SqlOperator WIDTH_BUCKET =
new org.opensearch.sql.expression.function.udf.binning.WidthBucketFunction()
.toUDF("WIDTH_BUCKET");
diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java
index b58253f9fbe..d7ae9b936f9 100644
--- a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java
+++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java
@@ -211,6 +211,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIMESTAMPDIFF;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIME_FORMAT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TIME_TO_SEC;
+import static org.opensearch.sql.expression.function.BuiltinFunctionName.TOSTRING;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TO_DAYS;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TO_SECONDS;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.TRANSFORM;
@@ -944,6 +945,13 @@ void populate() {
registerOperator(WEEKOFYEAR, PPLBuiltinOperators.WEEK);
registerOperator(INTERNAL_PATTERN_PARSER, PPLBuiltinOperators.PATTERN_PARSER);
+ registerOperator(TOSTRING, PPLBuiltinOperators.TOSTRING);
+ register(
+ TOSTRING,
+ (FunctionImp1)
+ (builder, source) ->
+ builder.makeCast(TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR, true), source),
+ PPLTypeChecker.family(SqlTypeFamily.ANY));
// Register MVJOIN to use Calcite's ARRAY_JOIN
register(
@@ -1112,6 +1120,7 @@ void populate() {
SqlTypeFamily.INTEGER,
SqlTypeFamily.INTEGER)),
false));
+
register(
LOG,
(FunctionImp2)
diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/ToStringFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/ToStringFunction.java
new file mode 100644
index 00000000000..e6e8dd01df0
--- /dev/null
+++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/ToStringFunction.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.expression.function.udf;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.NumberFormat;
+import java.util.List;
+import java.util.Locale;
+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.function.Strict;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.runtime.SqlFunctions;
+import org.apache.calcite.sql.*;
+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/boolean to string .
+ *
+ *
This operator is necessary because tostring has following requirements "binary" Converts a
+ * number to a binary value. "hex" Converts the number to a hexadecimal value. "commas" Formats the
+ * number with commas. If the number includes a decimal, the function rounds the number to nearest
+ * two decimal places. "duration" Converts the value in seconds to the readable time format
+ * HH:MM:SS. if not format parameter provided, then consider value as boolean
+ */
+public class ToStringFunction extends ImplementorUDF {
+ public ToStringFunction() {
+ super(
+ new org.opensearch.sql.expression.function.udf.ToStringFunction.ToStringImplementor(),
+ NullPolicy.ANY);
+ }
+
+ public static final String DURATION_FORMAT = "duration";
+ public static final String DURATION_MILLIS_FORMAT = "duration_millis";
+ public static final String HEX_FORMAT = "hex";
+ public static final String COMMAS_FORMAT = "commas";
+ public static final String BINARY_FORMAT = "binary";
+ public static final SqlFunctions.DateFormatFunction dateTimeFormatter =
+ new SqlFunctions.DateFormatFunction();
+ public static final String FORMAT_24_HOUR = "%H:%M:%S";
+
+ @Override
+ public SqlReturnTypeInference getReturnTypeInference() {
+ return PPLReturnTypes.STRING_FORCE_NULLABLE;
+ }
+
+ @Override
+ public UDFOperandMetadata getOperandMetadata() {
+ return PPLOperandTypes.NUMERIC_STRING_OR_STRING_STRING;
+ }
+
+ public static class ToStringImplementor implements NotNullImplementor {
+
+ @Override
+ public Expression implement(
+ RexToLixTranslator translator, RexCall call, List translatedOperands) {
+ Expression fieldValue = translatedOperands.get(0);
+ Expression format = translatedOperands.get(1);
+ return Expressions.call(ToStringFunction.class, "toString", fieldValue, format);
+ }
+ }
+
+ @Strict
+ public static String toString(BigDecimal num, String format) {
+ if (format.equals(DURATION_FORMAT)) {
+
+ return dateTimeFormatter.formatTime(FORMAT_24_HOUR, num.toBigInteger().intValue() * 1000);
+
+ } else if (format.equals(DURATION_MILLIS_FORMAT)) {
+
+ return dateTimeFormatter.formatTime(FORMAT_24_HOUR, num.toBigInteger().intValue());
+
+ } else if (format.equals(HEX_FORMAT)) {
+ return num.toBigInteger().toString(16);
+ } else if (format.equals(COMMAS_FORMAT)) {
+ NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault());
+ nf.setMinimumFractionDigits(0);
+ nf.setMaximumFractionDigits(2);
+ return nf.format(num);
+
+ } else if (format.equals(BINARY_FORMAT)) {
+ BigInteger integerPart = num.toBigInteger(); // 42
+ return integerPart.toString(2);
+ }
+ return num.toString();
+ }
+
+ @Strict
+ public static String toString(double num, String format) {
+ return toString(BigDecimal.valueOf(num), format);
+ }
+
+ @Strict
+ public static String toString(int num, String format) {
+ return toString(BigDecimal.valueOf(num), format);
+ }
+
+ @Strict
+ public static String toString(String str, String format) {
+ try {
+ BigDecimal bd = new BigDecimal(str);
+ return toString(bd, format);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/core/src/test/java/org/opensearch/sql/expression/function/udf/ToStringFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/function/udf/ToStringFunctionTest.java
new file mode 100644
index 00000000000..e29d6c10d19
--- /dev/null
+++ b/core/src/test/java/org/opensearch/sql/expression/function/udf/ToStringFunctionTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.expression.function.udf;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import org.junit.jupiter.api.Test;
+
+public class ToStringFunctionTest {
+
+ private final ToStringFunction function = new ToStringFunction();
+
+ @Test
+ void testBigDecimalToStringDurationFormat() {
+ BigDecimal num = new BigDecimal("3661"); // 1 hour 1 minute 1 second
+ String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
+ assertEquals("01:01:01", result);
+ }
+
+ @Test
+ void testBigDecimalToStringHexFormat() {
+ BigDecimal num = new BigDecimal("255");
+ String result = ToStringFunction.toString(num, ToStringFunction.HEX_FORMAT);
+ assertEquals("ff", result);
+ }
+
+ @Test
+ void testBigDecimalToStringCommasFormat() {
+ Locale.setDefault(Locale.US); // Ensure predictable comma placement
+ BigDecimal num = new BigDecimal("1234567.891");
+ String result = ToStringFunction.toString(num, ToStringFunction.COMMAS_FORMAT);
+ assertTrue(result.contains(","));
+ }
+
+ @Test
+ void testBigDecimalToStringBinaryFormat() {
+ BigDecimal num = new BigDecimal("10");
+ String result = ToStringFunction.toString(num, ToStringFunction.BINARY_FORMAT);
+ assertEquals("1010", result);
+ }
+
+ @Test
+ void testBigDecimalToStringDefault() {
+ BigDecimal num = new BigDecimal("123.45");
+ assertEquals("123.45", ToStringFunction.toString(num, "unknown"));
+ }
+
+ @Test
+ void testDoubleToStringDurationFormat() {
+ double num = 3661.4;
+ String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
+ assertEquals("01:01:01", result);
+ }
+
+ @Test
+ void testDoubleToStringHexFormat() {
+ double num = 10.5;
+ String result = ToStringFunction.toString(num, ToStringFunction.HEX_FORMAT);
+ assertTrue(result.equals("a"));
+ }
+
+ @Test
+ void testDoubleToStringCommasFormat() {
+ Locale.setDefault(Locale.US);
+ double num = 12345.678;
+ String result = ToStringFunction.toString(num, ToStringFunction.COMMAS_FORMAT);
+ assertTrue(result.contains(","));
+ }
+
+ @Test
+ void testDoubleToStringBinaryFormat() {
+ double num = 10.0;
+ String result = ToStringFunction.toString(num, ToStringFunction.BINARY_FORMAT);
+ assertNotNull(result);
+ assertFalse(result.isEmpty());
+ }
+
+ @Test
+ void testDoubleToStringDefault() {
+ assertEquals("10.5", ToStringFunction.toString(10.5, "unknown"));
+ }
+
+ @Test
+ void testIntToStringDurationFormat() {
+ int num = 3661;
+ String result = ToStringFunction.toString(num, ToStringFunction.DURATION_FORMAT);
+ assertEquals("01:01:01", result);
+ }
+
+ @Test
+ void testIntToStringHexFormat() {
+ assertEquals("ff", ToStringFunction.toString(255, ToStringFunction.HEX_FORMAT));
+ }
+
+ @Test
+ void testIntToStringCommasFormat() {
+ Locale.setDefault(Locale.US);
+ String result = ToStringFunction.toString(1234567, ToStringFunction.COMMAS_FORMAT);
+ assertTrue(result.contains(","));
+ }
+
+ @Test
+ void testIntToStringBinaryFormat() {
+ assertEquals("1010", ToStringFunction.toString(10, ToStringFunction.BINARY_FORMAT));
+ }
+
+ @Test
+ void testIntToStringDefault() {
+ assertEquals("123", ToStringFunction.toString(123, "unknown"));
+ }
+
+ @Test
+ void testStringNumericToStringIntFormat() {
+ String result = ToStringFunction.toString("42", ToStringFunction.HEX_FORMAT);
+ assertEquals("2a", result);
+ }
+
+ @Test
+ void testStringNumericToStringDoubleFormat() {
+ String result = ToStringFunction.toString("42.5", ToStringFunction.COMMAS_FORMAT);
+ assertTrue(result.contains("42"));
+ }
+
+ @Test
+ void testStringLargeNumberAsDouble() {
+ String largeNum = "1234567890123";
+ String result = ToStringFunction.toString(largeNum, ToStringFunction.BINARY_FORMAT);
+ assertNotNull(result);
+ }
+}
diff --git a/docs/user/ppl/functions/conversion.rst b/docs/user/ppl/functions/conversion.rst
index 849d2334e41..82d760cc3ce 100644
--- a/docs/user/ppl/functions/conversion.rst
+++ b/docs/user/ppl/functions/conversion.rst
@@ -117,3 +117,87 @@ Use string in comparison operator example ::
| True | False | True | False | True | True | null |
+------+-------+------+-------+------+------+------+
+
+TOSTRING
+-----------
+
+Description
+>>>>>>>>>>>
+The following usage options are available, depending on the parameter types and the number of parameters.
+
+Usage with format type: tostring(ANY, [format]): Converts the value in first argument to provided format type string in second argument. If second argument is not provided, then it converts to default string representation.
+Return type: string
+
+Usage for boolean parameter without format type tostring(boolean): Converts the string to 'TRUE' or 'FALSE'.
+Return type: string
+
+You can use this function with the eval commands and as part of eval expressions. If first argument can be any valid type , second argument is optional and if provided , it needs to be format name to convert to where first argument contains only numbers. If first argument is boolean, then second argument is not used even if its provided.
+
+Format types:
+
+a) "binary" Converts a number to a binary value.
+b) "hex" Converts the number to a hexadecimal value.
+c) "commas" Formats the number with commas. If the number includes a decimal, the function rounds the number to nearest two decimal places.
+d) "duration" Converts the value in seconds to the readable time format HH:MM:SS.
+e) "duration_millis" Converts the value in milliseconds to the readable time format HH:MM:SS.
+
+The format argument is optional and is only used when the value argument is a number. The tostring function supports the following formats.
+
+Basic examples:
+
+You can use this function to convert a number to a string of its binary representation.
+Example::
+city, city.name, city.location.latitude
+ os> source=accounts | where firstname = "Amber" | eval balance_binary = tostring(balance, "binary") | fields firstname, balance_binary, balance
+ fetched rows / total rows = 1/1
+ +-----------+------------------+---------+
+ | firstname | balance_binary | balance |
+ |-----------+------------------+---------|
+ | Amber | 1001100100111001 | 39225 |
+ +-----------+------------------+---------+
+
+
+You can use this function to convert a number to a string of its hex representation.
+Example::
+
+ os> source=accounts | where firstname = "Amber" | eval balance_hex = tostring(balance, "hex") | fields firstname, balance_hex, balance
+ fetched rows / total rows = 1/1
+ +-----------+-------------+---------+
+ | firstname | balance_hex | balance |
+ |-----------+-------------+---------|
+ | Amber | 9939 | 39225 |
+ +-----------+-------------+---------+
+
+The following example formats the column totalSales to display values with commas.
+Example::
+
+ os> source=accounts | where firstname = "Amber" | eval balance_commas = tostring(balance, "commas") | fields firstname, balance_commas, balance
+ fetched rows / total rows = 1/1
+ +-----------+----------------+---------+
+ | firstname | balance_commas | balance |
+ |-----------+----------------+---------|
+ | Amber | 39,225 | 39225 |
+ +-----------+----------------+---------+
+
+The following example converts number of seconds to HH:MM:SS format representing hours, minutes and seconds.
+Example::
+
+ os> source=accounts | where firstname = "Amber" | eval duration = tostring(6500, "duration") | fields firstname, duration
+ fetched rows / total rows = 1/1
+ +-----------+----------+
+ | firstname | duration |
+ |-----------+----------|
+ | Amber | 01:48:20 |
+ +-----------+----------+
+
+The following example for converts boolean parameter to string.
+Example::
+
+ os> source=accounts | where firstname = "Amber"| eval `boolean_str` = tostring(1=1)| fields `boolean_str`
+ fetched rows / total rows = 1/1
+ +-------------+
+ | boolean_str |
+ |-------------|
+ | TRUE |
+ +-------------+
+
diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
index ba1e4960bb2..e528bb553ab 100644
--- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
@@ -392,6 +392,7 @@ STRFTIME: 'STRFTIME';
// TEXT FUNCTIONS
SUBSTR: 'SUBSTR';
SUBSTRING: 'SUBSTRING';
+TOSTRING: 'TOSTRING';
LTRIM: 'LTRIM';
RTRIM: 'RTRIM';
TRIM: 'TRIM';
diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4
index bb872dfc25e..9e9679c159a 100644
--- a/ppl/src/main/antlr/OpenSearchPPLParser.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4
@@ -845,11 +845,13 @@ evalFunctionCall
: evalFunctionName LT_PRTHS functionArgs RT_PRTHS
;
+
// cast function
dataTypeFunctionCall
: CAST LT_PRTHS logicalExpression AS convertedDataType RT_PRTHS
;
+
convertedDataType
: typeName = DATE
| typeName = TIME
@@ -1214,6 +1216,7 @@ systemFunctionName
textFunctionName
: SUBSTR
| SUBSTRING
+ | TOSTRING
| TRIM
| LTRIM
| RTRIM
@@ -1432,6 +1435,7 @@ searchableKeyWord
| USING
| VALUE
| CAST
+ | TOSTRING
| GET_FORMAT
| EXTRACT
| INTERVAL
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLStringFunctionTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLStringFunctionTest.java
index 1e97052dea0..c67ba63a917 100644
--- a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLStringFunctionTest.java
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLStringFunctionTest.java
@@ -46,6 +46,239 @@ public void testLower() {
verifyPPLToSparkSQL(root, expectedSparkSql);
}
+ // This test evalutes tostring where it gets converted to cast call
+
+ @Test
+ public void testToStringFormatNotSpecified() {
+ String ppl =
+ "source=EMP | eval string_value = tostring(MGR) | eval cast_value = cast(MGR as string)|"
+ + " fields string_value, cast_value";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalProject(string_value=[CAST($3):VARCHAR], cast_value=[SAFE_CAST($3)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult =
+ "string_value=7902; cast_value=7902\n"
+ + "string_value=7698; cast_value=7698\n"
+ + "string_value=7698; cast_value=7698\n"
+ + "string_value=7839; cast_value=7839\n"
+ + "string_value=7698; cast_value=7698\n"
+ + "string_value=7839; cast_value=7839\n"
+ + "string_value=7839; cast_value=7839\n"
+ + "string_value=7566; cast_value=7566\n"
+ + "string_value=null; cast_value=null\n"
+ + "string_value=7698; cast_value=7698\n"
+ + "string_value=7788; cast_value=7788\n"
+ + "string_value=7698; cast_value=7698\n"
+ + "string_value=7566; cast_value=7566\n"
+ + "string_value=7782; cast_value=7782\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT CAST(`MGR` AS STRING) `string_value`, SAFE_CAST(`MGR` AS STRING) `cast_value`\n"
+ + "FROM `scott`.`EMP`";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringBoolean() {
+ String ppl = "source=EMP | eval boolean_value = tostring(1==1) | fields boolean_value |head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(boolean_value=['TRUE':VARCHAR])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult = "boolean_value=TRUE\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql = "SELECT 'TRUE' `boolean_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringBin() {
+ String ppl =
+ "source=EMP | eval salary_binary = tostring(SAL, \"binary\") | fields ENAME,"
+ + " salary_binary, SAL";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalProject(ENAME=[$1], salary_binary=[TOSTRING($5, 'binary':VARCHAR)], SAL=[$5])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult =
+ "ENAME=SMITH; salary_binary=1100100000; SAL=800.00\n"
+ + "ENAME=ALLEN; salary_binary=11001000000; SAL=1600.00\n"
+ + "ENAME=WARD; salary_binary=10011100010; SAL=1250.00\n"
+ + "ENAME=JONES; salary_binary=101110011111; SAL=2975.00\n"
+ + "ENAME=MARTIN; salary_binary=10011100010; SAL=1250.00\n"
+ + "ENAME=BLAKE; salary_binary=101100100010; SAL=2850.00\n"
+ + "ENAME=CLARK; salary_binary=100110010010; SAL=2450.00\n"
+ + "ENAME=SCOTT; salary_binary=101110111000; SAL=3000.00\n"
+ + "ENAME=KING; salary_binary=1001110001000; SAL=5000.00\n"
+ + "ENAME=TURNER; salary_binary=10111011100; SAL=1500.00\n"
+ + "ENAME=ADAMS; salary_binary=10001001100; SAL=1100.00\n"
+ + "ENAME=JAMES; salary_binary=1110110110; SAL=950.00\n"
+ + "ENAME=FORD; salary_binary=101110111000; SAL=3000.00\n"
+ + "ENAME=MILLER; salary_binary=10100010100; SAL=1300.00\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`(`SAL`, 'binary') `salary_binary`, `SAL`\nFROM `scott`.`EMP`";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringHex() {
+ String ppl =
+ "source=EMP | eval salary_hex = tostring(SAL, \"hex\") | fields ENAME, salary_hex, SAL";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalProject(ENAME=[$1], salary_hex=[TOSTRING($5, 'hex':VARCHAR)], SAL=[$5])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult =
+ "ENAME=SMITH; salary_hex=320; SAL=800.00\n"
+ + "ENAME=ALLEN; salary_hex=640; SAL=1600.00\n"
+ + "ENAME=WARD; salary_hex=4e2; SAL=1250.00\n"
+ + "ENAME=JONES; salary_hex=b9f; SAL=2975.00\n"
+ + "ENAME=MARTIN; salary_hex=4e2; SAL=1250.00\n"
+ + "ENAME=BLAKE; salary_hex=b22; SAL=2850.00\n"
+ + "ENAME=CLARK; salary_hex=992; SAL=2450.00\n"
+ + "ENAME=SCOTT; salary_hex=bb8; SAL=3000.00\n"
+ + "ENAME=KING; salary_hex=1388; SAL=5000.00\n"
+ + "ENAME=TURNER; salary_hex=5dc; SAL=1500.00\n"
+ + "ENAME=ADAMS; salary_hex=44c; SAL=1100.00\n"
+ + "ENAME=JAMES; salary_hex=3b6; SAL=950.00\n"
+ + "ENAME=FORD; salary_hex=bb8; SAL=3000.00\n"
+ + "ENAME=MILLER; salary_hex=514; SAL=1300.00\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`(`SAL`, 'hex') `salary_hex`, `SAL`\nFROM `scott`.`EMP`";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringHexFromNumberAsString() {
+ String ppl =
+ "source=EMP | eval salary_hex = tostring(\"1600\", \"hex\") | fields ENAME, salary_hex|"
+ + " head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(ENAME=[$1], salary_hex=[TOSTRING('1600':VARCHAR, 'hex':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult = "ENAME=SMITH; salary_hex=640\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`('1600', 'hex') `salary_hex`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringCommaFromNumberAsString() {
+ String ppl =
+ "source=EMP | eval salary_comma = tostring(\"160040222\", \"commas\") | fields ENAME,"
+ + " salary_comma| head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(ENAME=[$1], salary_comma=[TOSTRING('160040222':VARCHAR,"
+ + " 'commas':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult = "ENAME=SMITH; salary_comma=160,040,222\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`('160040222', 'commas') `salary_comma`\n"
+ + "FROM `scott`.`EMP`\n"
+ + "LIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringBinaryFromNumberAsString() {
+ String ppl =
+ "source=EMP | eval salary_binary = tostring(\"160040222\", \"binary\") | fields ENAME,"
+ + " salary_binary| head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(ENAME=[$1], salary_binary=[TOSTRING('160040222':VARCHAR,"
+ + " 'binary':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult = "ENAME=SMITH; salary_binary=1001100010100000010100011110\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`('160040222', 'binary') `salary_binary`\n"
+ + "FROM `scott`.`EMP`\n"
+ + "LIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringCommas() {
+ String ppl =
+ "source=EMP | eval salary_commas = tostring(SAL, \"commas\") | fields ENAME,"
+ + " salary_commas, SAL";
+ RelNode root = getRelNode(ppl);
+
+ String expectedLogical =
+ "LogicalProject(ENAME=[$1], salary_commas=[TOSTRING($5, 'commas':VARCHAR)], SAL=[$5])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult =
+ "ENAME=SMITH; salary_commas=800; SAL=800.00\n"
+ + "ENAME=ALLEN; salary_commas=1,600; SAL=1600.00\n"
+ + "ENAME=WARD; salary_commas=1,250; SAL=1250.00\n"
+ + "ENAME=JONES; salary_commas=2,975; SAL=2975.00\n"
+ + "ENAME=MARTIN; salary_commas=1,250; SAL=1250.00\n"
+ + "ENAME=BLAKE; salary_commas=2,850; SAL=2850.00\n"
+ + "ENAME=CLARK; salary_commas=2,450; SAL=2450.00\n"
+ + "ENAME=SCOTT; salary_commas=3,000; SAL=3000.00\n"
+ + "ENAME=KING; salary_commas=5,000; SAL=5000.00\n"
+ + "ENAME=TURNER; salary_commas=1,500; SAL=1500.00\n"
+ + "ENAME=ADAMS; salary_commas=1,100; SAL=1100.00\n"
+ + "ENAME=JAMES; salary_commas=950; SAL=950.00\n"
+ + "ENAME=FORD; salary_commas=3,000; SAL=3000.00\n"
+ + "ENAME=MILLER; salary_commas=1,300; SAL=1300.00\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`(`SAL`, 'commas') `salary_commas`, `SAL`\nFROM `scott`.`EMP`";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testToStringDuration() {
+ String ppl =
+ "source=EMP | eval duration_commas = tostring(6500, \"duration\") | fields ENAME,"
+ + " duration_commas|HEAD 1";
+
+ RelNode root = getRelNode(ppl);
+
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(ENAME=[$1], duration_commas=[TOSTRING(6500, 'duration':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ String expectedResult = "ENAME=SMITH; duration_commas=01:48:20\n";
+ verifyLogical(root, expectedLogical);
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT `ENAME`, `TOSTRING`(6500, 'duration') `duration_commas`\n"
+ + "FROM `scott`.`EMP`\n"
+ + "LIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
@Test
public void testLike() {
String ppl = "source=EMP | where like(JOB, 'SALE%') | stats count() as cnt";