Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ea02f53
feat: Implement PPL convert command with 5 conversion functions
aaarone90 Dec 31, 2025
4d1552a
Fixing integration tests
aaarone90 Jan 10, 2026
f18fdf3
Fixing ymal files for IT explain tests
aaarone90 Jan 12, 2026
dfe6957
Fix cross-cluster IT failure
aaarone90 Jan 12, 2026
2c24770
Making code more readable and removing unnecessary logic
aaarone90 Jan 12, 2026
27e4bb9
Refactor: Extract BaseConversionUDF to eliminate duplication
aaarone90 Jan 14, 2026
c3b6b8f
Merge branch 'opensearch-project:main' into feature/ppl-convert-command
aalva500-prog Jan 14, 2026
e277af8
Fixing CI failure and refactoring
aaarone90 Jan 15, 2026
458ca1e
trigger CI
aaarone90 Jan 15, 2026
6da30b8
Addressing CodeRabbit comments
aaarone90 Jan 15, 2026
a8e4dba
Adding support for memk function
aaarone90 Jan 16, 2026
052f40f
Fixing formatting
aaarone90 Jan 16, 2026
6fd45df
Merge branch 'opensearch-project:main' into feature/ppl-convert-command
aalva500-prog Jan 16, 2026
b98dc1e
Fixing CI failure
aaarone90 Jan 16, 2026
dc70fd1
Fixing visitConvert in FieldResolutionVisitor class
aaarone90 Jan 16, 2026
1aa6843
Updating documentation
aaarone90 Jan 22, 2026
7018b0a
Refactoring code to avoid regestering none() as a convert function an…
aaarone90 Jan 23, 2026
3f033ab
refactor: Simplify Convert command using Let expressions
aaarone90 Jan 24, 2026
11a5ead
Trigger CI
aaarone90 Jan 24, 2026
b67e0d6
Refactoring code to use Template Method Design Pattern
aaarone90 Jan 26, 2026
c4f987f
Updating documentation
aaarone90 Jan 26, 2026
062e3a8
Trigger CI
aaarone90 Jan 26, 2026
be2344f
Updating convert example with stats
aaarone90 Jan 27, 2026
aaa156f
Renaming unit test class, as ConversionUtil class was removed
aaarone90 Jan 27, 2026
0bef8c7
Fixing IT test case
aaarone90 Jan 27, 2026
f7d2f1b
Resolve merge conflict: Add testTransposeCommand alongside testConver…
aaarone90 Jan 27, 2026
0e27e47
Merge branch 'main' into feature/ppl-convert-command
aalva500-prog Jan 27, 2026
f820de0
Add null and empty string tests for NumConvertFunction
aaarone90 Jan 27, 2026
6a4bb40
Add null and empty string tests for RmunitConvertFunction
aaarone90 Jan 27, 2026
a2ec165
Verify convert AS clause preserves original field
aaarone90 Jan 27, 2026
19749c7
Adding edge test cases, as recommended by Coderabbit
aaarone90 Jan 27, 2026
10b8fc7
Adding test case to FieldResolutionVisitorTest class
aaarone90 Jan 27, 2026
5d5fc82
Trigger CI
aaarone90 Jan 27, 2026
9e161e0
Removing timeformat parameter for now, will add later
aaarone90 Jan 27, 2026
d6aea94
Trigger CI
aaarone90 Jan 27, 2026
11094a4
Re-trigger CI
aaarone90 Jan 27, 2026
a323176
Merge branch 'opensearch-project:main' into feature/ppl-convert-command
aalva500-prog Jan 28, 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 @@ -524,6 +525,11 @@ public LogicalPlan visitEval(Eval node, AnalysisContext context) {
return new LogicalEval(child, expressionsBuilder.build());
}

