Skip to content

Commit 2a699c6

Browse files
committed
Extract ProjectHandler from CalciteRelNodeVisitor
Signed-off-by: Tomoyuki Morita <[email protected]>
1 parent 0b7e86c commit 2a699c6

File tree

3 files changed

+234
-200
lines changed

3 files changed

+234
-200
lines changed

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 6 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
import java.util.Set;
4040
import java.util.stream.Collectors;
4141
import java.util.stream.Stream;
42-
import org.apache.calcite.plan.RelOptTable;
43-
import org.apache.calcite.plan.ViewExpanders;
4442
import org.apache.calcite.rel.RelNode;
4543
import org.apache.calcite.rel.core.Aggregate;
4644
import org.apache.calcite.rel.core.JoinRelType;
@@ -75,8 +73,6 @@
7573
import org.opensearch.sql.ast.dsl.AstDSL;
7674
import org.opensearch.sql.ast.expression.AggregateFunction;
7775
import org.opensearch.sql.ast.expression.Alias;
78-
import org.opensearch.sql.ast.expression.AllFields;
79-
import org.opensearch.sql.ast.expression.AllFieldsExcludeMeta;
8076
import org.opensearch.sql.ast.expression.Argument;
8177
import org.opensearch.sql.ast.expression.Argument.ArgumentMap;
8278
import org.opensearch.sql.ast.expression.Field;
@@ -133,14 +129,14 @@
133129
import org.opensearch.sql.ast.tree.UnresolvedPlan;
134130
import org.opensearch.sql.ast.tree.Values;
135131
import org.opensearch.sql.ast.tree.Window;
132+
import org.opensearch.sql.calcite.handlers.ProjectHandler;
133+
import org.opensearch.sql.calcite.handlers.ProjectionUtils;
136134
import org.opensearch.sql.calcite.plan.LogicalSystemLimit;
137135
import org.opensearch.sql.calcite.plan.LogicalSystemLimit.SystemLimitType;
138-
import org.opensearch.sql.calcite.plan.OpenSearchConstants;
139136
import org.opensearch.sql.calcite.utils.BinUtils;
140137
import org.opensearch.sql.calcite.utils.JoinAndLookupUtils;
141138
import org.opensearch.sql.calcite.utils.PlanUtils;
142139
import org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils;
143-
import org.opensearch.sql.calcite.utils.WildcardUtils;
144140
import org.opensearch.sql.common.patterns.PatternUtils;
145141
import org.opensearch.sql.common.utils.StringUtils;
146142
import org.opensearch.sql.datasource.DataSourceService;
@@ -157,11 +153,13 @@ public class CalciteRelNodeVisitor extends AbstractNodeVisitor<RelNode, CalciteP
157153
private final CalciteRexNodeVisitor rexVisitor;
158154
private final CalciteAggCallVisitor aggVisitor;
159155
private final DataSourceService dataSourceService;
156+
private final ProjectHandler projectHandler;
160157

161158
public CalciteRelNodeVisitor(DataSourceService dataSourceService) {
162159
this.rexVisitor = new CalciteRexNodeVisitor(this);
163160
this.aggVisitor = new CalciteAggCallVisitor(rexVisitor);
164161
this.dataSourceService = dataSourceService;
162+
this.projectHandler = new ProjectHandler(rexVisitor);
165163
}
166164

