Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
67abdc9
Add support for space-separated fields in addition to comma-separated
aaarone90 Jul 30, 2025
abe02d6
Byte number should treated as Long in doc values (#3928)
LantaoJin Jul 30, 2025
464a916
Adding table as alias of the fields command
aaarone90 Jul 30, 2025
43c8f5e
Fix create PIT permissions issue (#3921)
vamsimanohar Jul 30, 2025
f3e5ad3
Convert like function call to wildcard query for Calcite filter pushd…
songkant-aws Jul 31, 2025
4602a09
Update commons-lang exclude rule to exclude it everywhere (#3932)
Swiddis Jul 31, 2025
1b589d7
Adding wildcard support to fields command in Calcite
aaarone90 Jul 31, 2025
08ff8dd
Mixed delimiter support - Support both space and comma delimiters in …
aaarone90 Jul 31, 2025
3a44f15
Adding widlcard support to non-Calcite engine and updating documentat…
aaarone90 Jul 31, 2025
88a538f
Fixing formatting issues
aaarone90 Jul 31, 2025
d2ceb08
Support function argument coercion with Calcite (#3914)
yuancu Jul 31, 2025
27106ce
Add missing command in index.rst (#3943)
penghuo Jul 31, 2025
62a75cd
Append limit operator for QUEERY_SIZE_LIMIT (#3940)
qianheng-aws Jul 31, 2025
c6e0ad1
Performing code cleaning and fixing tests
aaarone90 Aug 2, 2025
74ad8f8
Performing code maintenance and adding more test cases
aaarone90 Aug 3, 2025
feca986
Doing some code cleaning and maintenance
aaarone90 Aug 5, 2025
30990c4
Fixing code and implementation logic
aaarone90 Aug 5, 2025
fd705f0
Add issue template specific for PPL commands and queries (#3962)
anasalkouz Aug 3, 2025
70693ce
Increase the precision of sum return type (#3974)
qianheng-aws Aug 4, 2025
e420c14
Disable a failed PPL query fallback to v2 by default (#3952)
LantaoJin Aug 5, 2025
52726f3
Update the maven snapshot publish endpoint and credential (#3806)
zelinh Aug 5, 2025
8cf3be0
Add release notes for 3.2.0 (#3985)
opensearch-ci-bot Aug 5, 2025
3c2fe23
Fixing documentation
aaarone90 Aug 5, 2025
20b9d37
Taking care of comments left by Tomo
aaarone90 Aug 6, 2025
f082b39
Adding full wildcard support functionality
aaarone90 Aug 7, 2025
39182fd
Increment version to 3.2.0-SNAPSHOT (#3819)
opensearch-trigger-bot[bot] Aug 6, 2025
bfca3a4
Support `reverse` command with Calcite (#3867)
selsong Aug 6, 2025
124bc88
Pass JOIN_TIME_OUT value to keepalive (#3826)
ahkcs Aug 7, 2025
f781bdf
Changed seenFields to Hashset instead of LinkedHashSet
aaarone90 Aug 11, 2025
2aa452e
Creating a rule only for the fields/table commands to avoid any inter…
aaarone90 Aug 11, 2025
f60572d
This commit is in response of PR comments left by Tomo and Chen
aaarone90 Aug 12, 2025
7201b62
Fixing Integration test failure
aaarone90 Aug 12, 2025
c173370
Adding anonymizer tests, wildcard unit tests, etc
aaarone90 Aug 13, 2025
ac1c18b
Disabling Calcite for enhance fields features
aaarone90 Aug 13, 2025
a18a95e
Disabling automatic de-deduplication when Calcite is disabled
aaarone90 Aug 13, 2025
622f348
Adding cross-cluster IT test
aaarone90 Aug 13, 2025
200db81
Adding a dedicated Cross-cluster IT test file for Calcite
aaarone90 Aug 14, 2025
4b85d3b
Fixing formatting issues
aaarone90 Aug 14, 2025
92d1ad0
Improving widlcard logic and exception message
aaarone90 Aug 18, 2025
6b5eae5
Addressing comments left by Tomo regarding wildcard logic implementation
aaarone90 Aug 19, 2025
26176d7
Empty commit
aaarone90 Aug 19, 2025
5639f9d
Add missing udfs in v3 (#3957)
ishaoxy Aug 8, 2025
80b6e92
fix snapshot uploading (#4006)
ahkcs Aug 8, 2025
ee864ab
Fix DOUBLE to STRING cast rendering zero values in scientific notatio…
yuancu Aug 11, 2025
19866cb
Eliminate reliance on assert in Calcite for integration test (#4016)
yuancu Aug 11, 2025
9b3711f
Prevent aggregation push down when it has inner filter (#4002)
qianheng-aws Aug 11, 2025
dbb9504
Fix span on negative timestamp (#4017)
qianheng-aws Aug 13, 2025
93a6c9b
Skip script encoding when run explain with 'extended' (#3930)
LantaoJin Aug 13, 2025
6248aea
Implement type checking for aggregation functions with Calcite (#4024)
yuancu Aug 15, 2025
6493cac
Allow equal expression as a function argument (#4001)
yuancu Aug 18, 2025
a26f0a6
Push down IP comparison as range query with Calcite (#3959)
yuancu Aug 18, 2025
de6a45b
eval sum, avg implementation (#3986)
vamsimanohar Aug 18, 2025
9efc085
Fix PPL eval command string concatenation with + operator (#4020)
ahkcs Aug 19, 2025
0323880
Support script push down on text field (#4010)
qianheng-aws Aug 19, 2025
3b5dc95
Enhance sort command in PPL (#3934)
ritvibhatt Aug 19, 2025
3b4c34c
Add example for String concat in eval.rst (#4075)
ahkcs Aug 20, 2025
9844ce6
Support pushdown dedup with Calcite (#3972)
LantaoJin Aug 20, 2025
ea2718e
Fix CI failure because of plan having changed (#4077)
qianheng-aws Aug 20, 2025
7423766
Empty commit
aaarone90 Aug 19, 2025
6ff2286
Empty commit
aaarone90 Aug 19, 2025
6b8a3d0
Merge branch 'main' into feature/fields-command-wildcard-support
aalva500-prog Aug 20, 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
139 changes: 91 additions & 48 deletions core/src/main/java/org/opensearch/sql/analysis/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.ImmutablePair;
Expand Down Expand Up @@ -284,13 +285,6 @@ public LogicalPlan visitFilter(Filter node, AnalysisContext context) {
return new LogicalFilter(child, optimized);
}

/**
* Ensure NESTED function is not used in GROUP BY, and HAVING clauses. Fallback to legacy engine.
* Can remove when support is added for NESTED function in WHERE, GROUP BY, ORDER BY, and HAVING
* clauses.
*
* @param condition : Filter condition
*/
private void verifySupportsCondition(Expression condition) {
if (condition instanceof FunctionExpression) {
if (((FunctionExpression) condition)
Expand Down Expand Up @@ -386,53 +380,106 @@ public LogicalPlan visitRareTopN(RareTopN node, AnalysisContext context) {
public LogicalPlan visitProject(Project node, AnalysisContext context) {
LogicalPlan child = node.getChild().get(0).accept(this, context);

if (node.hasArgument()) {
Argument argument = node.getArgExprList().get(0);
Boolean exclude = (Boolean) argument.getValue().getValue();
if (exclude) {
TypeEnvironment curEnv = context.peek();
List<ReferenceExpression> referenceExpressions =
node.getProjectList().stream()
.map(expr -> (ReferenceExpression) expressionAnalyzer.analyze(expr, context))
.collect(Collectors.toList());
referenceExpressions.forEach(ref -> curEnv.remove(ref));
return new LogicalRemove(child, ImmutableSet.copyOf(referenceExpressions));
}
}

// For each unresolved window function, analyze it by "insert" a window and sort operator
// between project and its child.
for (UnresolvedExpression expr : node.getProjectList()) {
WindowExpressionAnalyzer windowAnalyzer =
new WindowExpressionAnalyzer(expressionAnalyzer, child);
child = windowAnalyzer.analyze(expr, context);
if (isExcludeMode(node)) {
return buildLogicalRemove(node, child, context);
}

for (UnresolvedExpression expr : node.getProjectList()) {
HighlightAnalyzer highlightAnalyzer = new HighlightAnalyzer(expressionAnalyzer, child);
child = highlightAnalyzer.analyze(expr, context);
}
child = processWindowExpressions(node.getProjectList(), child, context);
child = processHighlightExpressions(node.getProjectList(), child, context);

List<NamedExpression> namedExpressions =
selectExpressionAnalyzer.analyze(
node.getProjectList(),
context,
new ExpressionReferenceOptimizer(expressionAnalyzer.getRepository(), child));

for (UnresolvedExpression expr : node.getProjectList()) {
NestedAnalyzer nestedAnalyzer =
new NestedAnalyzer(namedExpressions, expressionAnalyzer, child);
child = nestedAnalyzer.analyze(expr, context);
}
resolveFieldExpressions(node.getProjectList(), child, context);

child = processNestedAnalysis(node.getProjectList(), namedExpressions, child, context);

// new context
context.push();
TypeEnvironment newEnv = context.peek();
namedExpressions.forEach(
expr ->
newEnv.define(new Symbol(Namespace.FIELD_NAME, expr.getNameOrAlias()), expr.type()));
List<NamedExpression> namedParseExpressions = context.getNamedParseExpressions();
return new LogicalProject(child, namedExpressions, namedParseExpressions);

return new LogicalProject(child, namedExpressions, context.getNamedParseExpressions());
}

private boolean isExcludeMode(Project node) {
if (!node.hasArgument()) {
return false;
}
try {
Argument argument = node.getArgExprList().get(0);
Object value = argument.getValue().getValue();
return Boolean.TRUE.equals(value);
} catch (IndexOutOfBoundsException | NullPointerException e) {
return false;
}
}

private LogicalRemove buildLogicalRemove(
Project node, LogicalPlan child, AnalysisContext context) {
TypeEnvironment curEnv = context.peek();
List<ReferenceExpression> referenceExpressions =
collectExclusionFields(node.getProjectList(), context);

Set<String> allFields = curEnv.lookupAllFields(Namespace.FIELD_NAME).keySet();
Set<String> fieldsToExclude =
referenceExpressions.stream().map(ReferenceExpression::getAttr).collect(Collectors.toSet());

if (allFields.equals(fieldsToExclude)) {
throw new IllegalArgumentException(
"Invalid field exclusion: operation would exclude all fields from the result set");
}

referenceExpressions.forEach(curEnv::remove);
return new LogicalRemove(child, ImmutableSet.copyOf(referenceExpressions));
}

private LogicalPlan processWindowExpressions(
List<UnresolvedExpression> projectList, LogicalPlan child, AnalysisContext context) {
for (UnresolvedExpression expr : projectList) {
child = new WindowExpressionAnalyzer(expressionAnalyzer, child).analyze(expr, context);
}
return child;
}

private LogicalPlan processHighlightExpressions(
List<UnresolvedExpression> projectList, LogicalPlan child, AnalysisContext context) {
for (UnresolvedExpression expr : projectList) {
child = new HighlightAnalyzer(expressionAnalyzer, child).analyze(expr, context);
}
return child;
}

private List<NamedExpression> resolveFieldExpressions(
List<UnresolvedExpression> projectList, LogicalPlan child, AnalysisContext context) {
return selectExpressionAnalyzer.analyze(
projectList,
context,
new ExpressionReferenceOptimizer(expressionAnalyzer.getRepository(), child));
}

private LogicalPlan processNestedAnalysis(
List<UnresolvedExpression> projectList,
List<NamedExpression> namedExpressions,
LogicalPlan child,
AnalysisContext context) {
for (UnresolvedExpression expr : projectList) {
child =
new NestedAnalyzer(namedExpressions, expressionAnalyzer, child).analyze(expr, context);
}
return child;
}

private List<ReferenceExpression> collectExclusionFields(
List<UnresolvedExpression> projectList, AnalysisContext context) {
List<NamedExpression> namedExpressions =
projectList.stream()
.map(expr -> expressionAnalyzer.analyze(expr, context))
.map(DSL::named)
.collect(Collectors.toList());

return namedExpressions.stream()
.map(field -> (ReferenceExpression) field.getDelegated())
.collect(Collectors.toList());
}

/** Build {@link LogicalEval}. */
Expand Down Expand Up @@ -746,10 +793,6 @@ private LogicalSort buildSort(
return new LogicalSort(child, count, sortList);
}

/**
* The first argument is always "asc", others are optional. Given nullFirst argument, use its
* value. Otherwise just use DEFAULT_ASC/DESC.
*/
private SortOption analyzeSortOption(List<Argument> fieldArgs) {
Boolean asc = (Boolean) fieldArgs.get(0).getValue().getValue();
Optional<Argument> nullFirst =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
import org.opensearch.sql.calcite.utils.JoinAndLookupUtils;
import org.opensearch.sql.calcite.utils.PlanUtils;
import org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils;
import org.opensearch.sql.calcite.utils.WildcardUtils;
import org.opensearch.sql.common.patterns.PatternUtils;
import org.opensearch.sql.common.utils.StringUtils;
import org.opensearch.sql.exception.CalciteUnsupportedException;
Expand Down Expand Up @@ -191,30 +192,118 @@ private boolean containsSubqueryExpression(Node expr) {
@Override
public RelNode visitProject(Project node, CalcitePlanContext context) {
visitChildren(node, context);
List<RexNode> projectList;
if (node.getProjectList().size() == 1
&& node.getProjectList().getFirst() instanceof AllFields allFields) {
tryToRemoveNestedFields(context);
tryToRemoveMetaFields(context, allFields instanceof AllFieldsExcludeMeta);
return context.relBuilder.peek();
} else {
projectList =
node.getProjectList().stream()
.map(expr -> rexVisitor.analyze(expr, context))
.collect(Collectors.toList());

if (isSingleAllFieldsProject(node)) {
return handleAllFieldsProject(node, context);
}

List<String> currentFields = context.relBuilder.peek().getRowType().getFieldNames();
List<RexNode> expandedFields =
expandProjectFields(node.getProjectList(), currentFields, context);

if (node.isExcluded()) {
context.relBuilder.projectExcept(projectList);
validateExclusion(expandedFields, currentFields);
context.relBuilder.projectExcept(expandedFields);
} else {
// Only set when not resolving subquery and it's not projectExcept.
if (!context.isResolvingSubquery()) {
context.setProjectVisited(true);
}
context.relBuilder.project(projectList);
context.relBuilder.project(expandedFields);
}
return context.relBuilder.peek();
}

private boolean isSingleAllFieldsProject(Project node) {
return node.getProjectList().size() == 1
&& node.getProjectList().getFirst() instanceof AllFields;
}

private RelNode handleAllFieldsProject(Project node, CalcitePlanContext context) {
if (node.isExcluded()) {
throw new IllegalArgumentException(
"Invalid field exclusion: operation would exclude all fields from the result set");
}
AllFields allFields = (AllFields) node.getProjectList().getFirst();
tryToRemoveNestedFields(context);
tryToRemoveMetaFields(context, allFields instanceof AllFieldsExcludeMeta);
return context.relBuilder.peek();
}

private List<RexNode> expandProjectFields(
List<UnresolvedExpression> projectList,
List<String> currentFields,
CalcitePlanContext context) {
List<RexNode> expandedFields = new ArrayList<>();
Set<String> addedFields = new HashSet<>();

for (UnresolvedExpression expr : projectList) {
switch (expr) {
case Field field -> {
String fieldName = field.getField().toString();
if (WildcardUtils.containsWildcard(fieldName)) {
List<String> matchingFields =
WildcardUtils.expandWildcardPattern(fieldName, currentFields).stream()
.filter(f -> !isMetadataField(f))
.filter(addedFields::add)
.toList();
if (matchingFields.isEmpty()) {
continue;
}
matchingFields.forEach(f -> expandedFields.add(context.relBuilder.field(f)));
} else if (addedFields.add(fieldName)) {
expandedFields.add(rexVisitor.analyze(field, context));
}
}
case AllFields ignored -> {
currentFields.stream()
.filter(field -> !isMetadataField(field))
.filter(addedFields::add)
.forEach(field -> expandedFields.add(context.relBuilder.field(field)));
}
default -> throw new IllegalStateException(
"Unexpected expression type in project list: " + expr.getClass().getSimpleName());
}
}

if (expandedFields.isEmpty()) {
validateWildcardPatterns(projectList, currentFields);
}

return expandedFields;
}

private void validateExclusion(List<RexNode> fieldsToExclude, List<String> currentFields) {
Set<String> nonMetaFields =
currentFields.stream().filter(field -> !isMetadataField(field)).collect(Collectors.toSet());

if (fieldsToExclude.size() >= nonMetaFields.size()) {
throw new IllegalArgumentException(
"Invalid field exclusion: operation would exclude all fields from the result set");
}
}

private void validateWildcardPatterns(
List<UnresolvedExpression> projectList, List<String> currentFields) {
String firstWildcardPattern =
projectList.stream()
.filter(
expr ->
expr instanceof Field field
&& WildcardUtils.containsWildcard(field.getField().toString()))
.map(expr -> ((Field) expr).getField().toString())
.findFirst()
.orElse(null);

if (firstWildcardPattern != null) {
throw new IllegalArgumentException(
String.format("wildcard pattern [%s] matches no fields", firstWildcardPattern));
}
}

private boolean isMetadataField(String fieldName) {
return OpenSearchConstants.METADATAFIELD_TYPE_MAP.containsKey(fieldName);
}

/** See logic in {@link org.opensearch.sql.analysis.symbol.SymbolTable#lookupAllFields} */
private static void tryToRemoveNestedFields(CalcitePlanContext context) {
Set<String> allFields = new HashSet<>(context.relBuilder.peek().getRowType().getFieldNames());
Expand Down Expand Up @@ -499,7 +588,6 @@ public RelNode visitPatterns(Patterns node, CalcitePlanContext context) {
@Override
public RelNode visitEval(Eval node, CalcitePlanContext context) {
visitChildren(node, context);
List<String> originalFieldNames = context.relBuilder.peek().getRowType().getFieldNames();
node.getExpressionList()
.forEach(
expr -> {
Expand Down
Loading
Loading