Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
8362fc2
Initial checkpoint - following calcite way and commented legacy way
Oct 10, 2025
384ba15
Removed the build.gradle dependency opensearch-common
Oct 22, 2025
7f382f9
Ready to submit this PR
Oct 22, 2025
44c8124
Ready to submit this PR
Oct 22, 2025
3cad64e
Ready to submit this PR
Oct 22, 2025
8e4a2c5
Add mvexpand.rst
Oct 22, 2025
474617d
Add Tests
Oct 22, 2025
d502b03
Add the mvexpand.rst to the index.rst
Oct 23, 2025
c62defe
Remove the unwanted code
Oct 27, 2025
a3799b2
Fix the failing test
Oct 27, 2025
d90be9f
Address the PR comments and fix the tests accordingly
Oct 30, 2025
da16288
Address the PR comments and fix the tests accordingly
Oct 30, 2025
1301e06
Address the PR comments and fix the tests accordingly
Oct 30, 2025
beb31de
Add comment lines for buildUnnestForLeft
Oct 30, 2025
627ef8f
Fix the mvexpand.rst
Oct 31, 2025
58facf8
Fix the failing test
Nov 3, 2025
63cdbf7
Fix the failing test
Nov 3, 2025
bdc3aa1
Fix the failing test
Nov 3, 2025
fc8e345
Fix the failing test
Nov 3, 2025
c830356
Address the PR comments
Nov 6, 2025
e9b6f27
Address the PR comments
Nov 7, 2025
fa9436e
Address the PR comments
Nov 13, 2025
ea091d2
Address the PR comments
Nov 13, 2025
4d9b24d
Address the PR comments
Nov 14, 2025
b9d3164
Address the issue as the happy path scenario was not working the way …
Nov 19, 2025
26a59a4
MvExpand as its own implementation - not aliasing
Nov 20, 2025
43c806e
Refactoring EXPAND and MVEXPAND
Nov 20, 2025
a07dff2
Refactor EXPAND and MVEXPAND and fix its unittest
Nov 20, 2025
7be7473
Convert mvexpand.rst examples to doctest
Nov 20, 2025
2c0ea2c
metadata.rst was missing the mvexpand_logs entry
Nov 21, 2025
8749289
Address the PR comments for IT and visitMvExpand
Nov 23, 2025
9508874
Merge branch 'main' into main
srikanthpadakanti Nov 27, 2025
08b56ee
Add test for mvdedup function with duplicates
srikanthpadakanti Nov 28, 2025
3ae2c73
Merge branch 'main' into main
srikanthpadakanti Nov 28, 2025
bed2084
Update core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVi…
srikanthpadakanti Nov 28, 2025
5e616ff
Update core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVi…
srikanthpadakanti Nov 28, 2025
709704c
Address the PR comments
Nov 28, 2025
c45fa05
Address the PR comments
Nov 28, 2025
4f3435e
change the limit behavior from global to perDocument
Nov 28, 2025
a0b2c8c
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Dec 2, 2025
47779e1
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Dec 3, 2025
bf6b924
Fix the CI issues
Dec 4, 2025
9aec421
Merge branch 'main' into main
srikanthpadakanti Dec 9, 2025
bf87312
Update the index.rst
Dec 9, 2025
c9e2767
spotlessapply
Dec 9, 2025
2591a6c
Merge branch 'mywork-backup'
Dec 11, 2025
125cf3b
spotlessapply
Dec 11, 2025
00c990f
address merge issue
Dec 11, 2025
44814ab
address merge issue
Dec 11, 2025
f9dd692
change rst to md
Dec 11, 2025
69d6a5a
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Dec 11, 2025
2464675
change rst to md
Dec 11, 2025
0f86c52
delete unnecessary test
Dec 11, 2025
32d3867
Merge branch 'main' into main
srikanthpadakanti Dec 18, 2025
07509ae
remove index.rst and add mvexpand entry in index.md
Dec 18, 2025
34db739
spotless apply
Dec 18, 2025
602358e
merge issues.
Dec 18, 2025
b1f2e59
Merge remote-tracking branch 'upstream/main'
Jan 2, 2026
2adbf6f
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
559165f
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
f7d942d
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
6ca94e2
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
e747edb
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
587ccb2
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
600637f
Address and resolve the PR comments from Dec 30th 2025
Jan 3, 2026
79c9b9d
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Jan 7, 2026
19c2065
undo the overriden test
Jan 7, 2026
16dbaad
Add asserts to unittests
Jan 7, 2026
1d0a56e
Add asserts to unittests
Jan 7, 2026
d3651f8
Add asserts to unittests
Jan 7, 2026
5efd096
Address the PR comment to related to IT to use single one and test di…
Jan 7, 2026
e067b46
Address the PR comment to verify the output shape of mvexpand output
Jan 7, 2026
514a7dc
Address the PR comment for mvexpand.md example
Jan 7, 2026
9e8ea2c
Address the PR comment for mvexpand.md example
Jan 7, 2026
00f440b
Address the PR comment for mvexpand to throw special Exception class …
Jan 7, 2026
840a454
spotlessapply
Jan 7, 2026
50031ce
spotlessapply
Jan 7, 2026
1b6674f
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Jan 16, 2026
e988320
Fix the mvexpand.md review comments
Jan 16, 2026
c743933
Address the comments related to CalciteRelNodeVisitor
Jan 16, 2026
658c1e1
Address the comments related to CalciteRelNodeVisitor
Jan 16, 2026
98d9a53
Merge upstream/main into main
Feb 3, 2026
e3239ae
Apply mvexpand changes after upstream merge
Feb 4, 2026
c3d0f4d
Fix the mvexpand doctest
Feb 4, 2026
c72a752
Force run CI pipeline
Feb 4, 2026
a519507
change the crosscluster IT to proper location
Feb 9, 2026
ca7acbb
Merge branch 'opensearch-project:main' into main
srikanthpadakanti Feb 10, 2026
3fed3c3
Merge upstream to my branch
Feb 11, 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 @@ -81,6 +81,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -732,6 +733,11 @@ public LogicalPlan visitExpand(Expand expand, AnalysisContext context) {
throw getOnlyForCalciteException("Expand");
}

