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 ceae1ca7b51..8ce2175b5d9 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
@@ -106,6 +106,13 @@ private PPLOperandTypes() {}
UDFOperandMetadata.wrap(
OperandTypes.family(
SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER));
+
+ public static final UDFOperandMetadata STRING_OR_STRING_INTEGER =
+ UDFOperandMetadata.wrap(
+ (CompositeOperandTypeChecker)
+ OperandTypes.family(SqlTypeFamily.CHARACTER)
+ .or(OperandTypes.family(SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)));
+
public static final UDFOperandMetadata STRING_STRING_INTEGER_INTEGER =
UDFOperandMetadata.wrap(
OperandTypes.family(
diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java
index 7d5decf136d..72be016ecc2 100644
--- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java
+++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java
@@ -160,6 +160,7 @@ public enum BuiltinFunctionName {
/** Text Functions. */
TOSTRING(FunctionName.of("tostring")),
+ TONUMBER(FunctionName.of("tonumber")),
/** IP Functions. */
CIDRMATCH(FunctionName.of("cidrmatch")),
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 ef5b727294c..4a35087d87a 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.ToNumberFunction;
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;
@@ -412,6 +413,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 TONUMBER = new ToNumberFunction().toUDF("TONUMBER");
public static final SqlOperator TOSTRING = new ToStringFunction().toUDF("TOSTRING");
public static final SqlOperator WIDTH_BUCKET =
new org.opensearch.sql.expression.function.udf.binning.WidthBucketFunction()
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 83aa8f26d9b..b91adb9cdeb 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
@@ -218,6 +218,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.TONUMBER;
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;
@@ -983,6 +984,7 @@ void populate() {
registerOperator(WEEKOFYEAR, PPLBuiltinOperators.WEEK);
registerOperator(INTERNAL_PATTERN_PARSER, PPLBuiltinOperators.PATTERN_PARSER);
+ registerOperator(TONUMBER, PPLBuiltinOperators.TONUMBER);
registerOperator(TOSTRING, PPLBuiltinOperators.TOSTRING);
register(
TOSTRING,
@@ -1200,7 +1202,6 @@ void populate() {
SqlTypeFamily.INTEGER,
SqlTypeFamily.INTEGER)),
false));
-
register(
LOG,
(FunctionImp2)
diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/ToNumberFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/ToNumberFunction.java
new file mode 100644
index 00000000000..bc0a6dffa8d
--- /dev/null
+++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/ToNumberFunction.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.expression.function.udf;
+
+import java.math.BigInteger;
+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.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.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlReturnTypeInference;
+import org.opensearch.sql.calcite.utils.PPLOperandTypes;
+import org.opensearch.sql.expression.function.ImplementorUDF;
+import org.opensearch.sql.expression.function.UDFOperandMetadata;
+
+/**
+ * The following usage options are available, depending on the parameter types and the number of
+ * parameters.
+ *
+ *
Usage: {@code tonumber(string, [base])} converts the value in the first argument. The
+ * second argument describes the base of the first argument. If the second argument is not provided,
+ * the value is converted using base 10.
+ *
+ *
Return type: Number
+ *
+ *
You can use this function with the eval commands and as part of eval expressions.
+ *
+ *
Base values can range from 2 to 36. The maximum value supported for base 10 is {@code +(2 −
+ * 2^-52) · 2^1023} and the minimum is {@code −(2 − 2^-52) · 2^1023}.
+ *
+ *
The maximum for other supported bases is {@code 2^63 − 1} (or {@code 7FFFFFFFFFFFFFFF}) and
+ * the minimum is {@code -2^63} (or {@code -7FFFFFFFFFFFFFFF}).
+ */
+public class ToNumberFunction extends ImplementorUDF {
+ public ToNumberFunction() {
+ super(
+ new org.opensearch.sql.expression.function.udf.ToNumberFunction.ToNumberImplementor(),
+ NullPolicy.ANY);
+ }
+
+ @Override
+ public SqlReturnTypeInference getReturnTypeInference() {
+
+ return ReturnTypes.DOUBLE_FORCE_NULLABLE;
+ }
+
+ @Override
+ public UDFOperandMetadata getOperandMetadata() {
+ return PPLOperandTypes.STRING_OR_STRING_INTEGER;
+ }
+
+ public static class ToNumberImplementor implements NotNullImplementor {
+
+ @Override
+ public Expression implement(
+ RexToLixTranslator translator, RexCall call, List translatedOperands) {
+ Expression fieldValue = translatedOperands.get(0);
+ int base = 10;
+ if (translatedOperands.size() > 1) {
+ Expression baseExpr = translatedOperands.get(1);
+ return Expressions.call(ToNumberFunction.class, "toNumber", fieldValue, baseExpr);
+ } else {
+ return Expressions.call(ToNumberFunction.class, "toNumber", fieldValue);
+ }
+ }
+ }
+
+ @Strict
+ public static Number toNumber(String numStr) {
+ return toNumber(numStr, 10);
+ }
+
+ @Strict
+ public static Number toNumber(String numStr, int base) {
+ if (base < 2 || base > 36) {
+ throw new IllegalArgumentException("Base has to be between 2 and 36.");
+ }
+ Number result = null;
+ try {
+ if (base == 10) {
+ if (numStr.contains(".")) {
+ result = Double.parseDouble(numStr);
+ } else {
+ result = Long.parseLong(numStr);
+ }
+ } else {
+ BigInteger bigInteger = new BigInteger(numStr, base);
+ result = bigInteger.longValue();
+ }
+ } catch (Exception e) {
+
+ }
+ return result;
+ }
+}
diff --git a/core/src/test/java/org/opensearch/sql/expression/function/udf/ToNumberFunctionTest.java b/core/src/test/java/org/opensearch/sql/expression/function/udf/ToNumberFunctionTest.java
new file mode 100644
index 00000000000..34ed102ec5b
--- /dev/null
+++ b/core/src/test/java/org/opensearch/sql/expression/function/udf/ToNumberFunctionTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.expression.function.udf;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.junit.jupiter.api.Test;
+import org.opensearch.sql.calcite.utils.PPLOperandTypes;
+
+public class ToNumberFunctionTest {
+
+ private final ToNumberFunction function = new ToNumberFunction();
+
+ @Test
+ void testGetReturnTypeInference() {
+ assertEquals(ReturnTypes.DOUBLE_FORCE_NULLABLE, function.getReturnTypeInference());
+ }
+
+ @Test
+ void testGetOperandMetadata() {
+ assertEquals(PPLOperandTypes.STRING_OR_STRING_INTEGER, function.getOperandMetadata());
+ }
+
+ @Test
+ void testToNumberWithDefaultBase() {
+ assertEquals(123L, ToNumberFunction.toNumber("123"));
+ assertEquals(0L, ToNumberFunction.toNumber("0"));
+ assertEquals(-456L, ToNumberFunction.toNumber("-456"));
+ assertEquals(123.45, ToNumberFunction.toNumber("123.45"));
+ assertEquals(-123.45, ToNumberFunction.toNumber("-123.45"));
+ assertEquals(0.5, ToNumberFunction.toNumber("0.5"));
+ assertEquals(-0.5, ToNumberFunction.toNumber("-0.5"));
+ }
+
+ @Test
+ void testToNumberWithBase10() {
+ assertEquals(123L, ToNumberFunction.toNumber("123", 10));
+ assertEquals(0L, ToNumberFunction.toNumber("0", 10));
+ assertEquals(-456L, ToNumberFunction.toNumber("-456", 10));
+ assertEquals(123.45, ToNumberFunction.toNumber("123.45", 10));
+ assertEquals(-123.45, ToNumberFunction.toNumber("-123.45", 10));
+ }
+
+ @Test
+ void testToNumberWithBase2() {
+ assertEquals(5L, ToNumberFunction.toNumber("101", 2));
+ assertEquals(0L, ToNumberFunction.toNumber("0", 2));
+ assertEquals(1L, ToNumberFunction.toNumber("1", 2));
+ assertEquals(7L, ToNumberFunction.toNumber("111", 2));
+ assertEquals(10L, ToNumberFunction.toNumber("1010", 2));
+ }
+
+ @Test
+ void testToNumberWithBase8() {
+ assertEquals(64L, ToNumberFunction.toNumber("100", 8));
+ assertEquals(8L, ToNumberFunction.toNumber("10", 8));
+ assertEquals(83L, ToNumberFunction.toNumber("123", 8));
+ assertEquals(511L, ToNumberFunction.toNumber("777", 8));
+ }
+
+ @Test
+ void testToNumberWithBase16() {
+ assertEquals(255L, ToNumberFunction.toNumber("FF", 16));
+ assertEquals(16L, ToNumberFunction.toNumber("10", 16));
+ assertEquals(171L, ToNumberFunction.toNumber("AB", 16));
+ assertEquals(291L, ToNumberFunction.toNumber("123", 16));
+ assertEquals(4095L, ToNumberFunction.toNumber("FFF", 16));
+ }
+
+ @Test
+ void testToNumberWithBase36() {
+ assertEquals(35L, ToNumberFunction.toNumber("Z", 36));
+ assertEquals(1295L, ToNumberFunction.toNumber("ZZ", 36));
+ assertEquals(46655L, ToNumberFunction.toNumber("ZZZ", 36));
+ }
+
+ @Test
+ void testToNumberWithDecimalBase2() {
+ assertEquals(2L, ToNumberFunction.toNumber("10", 2));
+ assertEquals(1L, ToNumberFunction.toNumber("1", 2));
+ assertEquals(3L, ToNumberFunction.toNumber("11", 2));
+ }
+
+ @Test
+ void testToNumberWithDecimalBase16() {
+ assertEquals(255L, ToNumberFunction.toNumber("FF", 16));
+ assertEquals(16L, ToNumberFunction.toNumber("10", 16));
+ assertEquals(171L, ToNumberFunction.toNumber("AB", 16));
+ }
+
+ @Test
+ void testToNumberWithNegativeDecimal() {
+ assertEquals(-2L, ToNumberFunction.toNumber("-10", 2));
+ assertEquals(-255L, ToNumberFunction.toNumber("-FF", 16));
+ assertEquals(-123.45, ToNumberFunction.toNumber("-123.45", 10));
+ }
+
+ @Test
+ void testToNumberWithEmptyFractionalPart() {
+ assertEquals(123.0, ToNumberFunction.toNumber("123.", 10));
+ assertEquals(255L, ToNumberFunction.toNumber("FF", 16));
+ assertEquals(5L, ToNumberFunction.toNumber("101", 2));
+ }
+
+ @Test
+ void testToNumberWithZeroIntegerPart() {
+ assertEquals(0.5, ToNumberFunction.toNumber("0.5", 10));
+ assertEquals(0L, ToNumberFunction.toNumber("0", 2));
+ }
+
+ @Test
+ void testToNumberInvalidBase() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ToNumberFunction.toNumber("123", 1);
+ });
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ToNumberFunction.toNumber("123", 37);
+ });
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ToNumberFunction.toNumber("123", 0);
+ });
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ToNumberFunction.toNumber("123", -1);
+ });
+ }
+
+ @Test
+ void testToNumberInvalidDigits() {
+ assertEquals(null, ToNumberFunction.toNumber("12A", 10));
+ assertEquals(null, ToNumberFunction.toNumber("102", 2));
+ assertEquals(null, ToNumberFunction.toNumber("101.101", 2));
+ assertEquals(null, ToNumberFunction.toNumber("189", 8));
+ assertEquals(null, ToNumberFunction.toNumber("GHI", 16));
+ assertEquals(null, ToNumberFunction.toNumber("FF.8", 16));
+ }
+
+ @Test
+ void testToNumberEdgeCases() {
+ assertEquals(0L, ToNumberFunction.toNumber("0", 2));
+ assertEquals(0L, ToNumberFunction.toNumber("0", 36));
+ assertEquals(0.0, ToNumberFunction.toNumber("0.0", 10));
+ assertEquals(0.0, ToNumberFunction.toNumber("0.000", 10));
+ }
+
+ @Test
+ void testToNumberLargeNumbers() {
+ assertEquals(
+ (long) Integer.MAX_VALUE, ToNumberFunction.toNumber(String.valueOf(Integer.MAX_VALUE), 10));
+ assertEquals(
+ (long) Integer.MIN_VALUE, ToNumberFunction.toNumber(String.valueOf(Integer.MIN_VALUE), 10));
+ }
+
+ @Test
+ void testToNumberCaseInsensitivity() {
+ assertEquals(255L, ToNumberFunction.toNumber("ff", 16));
+ assertEquals(255L, ToNumberFunction.toNumber("FF", 16));
+ assertEquals(255L, ToNumberFunction.toNumber("fF", 16));
+ assertEquals(171L, ToNumberFunction.toNumber("ab", 16));
+ assertEquals(171L, ToNumberFunction.toNumber("AB", 16));
+ }
+}
diff --git a/docs/user/ppl/functions/conversion.md b/docs/user/ppl/functions/conversion.md
index a33a93bbd69..9e3b1d1ed7b 100644
--- a/docs/user/ppl/functions/conversion.md
+++ b/docs/user/ppl/functions/conversion.md
@@ -269,4 +269,71 @@ fetched rows / total rows = 1/1
| TRUE |
+-------------+
```
-
\ No newline at end of file
+
+## TONUMBER
+
+### Description
+
+The following usage options are available, depending on the parameter
+types and the number of parameters.
+
+Usage: tonumber(string, \[base\]) converts the value in first argument.
+The second argument describe the base of first argument. If second
+argument is not provided, then it converts to base 10 number
+representation.
+
+Return type: Number
+
+You can use this function with the eval commands and as part of eval
+expressions. Base values can be between 2 and 36. The maximum value
+supported for base 10 is +(2-2\^-52)·2\^1023 and minimum is
+-(2-2\^-52)·2\^1023. The maximum for other supported bases is 2\^63-1
+(or 7FFFFFFFFFFFFFFF) and minimum is -2\^63 (or -7FFFFFFFFFFFFFFF). If
+the tonumber function cannot parse a field value to a number, the
+function returns NULL. You can use this function to convert a string
+representation of a binary number to return the corresponding number in
+base 10.
+
+Following example converts a string in binary to the number
+representation:
+
+ os> source=people | eval int_value = tonumber('010101',2) | fields int_value | head 1
+ fetched rows / total rows = 1/1
+ +-----------+
+ | int_value |
+ |-----------|
+ | 21.0 |
+ +-----------+
+
+Following example converts a string in hex to the number representation:
+
+ os> source=people | eval int_value = tonumber('FA34',16) | fields int_value | head 1
+ fetched rows / total rows = 1/1
+ +-----------+
+ | int_value |
+ |-----------|
+ | 64052.0 |
+ +-----------+
+
+Following example converts a string in decimal to the number
+representation:
+
+ os> source=people | eval int_value = tonumber('4598') | fields int_value | head 1
+ fetched rows / total rows = 1/1
+ +-----------+
+ | int_value |
+ |-----------|
+ | 4598.0 |
+ +-----------+
+
+Following example converts a string in decimal with fraction to the
+number representation:
+
+ os> source=people | eval double_value = tonumber('4598.678') | fields double_value | head 1
+ fetched rows / total rows = 1/1
+ +--------------+
+ | double_value |
+ |--------------|
+ | 4598.678 |
+ +--------------+
+
diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/ConversionFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/ConversionFunctionIT.java
new file mode 100644
index 00000000000..9a870f1e49e
--- /dev/null
+++ b/integ-test/src/test/java/org/opensearch/sql/ppl/ConversionFunctionIT.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.ppl;
+
+import static org.opensearch.sql.legacy.TestsConstants.*;
+import static org.opensearch.sql.util.MatcherUtils.*;
+
+import java.io.IOException;
+import org.json.JSONObject;
+import org.junit.Test;
+
+public class ConversionFunctionIT extends PPLIntegTestCase {
+ @Override
+ public void init() throws Exception {
+ loadIndex(Index.ACCOUNT);
+ }
+
+ @Test
+ public void testDecimal() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('4598.678') | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ verifyDataRows(actual, rows(4598.678));
+ }
+
+ @Test
+ public void testHex() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('FF12CA',16) | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ verifyDataRows(actual, rows(16716490));
+ }
+
+ @Test
+ public void testBinary() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('0110111',2) | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ verifyDataRows(actual, rows(55));
+ }
+
+ @Test
+ public void testOctal() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('20415442',8) | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ verifyDataRows(actual, rows(4332322));
+ }
+
+ @Test
+ public void testOctalWithUnsupportedValue() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('20415.442',8) | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ assertEquals(actual.getJSONArray("datarows").getJSONArray(0).get(0), null);
+ }
+
+ @Test
+ public void testBinaryWithUnsupportedValue() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('1010.11',2) | fields a",
+ TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ assertEquals(actual.getJSONArray("datarows").getJSONArray(0).get(0), null);
+ }
+
+ @Test
+ public void testHexWithUnsupportedValue() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s |head 1| eval a = tonumber('A.B',16) | fields a", TEST_INDEX_ACCOUNT));
+
+ verifySchema(actual, schema("a", "double"));
+
+ assertEquals(actual.getJSONArray("datarows").getJSONArray(0).get(0), null);
+ }
+}
diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
index 694aabf43ab..4d1ad777d7f 100644
--- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4
@@ -412,6 +412,7 @@ STRFTIME: 'STRFTIME';
// TEXT FUNCTIONS
SUBSTR: 'SUBSTR';
SUBSTRING: 'SUBSTRING';
+TONUMBER: 'TONUMBER';
TOSTRING: 'TOSTRING';
LTRIM: 'LTRIM';
RTRIM: 'RTRIM';
diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4
index 1cc33cd7f5d..49891cad475 100644
--- a/ppl/src/main/antlr/OpenSearchPPLParser.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4
@@ -1321,6 +1321,7 @@ textFunctionName
| LOCATE
| REPLACE
| REVERSE
+ | TONUMBER
| REGEXP_REPLACE
;
@@ -1564,6 +1565,7 @@ searchableKeyWord
| USING
| VALUE
| CAST
+ | TONUMBER
| TOSTRING
| GET_FORMAT
| EXTRACT
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLToNumberFunctionTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLToNumberFunctionTest.java
new file mode 100644
index 00000000000..f92e94519e2
--- /dev/null
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLToNumberFunctionTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.ppl.calcite;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.test.CalciteAssert;
+import org.junit.Test;
+
+public class CalcitePPLToNumberFunctionTest extends CalcitePPLAbstractTest {
+
+ public CalcitePPLToNumberFunctionTest() {
+ super(CalciteAssert.SchemaSpec.SCOTT_WITH_TEMPORAL);
+ }
+
+ @Test
+ public void testNumberBinary() {
+ String ppl = "source=EMP | eval int_value = tonumber('010101',2) | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('010101':VARCHAR, 2)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=21.0\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('010101', 2) `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberBinaryUnsupportedResultNull() {
+ String ppl = "source=EMP | eval int_value = tonumber('010.101',2) | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('010.101':VARCHAR, 2)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=null\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('010.101', 2) `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHex() {
+ String ppl = "source=EMP | eval int_value = tonumber('FA34',16) | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('FA34':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=64052.0\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('FA34', 16) `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHexUnsupportedValuesResultNull() {
+ String ppl =
+ "source=EMP | eval double_value = tonumber('FA.34',16) | fields double_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(double_value=[TONUMBER('FA.34':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "double_value=null\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('FA.34', 16) `double_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHexMinLimit() {
+ String ppl =
+ "source=EMP | eval long_value = tonumber('-7FFFFFFFFFFFFFFF',16) | fields long_value|head"
+ + " 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(long_value=[TONUMBER('-7FFFFFFFFFFFFFFF':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "long_value=-9.223372036854776E18\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('-7FFFFFFFFFFFFFFF', 16) `long_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHexMaxLimit() {
+ String ppl =
+ "source=EMP | eval long_value = tonumber('7FFFFFFFFFFFFFFF',16) | fields long_value|head"
+ + " 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(long_value=[TONUMBER('7FFFFFFFFFFFFFFF':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "long_value=9.223372036854776E18\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('7FFFFFFFFFFFFFFF', 16) `long_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHexOverNegativeMaxLimit() {
+ String ppl =
+ "source=EMP | eval long_value = tonumber('-FFFFFFFFFFFFFFFF',16) | fields long_value|head"
+ + " 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(long_value=[TONUMBER('-FFFFFFFFFFFFFFFF':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "long_value=1.0\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('-FFFFFFFFFFFFFFFF', 16) `long_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberHexOverPositiveMaxLimit() {
+ String ppl =
+ "source=EMP | eval long_value = tonumber('FFFFFFFFFFFFFFFF',16) | fields long_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(long_value=[TONUMBER('FFFFFFFFFFFFFFFF':VARCHAR, 16)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "long_value=-1.0\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('FFFFFFFFFFFFFFFF', 16) `long_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumber() {
+ String ppl = "source=EMP | eval int_value = tonumber('4598') | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('4598':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=4598.0\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql = "SELECT TONUMBER('4598') `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberDecimal() {
+ String ppl = "source=EMP | eval int_value = tonumber('4598.54922') | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('4598.54922':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=4598.54922\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('4598.54922') `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+
+ @Test
+ public void testNumberUnsupportedResultNull() {
+ String ppl = "source=EMP | eval int_value = tonumber('4A598.54922') | fields int_value|head 1";
+ RelNode root = getRelNode(ppl);
+ String expectedLogical =
+ "LogicalSort(fetch=[1])\n"
+ + " LogicalProject(int_value=[TONUMBER('4A598.54922':VARCHAR)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ verifyLogical(root, expectedLogical);
+ String expectedResult = "int_value=null\n";
+ verifyResult(root, expectedResult);
+
+ String expectedSparkSql =
+ "SELECT TONUMBER('4A598.54922') `int_value`\nFROM `scott`.`EMP`\nLIMIT 1";
+ verifyPPLToSparkSQL(root, expectedSparkSql);
+ }
+}