@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 @@ -410,6 +411,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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.opensearch.sql.ast.tree.AppendPipe;
import org.opensearch.sql.ast.tree.Bin;
import org.opensearch.sql.ast.tree.Chart;
import org.opensearch.sql.ast.tree.Convert;
import org.opensearch.sql.ast.tree.ConvertFunction;
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 @@ -605,6 +607,20 @@ public Node visitExpand(Expand node, FieldResolutionContext context) {
return node;
}

@Override
public Node visitConvert(Convert node, FieldResolutionContext context) {
Set<String> convertFields = new HashSet<>();
for (ConvertFunction convertFunc : node.getConvertFunctions()) {
for (String fieldName : convertFunc.getFieldList()) {
convertFields.add(fieldName);
}
}
context.pushRequirements(context.getCurrentRequirements().or(convertFields));
visitChildren(node, context);
context.popRequirements();
return node;
}

private Set<String> extractFieldsFromAggregation(UnresolvedExpression expr) {
Set<String> fields = new HashSet<>();
if (expr instanceof Alias alias) {
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;

/** AST node representing the Convert command. */
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
public class Convert extends UnresolvedPlan {
private final String timeformat;
private final List<ConvertFunction> convertFunctions;
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
@@ -0,0 +1,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.ast.tree;

import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

/** Represents a single conversion function within a convert command. */
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public class ConvertFunction {
private final String functionName;
private final List<String> fieldList;
private final String asField;
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
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.ConvertFunction;
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 @@ -928,6 +930,90 @@ public RelNode visitEval(Eval node, CalcitePlanContext context) {
return context.relBuilder.peek();
}

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

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

java.util.Map<String, RexNode> replacements = new java.util.HashMap<>();
List<Pair<String, RexNode>> additions = new ArrayList<>();
Set<String> seenFields = new HashSet<>();

for (ConvertFunction convertFunc : node.getConvertFunctions()) {
processConversionFunction(convertFunc, replacements, additions, seenFields, context);
}

return buildProjectionWithConversions(replacements, additions, context);
}

private void processConversionFunction(
ConvertFunction convertFunc,
java.util.Map<String, RexNode> replacements,
List<Pair<String, RexNode>> additions,
Set<String> seenFields,
CalcitePlanContext context) {
String functionName = convertFunc.getFunctionName();
List<String> fieldList = convertFunc.getFieldList();
String asField = convertFunc.getAsField();

if (fieldList.size() != 1) {
throw new SemanticCheckException("Convert function must operate on exactly one field");
}

String fieldName = fieldList.get(0);

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

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

if (asField != null) {
additions.add(Pair.of(asField, context.relBuilder.alias(convertCall, asField)));
} else {
replacements.put(fieldName, context.relBuilder.alias(convertCall, fieldName));
}
}

private RelNode buildProjectionWithConversions(
java.util.Map<String, RexNode> replacements,
List<Pair<String, RexNode>> additions,
CalcitePlanContext context) {
List<String> originalFields = context.relBuilder.peek().getRowType().getFieldNames();
List<RexNode> projectList = new ArrayList<>();

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

Set<String> addedAsNames = new HashSet<>();

for (Pair<String, RexNode> addition : additions) {
String asName = addition.getLeft();

if (originalFields.contains(asName)) {
throw new SemanticCheckException(
String.format("AS name '%s' conflicts with existing field", asName));
}

if (!addedAsNames.add(asName)) {
throw new SemanticCheckException(
String.format("AS name '%s' is used multiple times in convert", 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 @@ -63,12 +63,18 @@
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.NoneConvertFunction;
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 @@ -421,6 +427,15 @@ 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 NONE = new NoneConvertFunction().toUDF("NONE");

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 @@ -22,6 +22,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 @@ -158,10 +160,12 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVJOIN;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVMAP;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVZIP;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NONE;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOT;
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 +189,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 +989,15 @@ 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);
registerOperator(NONE, PPLBuiltinOperators.NONE);

register(
TOSTRING,
(FunctionImp1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 {

public AutoConvertFunction() {
super("autoConvert");
}
}
Loading
Loading