2525import org .elasticsearch .xpack .esql .plan .logical .Project ;
2626import org .elasticsearch .xpack .esql .plan .logical .Sample ;
2727import org .elasticsearch .xpack .esql .plan .logical .UnaryPlan ;
28+ import org .elasticsearch .xpack .esql .plan .logical .UnionAll ;
2829import org .elasticsearch .xpack .esql .plan .logical .join .InlineJoin ;
2930import org .elasticsearch .xpack .esql .plan .logical .local .LocalRelation ;
3031import org .elasticsearch .xpack .esql .plan .logical .local .LocalSupplier ;
3132import org .elasticsearch .xpack .esql .planner .PlannerUtils ;
3233import org .elasticsearch .xpack .esql .rule .Rule ;
3334
3435import java .util .ArrayList ;
36+ import java .util .HashSet ;
3537import java .util .List ;
38+ import java .util .Set ;
3639
3740import static org .elasticsearch .xpack .esql .optimizer .rules .logical .PruneEmptyPlans .skipPlan ;
3841
@@ -46,9 +49,8 @@ public LogicalPlan apply(LogicalPlan plan) {
4649 return pruneColumns (plan , plan .outputSet ().asBuilder (), false );
4750 }
4851
49- private static LogicalPlan pruneColumns (LogicalPlan plan , AttributeSet .Builder used , boolean inlineJoin ) {
52+ static LogicalPlan pruneColumns (LogicalPlan plan , AttributeSet .Builder used , boolean inlineJoin ) {
5053 Holder <Boolean > forkPresent = new Holder <>(false );
51-
5254 // while going top-to-bottom (upstream)
5355 return plan .transformDown (p -> {
5456 // Note: It is NOT required to do anything special for binary plans like JOINs, except INLINE STATS. It is perfectly fine that
@@ -58,17 +60,13 @@ private static LogicalPlan pruneColumns(LogicalPlan plan, AttributeSet.Builder u
5860 // same index fields will have different name ids in the left and right hand sides - as in the extreme example
5961 // `FROM lookup_idx | LOOKUP JOIN lookup_idx ON key_field`.
6062
61- // TODO: revisit with every new command
62- // skip nodes that simply pass the input through and use no references
63- if (p instanceof Limit || p instanceof Sample ) {
63+ if (forkPresent .get ()) {
6464 return p ;
6565 }
6666
67- if (p instanceof Fork ) {
68- forkPresent .set (true );
69- }
70- // pruning columns for Fork branches can have the side effect of having misaligned outputs
71- if (forkPresent .get ()) {
67+ // TODO: revisit with every new command
68+ // skip nodes that simply pass the input through and use no references
69+ if (p instanceof Limit || p instanceof Sample ) {
7270 return p ;
7371 }
7472
@@ -83,6 +81,10 @@ private static LogicalPlan pruneColumns(LogicalPlan plan, AttributeSet.Builder u
8381 case Eval eval -> pruneColumnsInEval (eval , used , recheck );
8482 case Project project -> inlineJoin ? pruneColumnsInProject (project , used ) : p ;
8583 case EsRelation esr -> pruneColumnsInEsRelation (esr , used );
84+ case Fork fork -> {
85+ forkPresent .set (true );
86+ yield pruneColumnsInFork (fork , used );
87+ }
8688 default -> p ;
8789 };
8890 } while (recheck .get ());
@@ -94,7 +96,7 @@ private static LogicalPlan pruneColumns(LogicalPlan plan, AttributeSet.Builder u
9496 });
9597 }
9698
97- private static LogicalPlan pruneColumnsInAggregate (Aggregate aggregate , AttributeSet .Builder used , boolean inlineJoin ) {
99+ static LogicalPlan pruneColumnsInAggregate (Aggregate aggregate , AttributeSet .Builder used , boolean inlineJoin ) {
98100 LogicalPlan p = aggregate ;
99101
100102 var remaining = pruneUnusedAndAddReferences (aggregate .aggregates (), used );
@@ -134,7 +136,7 @@ private static LogicalPlan pruneColumnsInAggregate(Aggregate aggregate, Attribut
134136 return p ;
135137 }
136138
137- private static LogicalPlan pruneColumnsInInlineJoinRight (InlineJoin ij , AttributeSet .Builder used , Holder <Boolean > recheck ) {
139+ static LogicalPlan pruneColumnsInInlineJoinRight (InlineJoin ij , AttributeSet .Builder used , Holder <Boolean > recheck ) {
138140 LogicalPlan p = ij ;
139141
140142 used .addAll (ij .references ());
@@ -155,7 +157,7 @@ private static LogicalPlan pruneColumnsInInlineJoinRight(InlineJoin ij, Attribut
155157 return p ;
156158 }
157159
158- private static LogicalPlan pruneColumnsInEval (Eval eval , AttributeSet .Builder used , Holder <Boolean > recheck ) {
160+ static LogicalPlan pruneColumnsInEval (Eval eval , AttributeSet .Builder used , Holder <Boolean > recheck ) {
159161 LogicalPlan p = eval ;
160162
161163 var remaining = pruneUnusedAndAddReferences (eval .fields (), used );
@@ -173,7 +175,7 @@ private static LogicalPlan pruneColumnsInEval(Eval eval, AttributeSet.Builder us
173175 }
174176
175177 // Note: only run when the Project is a descendent of an InlineJoin.
176- private static LogicalPlan pruneColumnsInProject (Project project , AttributeSet .Builder used ) {
178+ static LogicalPlan pruneColumnsInProject (Project project , AttributeSet .Builder used ) {
177179 LogicalPlan p = project ;
178180
179181 var remaining = pruneUnusedAndAddReferences (project .projections (), used );
@@ -184,7 +186,7 @@ private static LogicalPlan pruneColumnsInProject(Project project, AttributeSet.B
184186 return p ;
185187 }
186188
187- private static LogicalPlan pruneColumnsInEsRelation (EsRelation esr , AttributeSet .Builder used ) {
189+ static LogicalPlan pruneColumnsInEsRelation (EsRelation esr , AttributeSet .Builder used ) {
188190 LogicalPlan p = esr ;
189191
190192 if (esr .indexMode () == IndexMode .LOOKUP ) {
@@ -200,6 +202,36 @@ private static LogicalPlan pruneColumnsInEsRelation(EsRelation esr, AttributeSet
200202 return p ;
201203 }
202204
205+ private static LogicalPlan pruneColumnsInFork (Fork fork , AttributeSet .Builder used ) {
206+ // prune the output attributes of fork based on usage from the rest of the plan
207+ // this does not consider the inner usage within each branch of the fork
208+ // as those will be handled when traversing down each branch in PruneColumnsInForkBranches
209+ LogicalPlan p = fork ;
210+
211+ // should exit early for UnionAll
212+ if (fork instanceof UnionAll ) {
213+ return p ;
214+ }
215+ boolean changed = false ;
216+ AttributeSet .Builder builder = AttributeSet .builder ();
217+ // if any of the fork outputs are used, keep them
218+ // otherwise, prune them based on the rest of the plan's usage
219+ Set <String > names = new HashSet <>(used .build ().names ());
220+ for (var attr : fork .output ()) {
221+ // we should also ensure to keep any synthetic attributes around as those could still be used for internal processing
222+ if (attr .synthetic () || names .contains (attr .name ())) {
223+ builder .add (attr );
224+ } else {
225+ changed = true ;
226+ }
227+ }
228+ if (changed ) {
229+ List <Attribute > attrs = builder .build ().stream ().toList ();
230+ p = new Fork (fork .source (), fork .children (), attrs );
231+ }
232+ return p ;
233+ }
234+
203235 private static LogicalPlan emptyLocalRelation (UnaryPlan plan ) {
204236 // create an empty local relation with no attributes
205237 return skipPlan (plan );
0 commit comments