1414import org .elasticsearch .xpack .esql .core .expression .Attribute ;
1515import org .elasticsearch .xpack .esql .core .expression .AttributeSet ;
1616import org .elasticsearch .xpack .esql .core .expression .Expressions ;
17- import org .elasticsearch .xpack .esql .core .expression .FieldAttribute ;
1817import org .elasticsearch .xpack .esql .core .expression .NamedExpression ;
1918import org .elasticsearch .xpack .esql .core .util .Holder ;
2019import org .elasticsearch .xpack .esql .plan .logical .Aggregate ;
2625import org .elasticsearch .xpack .esql .plan .logical .Project ;
2726import org .elasticsearch .xpack .esql .plan .logical .Sample ;
2827import org .elasticsearch .xpack .esql .plan .logical .join .InlineJoin ;
29- import org .elasticsearch .xpack .esql .plan .logical .join .StubRelation ;
3028import org .elasticsearch .xpack .esql .plan .logical .local .EmptyLocalSupplier ;
3129import org .elasticsearch .xpack .esql .plan .logical .local .LocalRelation ;
3230import org .elasticsearch .xpack .esql .plan .logical .local .LocalSupplier ;
@@ -79,7 +77,7 @@ private static LogicalPlan pruneColumns(LogicalPlan plan, AttributeSet.Builder u
7977 recheck .set (false );
8078 p = switch (p ) {
8179 case Aggregate agg -> pruneColumnsInAggregate (agg , used , inlineJoin );
82- case InlineJoin inj -> pruneColumnsInInlineJoin (inj , used , recheck );
80+ case InlineJoin inj -> pruneColumnsInInlineJoinRight (inj , used , recheck );
8381 case Eval eval -> pruneColumnsInEval (eval , used , recheck );
8482 case Project project -> inlineJoin ? pruneColumnsInProject (project , used ) : p ;
8583 case EsRelation esr -> pruneColumnsInEsRelation (esr , used );
@@ -123,23 +121,9 @@ private static LogicalPlan pruneColumnsInAggregate(Aggregate aggregate, Attribut
123121 } else {
124122 // not expecting high groups cardinality, nested loops in lists should be fine, no need for a HashSet
125123 if (inlineJoin && aggregate .groupings ().containsAll (remaining )) {
126- // It's an INLINEJOIN and all remaining attributes are groupings, which are already part of the IJ output (from the
127- // left-hand side).
128- // TODO: INLINESTATS: revisit condition when adding support for INLINESTATS filters
129- if (aggregate .child () instanceof StubRelation stub ) {
130- var message = "Aggregate groups references ["
131- + remaining
132- + "] not in child's (StubRelation) output: ["
133- + stub .outputSet ()
134- + "]" ;
135- assert stub .outputSet ().containsAll (Expressions .asAttributes (remaining )) : message ;
136-
137- p = emptyLocalRelation (aggregate );
138- } else {
139- // There are no aggregates to compute, just output the groupings; these are already in the IJ output, so only
140- // restrict the output to what remained.
141- p = new Project (aggregate .source (), aggregate .child (), remaining );
142- }
124+ // An INLINEJOIN right-hand side aggregation output had everything pruned, except for (some of the) groupings, which are
125+ // already part of the IJ output (from the left-hand side): the agg can just be dropped entirely.
126+ p = emptyLocalRelation (aggregate );
143127 } else { // not an INLINEJOIN or there are actually aggregates to compute
144128 p = aggregate .with (aggregate .groupings (), remaining );
145129 }
@@ -148,12 +132,12 @@ private static LogicalPlan pruneColumnsInAggregate(Aggregate aggregate, Attribut
148132 return p ;
149133 }
150134
151- private static LogicalPlan pruneColumnsInInlineJoin (InlineJoin ij , AttributeSet .Builder used , Holder <Boolean > recheck ) {
135+ private static LogicalPlan pruneColumnsInInlineJoinRight (InlineJoin ij , AttributeSet .Builder used , Holder <Boolean > recheck ) {
152136 LogicalPlan p = ij ;
153137
154138 used .addAll (ij .references ());
155139 var right = pruneColumns (ij .right (), used , true );
156- if (right .output ().isEmpty ()) {
140+ if (right .output ().isEmpty () || isLocalEmptyRelation ( right ) ) {
157141 p = ij .left ();
158142 recheck .set (true );
159143 } else if (right != ij .right ()) {
@@ -181,18 +165,13 @@ private static LogicalPlan pruneColumnsInEval(Eval eval, AttributeSet.Builder us
181165 return p ;
182166 }
183167
168+ // Note: only run when the Project is a descendent of an InlineJoin.
184169 private static LogicalPlan pruneColumnsInProject (Project project , AttributeSet .Builder used ) {
185170 LogicalPlan p = project ;
186171
187172 var remaining = pruneUnusedAndAddReferences (project .projections (), used );
188173 if (remaining != null ) {
189- p = remaining .isEmpty () || remaining .stream ().allMatch (FieldAttribute .class ::isInstance )
190- ? emptyLocalRelation (project )
191- : new Project (project .source (), project .child (), remaining );
192- } else if (project .output ().stream ().allMatch (FieldAttribute .class ::isInstance )) {
193- // Use empty relation as a marker for a subsequent pass, in case the project is only outputting field attributes (which are
194- // already part of the INLINEJOIN left-hand side output).
195- p = emptyLocalRelation (project );
174+ p = remaining .isEmpty () ? emptyLocalRelation (project ) : new Project (project .source (), project .child (), remaining );
196175 }
197176
198177 return p ;
@@ -216,7 +195,11 @@ private static LogicalPlan pruneColumnsInEsRelation(EsRelation esr, AttributeSet
216195
217196 private static LogicalPlan emptyLocalRelation (LogicalPlan plan ) {
218197 // create an empty local relation with no attributes
219- return new LocalRelation (plan .source (), List .of (), EmptyLocalSupplier .EMPTY );
198+ return new LocalRelation (plan .source (), plan .output (), EmptyLocalSupplier .EMPTY );
199+ }
200+
201+ private static boolean isLocalEmptyRelation (LogicalPlan plan ) {
202+ return plan instanceof LocalRelation local && local .hasEmptySupplier ();
220203 }
221204
222205 /**
0 commit comments