Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0ed23de
PPL tostring() implementation issue #4492
asifabashar Oct 16, 2025
5a5b778
removed sql changes
asifabashar Oct 16, 2025
9d71a95
doc changes
asifabashar Oct 16, 2025
be2c2e2
docs changes
asifabashar Oct 16, 2025
fc763a4
reverted string doc changes
asifabashar Oct 16, 2025
a04eb14
removed extra word
asifabashar Oct 16, 2025
590a8e6
added any type
asifabashar Oct 16, 2025
6938e2c
doc formatting fixes
asifabashar Oct 16, 2025
314fccd
description for boolean example
asifabashar Oct 16, 2025
0ee17b9
added format_time call from calcite , added duration_millis as splunk…
asifabashar Oct 17, 2025
6e24aa3
added doc update to specifically set 2nd argument as optional
asifabashar Oct 17, 2025
454cfc8
mentioned as value instead of number specifically
asifabashar Oct 17, 2025
b221e8c
fixed wrong bullet point
asifabashar Oct 17, 2025
a0a89c4
issue #4514 tonumber function as part of roadmap #4287
asifabashar Oct 20, 2025
ea9ba0f
added more unit tests
asifabashar Oct 20, 2025
370b9bc
Update docs/user/ppl/functions/conversion.rst
asifabashar Oct 22, 2025
f070684
fix per recommendation
asifabashar Oct 22, 2025
a2accf5
updated recommended changes
asifabashar Oct 22, 2025
3adda5d
updated recommended changes
asifabashar Oct 22, 2025
17b1d86
updated recommended changes
asifabashar Oct 22, 2025
eca7d13
updated recommended changes
asifabashar Oct 22, 2025
e780c15
updated recommended changes
asifabashar Oct 22, 2025
e3884b0
updated recommended changes
asifabashar Oct 22, 2025
042d3e8
spotless apply
asifabashar Oct 22, 2025
38ce420
merge conflict fix
asifabashar Oct 28, 2025
58d8e1b
merge conflict fix
asifabashar Oct 28, 2025
34f8635
added recommended changes
asifabashar Nov 2, 2025
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 @@ -104,6 +104,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public enum BuiltinFunctionName {

/** Text Functions. */
TOSTRING(FunctionName.of("tostring")),
TONUMBER(FunctionName.of("tonumber")),

/** IP Functions. */
CIDRMATCH(FunctionName.of("cidrmatch")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.condition.EarliestFunction;
import org.opensearch.sql.expression.function.udf.condition.EnhancedCoalesceFunction;
import org.opensearch.sql.expression.function.udf.condition.LatestFunction;
Expand Down Expand Up @@ -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 TONUMBER = new ToNumberFunction().toUDF("TONUMBER");
public static final SqlOperator WIDTH_BUCKET =
new org.opensearch.sql.expression.function.udf.binning.WidthBucketFunction()
.toUDF("WIDTH_BUCKET");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.TONUMBER;
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;
Expand Down Expand Up @@ -944,6 +945,7 @@ void populate() {
registerOperator(WEEKOFYEAR, PPLBuiltinOperators.WEEK);

registerOperator(INTERNAL_PATTERN_PARSER, PPLBuiltinOperators.PATTERN_PARSER);
registerOperator(TONUMBER, PPLBuiltinOperators.TONUMBER);

// Register MVJOIN to use Calcite's ARRAY_JOIN
register(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function.udf;

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.runtime.SqlFunctions;
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;

/**
* A custom implementation of number/boolean to string .
*
* <p>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 ToNumberFunction extends ImplementorUDF {
public ToNumberFunction() {
super(
new org.opensearch.sql.expression.function.udf.ToNumberFunction.ToNumberImplementor(),
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 format24hour = "%H:%M:%S"; // 24-hour format

@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<Expression> 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 must be between 2 and 36");
}

if (numStr.contains(".")) {

boolean isNegative = numStr.startsWith("-");
if (isNegative) {
numStr = numStr.substring(1);
}

// Split integer and fractional parts
String[] parts = numStr.split("\\.");
String intPart = parts[0];
String fracPart = parts.length > 1 ? parts[1] : "";

// Convert integer part
double intValue = 0;
for (char c : intPart.toCharArray()) {
int digit = Character.digit(c, base);
if (digit < 0) throw new IllegalArgumentException("Invalid digit: " + c);
intValue = intValue * base + digit;
}

// Convert fractional part
double fracValue = 0;
if (base == 10) {
double divisor = base;
for (char c : fracPart.toCharArray()) {
int digit = Character.digit(c, base);
if (digit < 0) throw new IllegalArgumentException("Invalid digit: " + c);
fracValue += (double) digit / divisor;
divisor *= base;
}
}

double result = intValue + fracValue;
result = isNegative ? -result : result;
if (base == 10) {
return result;
}
else {
return (long) result;
}
} else {
return Long.parseLong(numStr, base);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* 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.1", 2));
assertEquals(1L, ToNumberFunction.toNumber("1.1", 2));
assertEquals(3L, ToNumberFunction.toNumber("11.11", 2));
}

@Test
void testToNumberWithDecimalBase16() {
assertEquals(255L, ToNumberFunction.toNumber("FF.8", 16));
assertEquals(16L, ToNumberFunction.toNumber("10.4", 16));
assertEquals(171L, ToNumberFunction.toNumber("AB.B", 16));
}

@Test
void testToNumberWithNegativeDecimal() {
assertEquals(-2L, ToNumberFunction.toNumber("-10.1", 2));
assertEquals(-255L, ToNumberFunction.toNumber("-FF.8", 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.1", 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() {
assertThrows(
IllegalArgumentException.class,
() -> {
ToNumberFunction.toNumber("12A", 10);
});

assertThrows(
IllegalArgumentException.class,
() -> {
ToNumberFunction.toNumber("102", 2);
});

assertThrows(
IllegalArgumentException.class,
() -> {
ToNumberFunction.toNumber("189", 8);
});

assertThrows(
IllegalArgumentException.class,
() -> {
ToNumberFunction.toNumber("GHI", 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));
}
}
Loading
Loading