Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1168a32
feat: Implement PPL convert command with 5 conversion functions
aaarone90 Dec 31, 2025
d5606bf
Fixing integration tests
aaarone90 Jan 10, 2026
67ddf4f
Fixing ymal files for IT explain tests
aaarone90 Jan 12, 2026
3cb1fa3
Fix cross-cluster IT failure
aaarone90 Jan 12, 2026
66a23d7
Making code more readable and removing unnecessary logic
aaarone90 Jan 12, 2026
b0781cd
Refactor: Extract BaseConversionUDF to eliminate duplication
aaarone90 Jan 14, 2026
6ff9c2a
Fixing CI failure and refactoring
aaarone90 Jan 15, 2026
2f24dba
trigger CI
aaarone90 Jan 15, 2026
6763d8f
Addressing CodeRabbit comments
aaarone90 Jan 15, 2026
4e89f18
Adding support for memk function
aaarone90 Jan 16, 2026
20f0b16
Fixing formatting
aaarone90 Jan 16, 2026
a5a672b
Updating documentation
aaarone90 Jan 22, 2026
1bc2c61
Refactoring code to avoid regestering none() as a convert function an…
aaarone90 Jan 23, 2026
a8020f8
refactor: Simplify Convert command using Let expressions
aaarone90 Jan 24, 2026
c77fdfb
Trigger CI
aaarone90 Jan 24, 2026
4803558
Refactoring code to use Template Method Design Pattern
aaarone90 Jan 26, 2026
a2db4e2
Updating documentation
aaarone90 Jan 26, 2026
0daf0a5
Trigger CI
aaarone90 Jan 26, 2026
9f69d4d
Updating convert example with stats
aaarone90 Jan 27, 2026
8658396
Renaming unit test class, as ConversionUtil class was removed
aaarone90 Jan 27, 2026
bd13675
Fixing IT test case
aaarone90 Jan 27, 2026
af71278
Resolve merge conflict: Add testTransposeCommand alongside testConver…
aaarone90 Jan 27, 2026
78ec131
Add null and empty string tests for NumConvertFunction
aaarone90 Jan 27, 2026
4561ba8
Add null and empty string tests for RmunitConvertFunction
aaarone90 Jan 27, 2026
7f19738
Verify convert AS clause preserves original field
aaarone90 Jan 27, 2026
841413d
Adding edge test cases, as recommended by Coderabbit
aaarone90 Jan 27, 2026
638c1be
Trigger CI
aaarone90 Jan 27, 2026
7d5f959
Removing timeformat parameter for now, will add later
aaarone90 Jan 27, 2026
9f61b68
Trigger CI
aaarone90 Jan 27, 2026
8aa1c61
Re-trigger CI
aaarone90 Jan 27, 2026
4b4ca2e
fix merge conflicts
ritvibhatt Feb 19, 2026
3117082
fix cross cluster tests
ritvibhatt Feb 19, 2026
2733ce7
Merge branch 'main' into ppl-convert
ritvibhatt Feb 21, 2026
48312d0
fix merge conflict
ritvibhatt Feb 21, 2026
3293010
fix merge conflicts
ritvibhatt Feb 23, 2026
ac93662
use singleton for udf instance
ritvibhatt Feb 24, 2026
cbe577b
Merge branch 'main' into ppl-convert
ritvibhatt Feb 27, 2026
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 @@ -66,6 +66,7 @@
import org.opensearch.sql.ast.tree.Bin;
import org.opensearch.sql.ast.tree.Chart;
import org.opensearch.sql.ast.tree.CloseCursor;
import org.opensearch.sql.ast.tree.Convert;
import org.opensearch.sql.ast.tree.Dedupe;
import org.opensearch.sql.ast.tree.Eval;
import org.opensearch.sql.ast.tree.Expand;
Expand Down Expand Up @@ -534,6 +535,11 @@ public LogicalPlan visitFieldFormat(Eval node, AnalysisContext context) {
throw getOnlyForCalciteException("fieldformat");
}

