3939import java .util .Set ;
4040import java .util .stream .Collectors ;
4141import java .util .stream .Stream ;
42- import org .apache .calcite .plan .RelOptTable ;
43- import org .apache .calcite .plan .ViewExpanders ;
4442import org .apache .calcite .rel .RelNode ;
4543import org .apache .calcite .rel .core .Aggregate ;
4644import org .apache .calcite .rel .core .JoinRelType ;
7573import org .opensearch .sql .ast .dsl .AstDSL ;
7674import org .opensearch .sql .ast .expression .AggregateFunction ;
7775import org .opensearch .sql .ast .expression .Alias ;
78- import org .opensearch .sql .ast .expression .AllFields ;
79- import org .opensearch .sql .ast .expression .AllFieldsExcludeMeta ;
8076import org .opensearch .sql .ast .expression .Argument ;
8177import org .opensearch .sql .ast .expression .Argument .ArgumentMap ;
8278import org .opensearch .sql .ast .expression .Field ;
133129import org .opensearch .sql .ast .tree .UnresolvedPlan ;
134130import org .opensearch .sql .ast .tree .Values ;
135131import org .opensearch .sql .ast .tree .Window ;
132+ import org .opensearch .sql .calcite .handlers .ProjectHandler ;
133+ import org .opensearch .sql .calcite .handlers .ProjectionUtils ;
136134import org .opensearch .sql .calcite .plan .LogicalSystemLimit ;
137135import org .opensearch .sql .calcite .plan .LogicalSystemLimit .SystemLimitType ;
138- import org .opensearch .sql .calcite .plan .OpenSearchConstants ;
139136import org .opensearch .sql .calcite .utils .BinUtils ;
140137import org .opensearch .sql .calcite .utils .JoinAndLookupUtils ;
141138import org .opensearch .sql .calcite .utils .PlanUtils ;
142139import org .opensearch .sql .calcite .utils .UserDefinedFunctionUtils ;
143- import org .opensearch .sql .calcite .utils .WildcardUtils ;
144140import org .opensearch .sql .common .patterns .PatternUtils ;
145141import org .opensearch .sql .common .utils .StringUtils ;
146142import 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