5656import org .elasticsearch .xpack .esql .parser .QueryParams ;
5757import org .elasticsearch .xpack .esql .plan .IndexPattern ;
5858import org .elasticsearch .xpack .esql .plan .logical .Aggregate ;
59+ import org .elasticsearch .xpack .esql .plan .logical .Drop ;
5960import org .elasticsearch .xpack .esql .plan .logical .Enrich ;
61+ import org .elasticsearch .xpack .esql .plan .logical .Eval ;
62+ import org .elasticsearch .xpack .esql .plan .logical .Filter ;
63+ import org .elasticsearch .xpack .esql .plan .logical .InlineStats ;
6064import org .elasticsearch .xpack .esql .plan .logical .Keep ;
65+ import org .elasticsearch .xpack .esql .plan .logical .Limit ;
6166import org .elasticsearch .xpack .esql .plan .logical .LogicalPlan ;
67+ import org .elasticsearch .xpack .esql .plan .logical .MvExpand ;
68+ import org .elasticsearch .xpack .esql .plan .logical .OrderBy ;
6269import org .elasticsearch .xpack .esql .plan .logical .Project ;
6370import org .elasticsearch .xpack .esql .plan .logical .RegexExtract ;
71+ import org .elasticsearch .xpack .esql .plan .logical .Rename ;
72+ import org .elasticsearch .xpack .esql .plan .logical .TopN ;
6473import org .elasticsearch .xpack .esql .plan .logical .UnresolvedRelation ;
6574import org .elasticsearch .xpack .esql .plan .logical .join .InlineJoin ;
6675import org .elasticsearch .xpack .esql .plan .logical .join .JoinTypes ;
@@ -443,6 +452,7 @@ private void preAnalyzeIndices(
443452
444453 /**
445454 * Check if there are any clusters to search.
455+ *
446456 * @return true if there are no clusters to search, false otherwise
447457 */
448458 private boolean allCCSClustersSkipped (
@@ -542,6 +552,8 @@ static PreAnalysisResult fieldNames(LogicalPlan parsed, Set<String> enrichPolicy
542552 var keepJoinRefsBuilder = AttributeSet .builder ();
543553 Set <String > wildcardJoinIndices = new java .util .HashSet <>();
544554
555+ boolean [] canRemoveAliases = new boolean [] { true };
556+
545557 parsed .forEachDown (p -> {// go over each plan top-down
546558 if (p instanceof RegexExtract re ) { // for Grok and Dissect
547559 // remove other down-the-tree references to the extracted fields
@@ -587,20 +599,37 @@ static PreAnalysisResult fieldNames(LogicalPlan parsed, Set<String> enrichPolicy
587599 }
588600 }
589601
590- // remove any already discovered UnresolvedAttributes that are in fact aliases defined later down in the tree
591- // for example "from test | eval x = salary | stats max = max(x) by gender"
592- // remove the UnresolvedAttribute "x", since that is an Alias defined in "eval"
593- AttributeSet planRefs = p .references ();
594- Set <String > fieldNames = planRefs .names ();
595- p .forEachExpressionDown (Alias .class , alias -> {
596- // do not remove the UnresolvedAttribute that has the same name as its alias, ie "rename id = id"
597- // or the UnresolvedAttributes that are used in Functions that have aliases "STATS id = MAX(id)"
598- if (fieldNames .contains (alias .name ())) {
599- return ;
600- }
601- referencesBuilder .removeIf (attr -> matchByName (attr , alias .name (), keepCommandRefsBuilder .contains (attr )));
602- });
602+ // If the current node in the tree is of type JOIN (lookup join, inlinestats) or ENRICH or other type of
603+ // command that we may add in the future which can override already defined Aliases with EVAL
604+ // (for example
605+ //
606+ // from test
607+ // | eval ip = 123
608+ // | enrich ips_policy ON hostname
609+ // | rename ip AS my_ip
610+ //
611+ // and ips_policy enriches the results with the same name ip field),
612+ // these aliases should be kept in the list of fields.
613+ if (canRemoveAliases [0 ] && couldOverrideAliases (p )) {
614+ canRemoveAliases [0 ] = false ;
615+ }
616+ if (canRemoveAliases [0 ]) {
617+ // remove any already discovered UnresolvedAttributes that are in fact aliases defined later down in the tree
618+ // for example "from test | eval x = salary | stats max = max(x) by gender"
619+ // remove the UnresolvedAttribute "x", since that is an Alias defined in "eval"
620+ AttributeSet planRefs = p .references ();
621+ Set <String > fieldNames = planRefs .names ();
622+ p .forEachExpressionDown (Alias .class , alias -> {
623+ // do not remove the UnresolvedAttribute that has the same name as its alias, ie "rename id AS id"
624+ // or the UnresolvedAttributes that are used in Functions that have aliases "STATS id = MAX(id)"
625+ if (fieldNames .contains (alias .name ())) {
626+ return ;
627+ }
628+ referencesBuilder .removeIf (attr -> matchByName (attr , alias .name (), keepCommandRefsBuilder .contains (attr )));
629+ });
630+ }
603631 });
632+
604633 // Add JOIN ON column references afterward to avoid Alias removal
605634 referencesBuilder .addAll (keepJoinRefsBuilder );
606635 // If any JOIN commands need wildcard field-caps calls, persist the index names
@@ -624,6 +653,29 @@ static PreAnalysisResult fieldNames(LogicalPlan parsed, Set<String> enrichPolicy
624653 }
625654 }
626655
656+ /**
657+ * Could a plan "accidentally" override aliases?
658+ * Examples are JOIN and ENRICH, that _could_ produce fields with the same
659+ * name of an existing alias, based on their index mapping.
660+ * Here we just have to consider commands where this information is not available before index resolution,
661+ * eg. EVAL, GROK, DISSECT can override an alias, but we know it in advance, ie. we don't need to resolve indices to know.
662+ */
663+ private static boolean couldOverrideAliases (LogicalPlan p ) {
664+ return (p instanceof Aggregate
665+ || p instanceof Drop
666+ || p instanceof Eval
667+ || p instanceof Filter
668+ || p instanceof InlineStats
669+ || p instanceof Keep
670+ || p instanceof Limit
671+ || p instanceof MvExpand
672+ || p instanceof OrderBy
673+ || p instanceof Project
674+ || p instanceof RegexExtract
675+ || p instanceof Rename
676+ || p instanceof TopN ) == false ;
677+ }
678+
627679 private static boolean matchByName (Attribute attr , String other , boolean skipIfPattern ) {
628680 boolean isPattern = Regex .isSimpleMatchPattern (attr .name ());
629681 if (skipIfPattern && isPattern ) {
0 commit comments