167165
public RelNode analyze(UnresolvedPlan unresolved, CalcitePlanContext context) {
@@ -189,17 +187,6 @@ public RelNode visitRelation(Relation node, CalcitePlanContext context) {
189187
return context.relBuilder.peek();
190188
}
191189

192-
// This is a tool method to add an existed RelOptTable to builder stack, not used for now
193-
private RelBuilder scan(RelOptTable tableSchema, CalcitePlanContext context) {
194-
final RelNode scan =
195-
context
196-
.relBuilder
197-
.getScanFactory()
198-
.createScan(ViewExpanders.simpleContext(context.relBuilder.getCluster()), tableSchema);
199-
context.relBuilder.push(scan);
200-
return context.relBuilder;
201-
}
202-
203190
@Override
204191
public RelNode visitSearch(Search node, CalcitePlanContext context) {
205192
// Visit the Relation child to get the scan
@@ -343,191 +330,10 @@ private boolean containsSubqueryExpression(Node expr) {
343330
@Override
344331
public RelNode visitProject(Project node, CalcitePlanContext context) {
345332
visitChildren(node, context);
346-
347-
if (isSingleAllFieldsProject(node)) {
348-
return handleAllFieldsProject(node, context);
349-
}
350-
351-
List<String> currentFields = context.relBuilder.peek().getRowType().getFieldNames();
352-
List<RexNode> expandedFields =
353-
expandProjectFields(node.getProjectList(), currentFields, context);
354-
355-
if (node.isExcluded()) {
356-
validateExclusion(expandedFields, currentFields);
357-
context.relBuilder.projectExcept(expandedFields);
358-
} else {
359-
if (!context.isResolvingSubquery()) {
360-
context.setProjectVisited(true);
361-
}
362-
context.relBuilder.project(expandedFields);
363-
}
364-
return context.relBuilder.peek();
365-
}
366-
367-
private boolean isSingleAllFieldsProject(Project node) {
368-
return node.getProjectList().size() == 1
369-
&& node.getProjectList().getFirst() instanceof AllFields;
370-
}
371-
372-
private RelNode handleAllFieldsProject(Project node, CalcitePlanContext context) {
373-
if (node.isExcluded()) {
374-
throw new IllegalArgumentException(
375-
"Invalid field exclusion: operation would exclude all fields from the result set");
376-
}
377-
AllFields allFields = (AllFields) node.getProjectList().getFirst();
378-
tryToRemoveNestedFields(context);
379-
tryToRemoveMetaFields(context, allFields instanceof AllFieldsExcludeMeta);
333+
projectHandler.handleProject(node, context);
380334
return context.relBuilder.peek();
381335
}
382336

383-
private List<RexNode> expandProjectFields(
384-
List<UnresolvedExpression> projectList,
385-
List<String> currentFields,
386-
CalcitePlanContext context) {
387-
List<RexNode> expandedFields = new ArrayList<>();
388-
Set<String> addedFields = new HashSet<>();
389-
390-
for (UnresolvedExpression expr : projectList) {
391-
switch (expr) {
392-
case Field field -> {
393-
String fieldName = field.getField().toString();
394-
if (WildcardUtils.containsWildcard(fieldName)) {
395-
List<String> matchingFields =
396-
WildcardUtils.expandWildcardPattern(fieldName, currentFields).stream()
397-
.filter(f -> !isMetadataField(f))
398-
.filter(addedFields::add)
399-
.toList();
400-
if (matchingFields.isEmpty()) {
401-
continue;
402-
}
403-
matchingFields.forEach(f -> expandedFields.add(context.relBuilder.field(f)));
404-
} else if (addedFields.add(fieldName)) {
405-
expandedFields.add(rexVisitor.analyze(field, context));
406-
}
407-
}
408-
case AllFields ignored -> {
409-
currentFields.stream()
410-
.filter(field -> !isMetadataField(field))
411-
.filter(addedFields::add)
412-
.forEach(field -> expandedFields.add(context.relBuilder.field(field)));
413-
}
414-
default -> throw new IllegalStateException(
415-
"Unexpected expression type in project list: " + expr.getClass().getSimpleName());
416-
}
417-
}
418-
419-
if (expandedFields.isEmpty()) {
420-
validateWildcardPatterns(projectList, currentFields);
421-
}
422-
423-
return expandedFields;
424-
}
425-
426-
private void validateExclusion(List<RexNode> fieldsToExclude, List<String> currentFields) {
427-
Set<String> nonMetaFields =
428-
currentFields.stream().filter(field -> !isMetadataField(field)).collect(Collectors.toSet());
429-
430-
if (fieldsToExclude.size() >= nonMetaFields.size()) {
431-
throw new IllegalArgumentException(
432-
"Invalid field exclusion: operation would exclude all fields from the result set");
433-
}
434-
}
435-
436-
private void validateWildcardPatterns(
437-
List<UnresolvedExpression> projectList, List<String> currentFields) {
438-
String firstWildcardPattern =
439-
projectList.stream()
440-
.filter(
441-
expr ->
442-
expr instanceof Field field
443-
&& WildcardUtils.containsWildcard(field.getField().toString()))
444-
.map(expr -> ((Field) expr).getField().toString())
445-
.findFirst()
446-
.orElse(null);
447-
448-
if (firstWildcardPattern != null) {
449-
throw new IllegalArgumentException(
450-
String.format("wildcard pattern [%s] matches no fields", firstWildcardPattern));
451-
}
452-
}
453-
454-
private boolean isMetadataField(String fieldName) {
455-
return OpenSearchConstants.METADATAFIELD_TYPE_MAP.containsKey(fieldName);
456-
}
457-
458-
/** See logic in {@link org.opensearch.sql.analysis.symbol.SymbolTable#lookupAllFields} */
459-
private static void tryToRemoveNestedFields(CalcitePlanContext context) {
460-
Set<String> allFields = new HashSet<>(context.relBuilder.peek().getRowType().getFieldNames());
461-
List<RexNode> duplicatedNestedFields =
462-
allFields.stream()
463-
.filter(
464-
field -> {
465-
int lastDot = field.lastIndexOf(".");
466-
return -1 != lastDot && allFields.contains(field.substring(0, lastDot));
467-
})
468-
.map(field -> (RexNode) context.relBuilder.field(field))
469-
.toList();
470-
if (!duplicatedNestedFields.isEmpty()) {
471-
// This is a workaround to avoid the bug in Calcite:
472-
// In {@link RelBuilder#project_(Iterable, Iterable, Iterable, boolean, Iterable)},
473-
// the check `RexUtil.isIdentity(nodeList, inputRowType)` will pass when the input
474-
// and the output nodeList refer to the same fields, even if the field name list
475-
// is different. As a result, renaming operation will not be applied. This makes
476-
// the logical plan for the flatten command incorrect, where the operation is
477-
// equivalent to renaming the flattened sub-fields. E.g. emp.name -> name.
478-
forceProjectExcept(context.relBuilder, duplicatedNestedFields);
479-
}
480-
}
481-
482-
/**
483-
* Project except with force.
484-
*
485-
* <p>This method is copied from {@link RelBuilder#projectExcept(Iterable)} and modified with the
486-
* force flag in project set to true. It is subject to future changes in Calcite.
487-
*
488-
* @param relBuilder RelBuilder
489-
* @param expressions Expressions to exclude from the project
490-
*/
491-
private static void forceProjectExcept(RelBuilder relBuilder, Iterable<RexNode> expressions) {
492-
List<RexNode> allExpressions = new ArrayList<>(relBuilder.fields());
493-
Set<RexNode> excludeExpressions = new HashSet<>();
494-
for (RexNode excludeExp : expressions) {
495-
if (!excludeExpressions.add(excludeExp)) {
496-
throw new IllegalArgumentException(
497-
"Input list contains duplicates. Expression " + excludeExp + " exists multiple times.");
498-
}
499-
if (!allExpressions.remove(excludeExp)) {
500-
throw new IllegalArgumentException("Expression " + excludeExp.toString() + " not found.");
501-
}
502-
}
503-
relBuilder.project(allExpressions, ImmutableList.of(), true);
504-
}
505-
506-
/**
507-
* Try to remove metadata fields in two cases:
508-
*
509-
* <p>1. It's explicitly specified excluding by force, usually for join or subquery.
510-
*
511-
* <p>2. There is no other project ever visited in the main query
512-
*
513-
* @param context CalcitePlanContext
514-
* @param excludeByForce whether exclude metadata fields by force
515-
*/
516-
private static void tryToRemoveMetaFields(CalcitePlanContext context, boolean excludeByForce) {
517-
if (excludeByForce || !context.isProjectVisited()) {
518-
List<String> originalFields = context.relBuilder.peek().getRowType().getFieldNames();
519-
List<RexNode> metaFieldsRef =
520-
originalFields.stream()
521-
.filter(OpenSearchConstants.METADATAFIELD_TYPE_MAP::containsKey)
522-
.map(metaField -> (RexNode) context.relBuilder.field(metaField))
523-
.toList();
524-
// Remove metadata fields if there is and ensure there are other fields.
525-
if (!metaFieldsRef.isEmpty() && metaFieldsRef.size() != originalFields.size()) {
526-
context.relBuilder.projectExcept(metaFieldsRef);
527-
}
528-
}
529-
}
530-
531337
@Override
532338
public RelNode visitRename(Rename node, CalcitePlanContext context) {
533339
visitChildren(node, context);
@@ -2594,7 +2400,7 @@ private void buildExpandRelNode(
25942400

25952401
if (alias != null) {
25962402
// Sub-nested fields cannot be removed after renaming the nested field.
2597-
tryToRemoveNestedFields(context);
2403+
ProjectionUtils.tryToRemoveNestedFields(context);
25982404
RexInputRef expandedField = context.relBuilder.field(arrayFieldName);
25992405
List<String> names = new ArrayList<>(context.relBuilder.peek().getRowType().getFieldNames());
26002406
names.set(expandedField.getIndex(), alias);

0 commit comments

Comments
 (0)