@Override
public LogicalPlan visitConvert(Convert node, AnalysisContext context) {
throw getOnlyForCalciteException("convert");
}

@Override
public LogicalPlan visitAddTotals(AddTotals node, AnalysisContext context) {
throw getOnlyForCalciteException("addtotals");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.opensearch.sql.ast.tree.Bin;
import org.opensearch.sql.ast.tree.Chart;
import org.opensearch.sql.ast.tree.CloseCursor;
import org.opensearch.sql.ast.tree.Convert;
import org.opensearch.sql.ast.tree.Dedupe;
import org.opensearch.sql.ast.tree.Eval;
import org.opensearch.sql.ast.tree.Expand;
Expand Down Expand Up @@ -423,6 +424,10 @@ public T visitFillNull(FillNull fillNull, C context) {
return visitChildren(fillNull, context);
}

public T visitConvert(Convert node, C context) {
return visitChildren(node, context);
}

public T visitPatterns(Patterns patterns, C context) {
return visitChildren(patterns, context);
}
Expand Down
43 changes: 43 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/Convert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.ast.tree;

import com.google.common.collect.ImmutableList;
import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.ast.expression.Let;

/** AST node representing the Convert command. */
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
public class Convert extends UnresolvedPlan {
private final List<Let> conversions;
private UnresolvedPlan child;

@Override
public Convert attach(UnresolvedPlan child) {
this.child = child;
return this;
}

@Override
public List<UnresolvedPlan> getChild() {
return this.child == null ? ImmutableList.of() : ImmutableList.of(this.child);
}

@Override
public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
return nodeVisitor.visitConvert(this, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -111,6 +112,7 @@
import org.opensearch.sql.ast.tree.Bin;
import org.opensearch.sql.ast.tree.Chart;
import org.opensearch.sql.ast.tree.CloseCursor;
import org.opensearch.sql.ast.tree.Convert;
import org.opensearch.sql.ast.tree.Dedupe;
import org.opensearch.sql.ast.tree.Eval;
import org.opensearch.sql.ast.tree.Expand;
Expand Down Expand Up @@ -982,6 +984,117 @@ public RelNode visitEval(Eval node, CalcitePlanContext context) {
return context.relBuilder.peek();
}

@Override
public RelNode visitConvert(Convert node, CalcitePlanContext context) {
visitChildren(node, context);

if (node.getConversions() == null || node.getConversions().isEmpty()) {
return context.relBuilder.peek();
}

ConversionState state = new ConversionState();

for (Let conversion : node.getConversions()) {
processConversion(conversion, state, context);
}

return buildConversionProjection(state, context);
}

private static class ConversionState {
final Map<String, RexNode> replacements = new HashMap<>();
final List<Pair<String, RexNode>> additions = new ArrayList<>();
final Set<String> seenFields = new HashSet<>();
}

private void processConversion(
Let conversion, ConversionState state, CalcitePlanContext context) {
String target = conversion.getVar().getField().toString();
UnresolvedExpression expression = conversion.getExpression();

if (expression instanceof Field) {
processFieldCopyConversion(target, (Field) expression, state, context);
} else if (expression instanceof Function) {
processFunctionConversion(target, (Function) expression, state, context);
} else {
throw new SemanticCheckException("Convert command requires function call expressions");
}
}

private void processFieldCopyConversion(
String target, Field field, ConversionState state, CalcitePlanContext context) {
String source = field.getField().toString();

if (state.seenFields.contains(source)) {
throw new SemanticCheckException(
String.format("Field '%s' cannot be converted more than once", source));
}
state.seenFields.add(source);

if (!target.equals(source)) {
RexNode sourceField = context.relBuilder.field(source);
state.additions.add(Pair.of(target, context.relBuilder.alias(sourceField, target)));
}
}

private void processFunctionConversion(
String target, Function function, ConversionState state, CalcitePlanContext context) {
String functionName = function.getFuncName();
List<UnresolvedExpression> args = function.getFuncArgs();

if (args.size() != 1 || !(args.get(0) instanceof Field)) {
throw new SemanticCheckException("Convert function must have exactly one field argument");
}

String source = ((Field) args.get(0)).getField().toString();

if (state.seenFields.contains(source)) {
throw new SemanticCheckException(
String.format("Field '%s' cannot be converted more than once", source));
}
state.seenFields.add(source);

RexNode sourceField = context.relBuilder.field(source);
RexNode convertCall =
PPLFuncImpTable.INSTANCE.resolve(context.rexBuilder, functionName, sourceField);

if (!target.equals(source)) {
state.additions.add(Pair.of(target, context.relBuilder.alias(convertCall, target)));
} else {
state.replacements.put(source, context.relBuilder.alias(convertCall, source));
}
}

private RelNode buildConversionProjection(ConversionState state, CalcitePlanContext context) {
List<String> originalFields = context.relBuilder.peek().getRowType().getFieldNames();
List<RexNode> projectList = new ArrayList<>();

for (String fieldName : originalFields) {
projectList.add(
state.replacements.getOrDefault(fieldName, context.relBuilder.field(fieldName)));
}

Set<String> addedAsNames = new HashSet<>();
for (Pair<String, RexNode> addition : state.additions) {
String asName = addition.getLeft();

if (originalFields.contains(asName)) {
throw new SemanticCheckException(
String.format("AS name '%s' conflicts with existing field", asName));
}
if (addedAsNames.contains(asName)) {
throw new SemanticCheckException(
String.format("AS name '%s' is used multiple times in convert", asName));
}

addedAsNames.add(asName);
projectList.add(addition.getRight());
}

context.relBuilder.project(projectList);
return context.relBuilder.peek();
}

private void projectPlusOverriding(
List<RexNode> newFields, List<String> newNames, CalcitePlanContext context) {
List<String> originalFieldNames = context.relBuilder.peek().getRowType().getFieldNames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,18 @@ public enum BuiltinFunctionName {

INTERVAL(FunctionName.of("interval")),

/** PPL Convert Command Functions. */
AUTO(FunctionName.of("auto")),
NUM(FunctionName.of("num")),
CTIME(FunctionName.of("ctime")),
MKTIME(FunctionName.of("mktime")),
DUR2SEC(FunctionName.of("dur2sec")),
MEMK(FunctionName.of("memk")),
MSTIME(FunctionName.of("mstime")),
RMUNIT(FunctionName.of("rmunit")),
RMCOMMA(FunctionName.of("rmcomma")),
NONE(FunctionName.of("none")),

/** Data Type Convert Function. */
CAST_TO_STRING(FunctionName.of("cast_to_string")),
CAST_TO_BYTE(FunctionName.of("cast_to_byte")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@
import org.opensearch.sql.expression.function.jsonUDF.JsonFunctionImpl;
import org.opensearch.sql.expression.function.jsonUDF.JsonKeysFunctionImpl;
import org.opensearch.sql.expression.function.jsonUDF.JsonSetFunctionImpl;
import org.opensearch.sql.expression.function.udf.AutoConvertFunction;
import org.opensearch.sql.expression.function.udf.CryptographicFunction;
import org.opensearch.sql.expression.function.udf.MemkConvertFunction;
import org.opensearch.sql.expression.function.udf.NumConvertFunction;
import org.opensearch.sql.expression.function.udf.ParseFunction;
import org.opensearch.sql.expression.function.udf.RelevanceQueryFunction;
import org.opensearch.sql.expression.function.udf.RexExtractFunction;
import org.opensearch.sql.expression.function.udf.RexExtractMultiFunction;
import org.opensearch.sql.expression.function.udf.RexOffsetFunction;
import org.opensearch.sql.expression.function.udf.RmcommaConvertFunction;
import org.opensearch.sql.expression.function.udf.RmunitConvertFunction;
import org.opensearch.sql.expression.function.udf.SpanFunction;
import org.opensearch.sql.expression.function.udf.ToNumberFunction;
import org.opensearch.sql.expression.function.udf.ToStringFunction;
Expand Down Expand Up @@ -419,6 +424,14 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
new NumberToStringFunction().toUDF("NUMBER_TO_STRING");
public static final SqlOperator TONUMBER = new ToNumberFunction().toUDF("TONUMBER");
public static final SqlOperator TOSTRING = new ToStringFunction().toUDF("TOSTRING");

// PPL Convert command functions
public static final SqlOperator AUTO = new AutoConvertFunction().toUDF("AUTO");
public static final SqlOperator NUM = new NumConvertFunction().toUDF("NUM");
public static final SqlOperator RMCOMMA = new RmcommaConvertFunction().toUDF("RMCOMMA");
public static final SqlOperator RMUNIT = new RmunitConvertFunction().toUDF("RMUNIT");
public static final SqlOperator MEMK = new MemkConvertFunction().toUDF("MEMK");

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 @@ -23,6 +23,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ASIN;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ATAN;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ATAN2;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.AUTO;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.AVG;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CBRT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CEIL;
Expand Down Expand Up @@ -136,6 +137,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MAX;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MD5;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MEDIAN;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MEMK;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MICROSECOND;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MIN;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MINSPAN_BUCKET;
Expand All @@ -162,6 +164,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOTEQUAL;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOW;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NULLIF;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NUM;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.OR;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.PERCENTILE_APPROX;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.PERIOD_ADD;
Expand All @@ -185,6 +188,8 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.REX_OFFSET;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.RIGHT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.RINT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.RMCOMMA;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.RMUNIT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ROUND;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.RTRIM;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.SCALAR_MAX;
Expand Down Expand Up @@ -983,6 +988,14 @@ void populate() {
registerOperator(INTERNAL_PATTERN_PARSER, PPLBuiltinOperators.PATTERN_PARSER);
registerOperator(TONUMBER, PPLBuiltinOperators.TONUMBER);
registerOperator(TOSTRING, PPLBuiltinOperators.TOSTRING);

// Register PPL Convert command functions
registerOperator(AUTO, PPLBuiltinOperators.AUTO);
registerOperator(NUM, PPLBuiltinOperators.NUM);
registerOperator(RMCOMMA, PPLBuiltinOperators.RMCOMMA);
registerOperator(RMUNIT, PPLBuiltinOperators.RMUNIT);
registerOperator(MEMK, PPLBuiltinOperators.MEMK);

register(
TOSTRING,
(FunctionImp1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

/** PPL auto() conversion function. */
public class AutoConvertFunction extends BaseConversionUDF {

private static final AutoConvertFunction INSTANCE = new AutoConvertFunction();

public AutoConvertFunction() {
super(AutoConvertFunction.class);
}

public static Object convert(Object value) {
return INSTANCE.convertValue(value);
}
Comment on lines +13 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add JavaDoc for the public constructor and convert entrypoint.

These public methods currently lack JavaDoc with @param/@return/@throws. Please add the required JavaDoc for the constructor and static convert method.

As per coding guidelines: "core/src/main/java/**/*.java: Public methods MUST have JavaDoc with @param, @return, and @throws".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/org/opensearch/sql/expression/function/udf/AutoConvertFunction.java`
around lines 11 - 17, Add JavaDoc comments for the public AutoConvertFunction()
constructor and the public static convert(Object value) entrypoint: for
AutoConvertFunction(), add a short description and an `@throws` tag if the
constructor can propagate any runtime exceptions (or explicitly state none are
thrown); for convert(Object) add a description, an `@param` value explaining
accepted input, an `@return` describing the conversion result, and an `@throws`
documenting the runtime exception(s) that may be raised (e.g., if
convertValue(value) fails). Reference the constructor AutoConvertFunction(), the
static method convert(Object), and the instance helper convertValue(Object) in
the comments so maintainers can trace behavior.


@Override
protected Object applyConversion(String preprocessedValue) {
Double result = tryConvertMemoryUnit(preprocessedValue);
if (result != null) {
return result;
}

return NumConvertFunction.INSTANCE.applyConversion(preprocessedValue);
}
}
Loading
Loading