@Override
public LogicalPlan visitMvExpand(MvExpand node, AnalysisContext context) {
throw getOnlyForCalciteException("MvExpand");
}

/** Build {@link LogicalTrendline} for Trendline command. */
@Override
public LogicalPlan visitTrendline(Trendline node, AnalysisContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -475,4 +476,8 @@ public T visitAddColTotals(AddColTotals node, C context) {
public T visitMvCombine(MvCombine node, C context) {
return visitChildren(node, context);
}

public T visitMvExpand(MvExpand node, C context) {
return visitChildren(node, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.opensearch.sql.ast.tree.Lookup;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
import org.opensearch.sql.ast.tree.Project;
Expand Down Expand Up @@ -657,6 +658,15 @@ public Node visitMvCombine(MvCombine node, FieldResolutionContext context) {
return node;
}

@Override
public Node visitMvExpand(MvExpand node, FieldResolutionContext context) {
Set<String> mvExpandFields = extractFieldsFromExpression(node.getField());
context.pushRequirements(context.getCurrentRequirements().or(mvExpandFields));
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
5 changes: 5 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.opensearch.sql.ast.tree.Limit;
import org.opensearch.sql.ast.tree.MinSpanBin;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
import org.opensearch.sql.ast.tree.Project;
Expand Down Expand Up @@ -137,6 +138,10 @@ public Expand expand(UnresolvedPlan input, Field field, String alias) {
return new Expand(field, alias).attach(input);
}

public static UnresolvedPlan mvexpand(UnresolvedPlan input, Field field, Integer limit) {
return new MvExpand(field, limit).attach(input);
}

public static UnresolvedPlan projectWithArg(
UnresolvedPlan input, List<Argument> argList, UnresolvedExpression... projectList) {
return new Project(Arrays.asList(projectList), argList).attach(input);
Expand Down
46 changes: 46 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/MvExpand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.ast.expression.Field;

/** AST node representing the {@code mvexpand} PPL command: {@code mvexpand <field> [limit=N]}. */
@ToString
@EqualsAndHashCode(callSuper = false)
public class MvExpand extends UnresolvedPlan {

private UnresolvedPlan child;
@Getter private final Field field;
@Getter @Nullable private final Integer limit;

public MvExpand(Field field, @Nullable Integer limit) {
this.field = field;
this.limit = limit;
}

@Override
public MvExpand 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.visitMvExpand(this, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -955,7 +956,11 @@ public RelNode visitPatterns(Patterns node, CalcitePlanContext context) {
.toList();
context.relBuilder.aggregate(context.relBuilder.groupKey(groupByList), aggCall);
buildExpandRelNode(
context.relBuilder.field(node.getAlias()), node.getAlias(), node.getAlias(), context);
context.relBuilder.field(node.getAlias()),
node.getAlias(),
node.getAlias(),
null,
context);
flattenParsedPattern(
node.getAlias(),
context.relBuilder.field(node.getAlias()),
Expand Down Expand Up @@ -3166,7 +3171,7 @@ public RelNode visitExpand(Expand expand, CalcitePlanContext context) {
RexInputRef arrayFieldRex = (RexInputRef) rexVisitor.analyze(arrayField, context);
String alias = expand.getAlias();

buildExpandRelNode(arrayFieldRex, arrayField.getField().toString(), alias, context);
buildExpandRelNode(arrayFieldRex, arrayField.getField().toString(), alias, null, context);

return context.relBuilder.peek();
}
Expand Down Expand Up @@ -3339,6 +3344,61 @@ private void restoreColumnOrderAfterArrayAgg(
relBuilder.project(projections, projectionNames, /* force= */ true);
}

/**
* MVExpand command visitor.
*
* <p>Expands a multi-value (array) field into separate rows using Calcite's CORRELATE join with
* UNCOLLECT. Each element of the array becomes a separate row while preserving all other fields
* from the original row.
*
* <p>Implementation uses {@link #buildExpandRelNode} to create a correlate join between the
* original relation and an uncollected (unnested) version of the target array field.
*
* <p>Behavior:
*
* <ul>
* <li>Array fields: Each array element is expanded into a separate row
* <li>Non-array fields: Treated as single-element arrays (returns original row unchanged)
* <li>Missing fields: Throws {@link SemanticCheckException}
* <li>Optional limit parameter: Limits the number of expanded elements per document
* </ul>
*
* @param mvExpand MVExpand command containing the field to expand and optional limit
* @param context CalcitePlanContext containing the RelBuilder and planning context
* @return RelNode representing the relation with the expanded multi-value field
* @throws SemanticCheckException if the target field does not exist in the schema
*/
@Override
public RelNode visitMvExpand(MvExpand mvExpand, CalcitePlanContext context) {
visitChildren(mvExpand, context);

final RelBuilder relBuilder = context.relBuilder;
final Field field = mvExpand.getField();
final String fieldName = field.getField().toString();

final RelDataType inputType = relBuilder.peek().getRowType();
final RelDataTypeField inputField =
inputType.getField(fieldName, /*caseSensitive*/ true, /*elideRecord*/ false);

if (inputField == null) {
throw new SemanticCheckException(
String.format("Field '%s' not found in the schema", fieldName));
}

final RexInputRef arrayFieldRex = (RexInputRef) rexVisitor.analyze(field, context);

final SqlTypeName actual = arrayFieldRex.getType().getSqlTypeName();
if (actual != SqlTypeName.ARRAY) {
// For non-array fields (scalars), mvexpand just returns the field unchanged.
// This treats single-value fields as if they were arrays with one element.
return relBuilder.peek();
}

buildExpandRelNode(arrayFieldRex, fieldName, fieldName, mvExpand.getLimit(), context);

return relBuilder.peek();
}

@Override
public RelNode visitValues(Values values, CalcitePlanContext context) {
if (values.getValues() == null || values.getValues().isEmpty()) {
Expand Down Expand Up @@ -3583,7 +3643,11 @@ private void flattenParsedPattern(
}

private void buildExpandRelNode(
RexInputRef arrayFieldRex, String arrayFieldName, String alias, CalcitePlanContext context) {
RexInputRef arrayFieldRex,
String arrayFieldName,
String alias,
@Nullable Integer perDocLimit,
CalcitePlanContext context) {
// 3. Capture the outer row in a CorrelationId
Holder<RexCorrelVariable> correlVariable = Holder.empty();
context.relBuilder.variable(correlVariable::set);
Expand All @@ -3598,14 +3662,17 @@ private void buildExpandRelNode(
RelNode leftNode = context.relBuilder.build();

// 5. Build join right node and expand the array field using uncollect
RelNode rightNode =
context
.relBuilder
// fake input, see convertUnnest and convertExpression in Calcite SqlToRelConverter
.push(LogicalValues.createOneRow(context.relBuilder.getCluster()))
.project(List.of(correlArrayFieldAccess), List.of(arrayFieldName))
.uncollect(List.of(), false)
.build();
context
.relBuilder
// fake input, see convertUnnest and convertExpression in Calcite SqlToRelConverter
.push(LogicalValues.createOneRow(context.relBuilder.getCluster()))
.project(List.of(correlArrayFieldAccess), List.of(arrayFieldName))
.uncollect(List.of(), false);

if (perDocLimit != null) {
context.relBuilder.limit(0, perDocLimit);
}
RelNode rightNode = context.relBuilder.build();

// 6. Perform a nested-loop join (correlate) between the original table and the expanded
// array field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,14 @@ void populate() {
OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.INTEGER)
.or(OperandTypes.family(SqlTypeFamily.MAP, SqlTypeFamily.ANY)),
false));
// Allow using INTERNAL_ITEM when the element type is unknown/undefined at planning time.
// Some datasets (or Calcite's type inference) may give the element an UNDEFINED type.
// Accept a "ignore" first-argument family so INTERNAL_ITEM(elem, 'key') can still be planned
// and resolved at runtime (fallback semantics handled at execution side). - Used in MVEXPAND
registerOperator(
INTERNAL_ITEM,
SqlStdOperatorTable.ITEM,
PPLTypeChecker.family(SqlTypeFamily.IGNORE, SqlTypeFamily.CHARACTER));
registerOperator(
XOR,
SqlStdOperatorTable.NOT_EQUALS,
Expand Down
3 changes: 2 additions & 1 deletion docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"user/ppl/cmd/regex.md",
"user/ppl/cmd/rename.md",
"user/ppl/cmd/multisearch.md",
"user/ppl/cmd/mvexpand.md",
"user/ppl/cmd/replace.md",
"user/ppl/cmd/rex.md",
"user/ppl/cmd/search.md",
Expand Down Expand Up @@ -82,4 +83,4 @@
"bash_settings": [
"user/ppl/admin/settings.md"
]
}
}
1 change: 1 addition & 0 deletions docs/user/dql/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,4 @@ SQL query::
| docTestCluster | null | accounts | firstname | null | text | null | null | null | 10 | 2 | null | null | null | null | null | 1 | | null | null | null | null | NO | |
| docTestCluster | null | accounts | lastname | null | text | null | null | null | 10 | 2 | null | null | null | null | null | 10 | | null | null | null | null | NO | |
+----------------+-------------+------------+-------------+-----------+-----------+-------------+---------------+----------------+----------------+----------+---------+------------+---------------+------------------+-------------------+------------------+-------------+---------------+--------------+-------------+------------------+------------------+--------------------+

Loading
Loading