Skip to content

Commit ecb06ef

Browse files
authored
ESQL: Fix missing refs due to pruning renamed grouping columns (#107328) (#107599)
Sometimes, CombineProjections does not correctly update an aggregation's groupings when combining with a preceding projection. Fix this by resolving any aliases used in the groupings and de-duplicating them. --------- Co-authored-by: Andrei Stefan <[email protected]> (cherry picked from commit adaa476) # Conflicts: # x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec # x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java
1 parent 8de6451 commit ecb06ef

File tree

4 files changed

+103
-18
lines changed

4 files changed

+103
-18
lines changed

docs/changelog/107328.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pr: 107328
2+
summary: "ESQL: Fix missing refs due to pruning renamed grouping columns"
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 107083
7+
- 107166

x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,3 +1349,20 @@ c:l | k1:i | languages:i
13491349
21 | 5 | 5
13501350
10 | null | null
13511351
;
1352+
1353+
evalMultipleOverridingKeysWithAggregateExpr#[skip:-8.13.2,reason:supported in 8.13.3]
1354+
FROM employees
1355+
| EVAL k = languages, k1 = k
1356+
| STATS c = 3*COUNT() BY languages, k, k1, languages
1357+
| DROP k
1358+
| SORT languages
1359+
;
1360+
1361+
c:l | k1:i | languages:i
1362+
45 | 1 | 1
1363+
57 | 2 | 2
1364+
51 | 3 | 3
1365+
54 | 4 | 4
1366+
63 | 5 | 5
1367+
30 | null | null
1368+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ static class CombineProjections extends OptimizerRules.OptimizerRule<UnaryPlan>
318318
}
319319

320320
@Override
321+
@SuppressWarnings("unchecked")
321322
protected LogicalPlan rule(UnaryPlan plan) {
322323
LogicalPlan child = plan.child();
323324

@@ -348,7 +349,22 @@ protected LogicalPlan rule(UnaryPlan plan) {
348349
// Agg with underlying Project (group by on sub-queries)
349350
if (plan instanceof Aggregate a) {
350351
if (child instanceof Project p) {
351-
plan = new Aggregate(a.source(), p.child(), a.groupings(), combineProjections(a.aggregates(), p.projections()));
352+
var groupings = a.groupings();
353+
List<Attribute> groupingAttrs = new ArrayList<>(a.groupings().size());
354+
for (Expression grouping : groupings) {
355+
if (grouping instanceof Attribute attribute) {
356+
groupingAttrs.add(attribute);
357+
} else {
358+
// After applying ReplaceStatsNestedExpressionWithEval, groupings can only contain attributes.
359+
throw new EsqlIllegalArgumentException("Expected an Attribute, got {}", grouping);
360+
}
361+
}
362+
plan = new Aggregate(
363+
a.source(),
364+
p.child(),
365+
combineUpperGroupingsAndLowerProjections(groupingAttrs, p.projections()),
366+
combineProjections(a.aggregates(), p.projections())
367+
);
352368
}
353369
}
354370

@@ -411,6 +427,26 @@ private static List<NamedExpression> combineProjections(
411427
return replaced;
412428
}
413429

430+
private static List<Expression> combineUpperGroupingsAndLowerProjections(
431+
List<? extends Attribute> upperGroupings,
432+
List<? extends NamedExpression> lowerProjections
433+
) {
434+
// Collect the alias map for resolving the source (f1 = 1, f2 = f1, etc..)
435+
AttributeMap<Attribute> aliases = new AttributeMap<>();
436+
for (NamedExpression ne : lowerProjections) {
437+
// Projections are just aliases for attributes, so casting is safe.
438+
aliases.put(ne.toAttribute(), (Attribute) Alias.unwrap(ne));
439+
}
440+
441+
// Replace any matching attribute directly with the aliased attribute from the projection.
442+
AttributeSet replaced = new AttributeSet();
443+
for (Attribute attr : upperGroupings) {
444+
// All substitutions happen before; groupings must be attributes at this point.
445+
replaced.add(aliases.resolve(attr, attr));
446+
}
447+
return new ArrayList<>(replaced);
448+
}
449+
414450
/**
415451
* Replace grouping alias previously contained in the aggregations that might have been projected away.
416452
*/

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,8 @@ public void testCombineProjectionWithAggregation() {
397397
/**
398398
* Expects
399399
* Limit[1000[INTEGER]]
400-
* \_Aggregate[[last_name{f}#23, first_name{f}#20, k{r}#4],[SUM(salary{f}#24) AS s, last_name{f}#23, first_name{f}#20, first_n
401-
* ame{f}#20 AS k]]
400+
* \_Aggregate[[last_name{f}#23, first_name{f}#20],[SUM(salary{f}#24) AS s, last_name{f}#23, first_name{f}#20, first_name{f}#2
401+
* 0 AS k]]
402402
* \_EsRelation[test][_meta_field{f}#25, emp_no{f}#19, first_name{f}#20, ..]
403403
*/
404404
public void testCombineProjectionWithAggregationAndEval() {
@@ -412,7 +412,7 @@ public void testCombineProjectionWithAggregationAndEval() {
412412
var limit = as(plan, Limit.class);
413413
var agg = as(limit.child(), Aggregate.class);
414414
assertThat(Expressions.names(agg.aggregates()), contains("s", "last_name", "first_name", "k"));
415-
assertThat(Expressions.names(agg.groupings()), contains("last_name", "first_name", "k"));
415+
assertThat(Expressions.names(agg.groupings()), contains("last_name", "first_name"));
416416
}
417417

418418
/**
@@ -454,6 +454,12 @@ public void testQlComparisonOptimizationsApply() {
454454
assertThat(con.value(), equalTo(5));
455455
}
456456

457+
/**
458+
* Expects
459+
* Limit[1000[INTEGER]]
460+
* \_Aggregate[[first_name{f}#12],[COUNT(salary{f}#16) AS count(salary), first_name{f}#12 AS x]]
461+
* \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..]
462+
*/
457463
public void testCombineProjectionWithPruning() {
458464
var plan = plan("""
459465
from test
@@ -465,19 +471,17 @@ public void testCombineProjectionWithPruning() {
465471
var limit = as(plan, Limit.class);
466472
var agg = as(limit.child(), Aggregate.class);
467473
assertThat(Expressions.names(agg.aggregates()), contains("count(salary)", "x"));
468-
assertThat(Expressions.names(agg.groupings()), contains("x"));
474+
assertThat(Expressions.names(agg.groupings()), contains("first_name"));
469475
var alias = as(agg.aggregates().get(1), Alias.class);
470476
var field = as(alias.child(), FieldAttribute.class);
471477
assertThat(field.name(), is("first_name"));
472-
var group = as(agg.groupings().get(0), Attribute.class);
473-
assertThat(group, is(alias.toAttribute()));
474478
var from = as(agg.child(), EsRelation.class);
475479
}
476480

477481
/**
478482
* Expects
479483
* Limit[1000[INTEGER]]
480-
* \_Aggregate[[f{r}#7],[SUM(emp_no{f}#15) AS s, COUNT(first_name{f}#16) AS c, first_name{f}#16 AS f]]
484+
* \_Aggregate[[first_name{f}#16],[SUM(emp_no{f}#15) AS s, COUNT(first_name{f}#16) AS c, first_name{f}#16 AS f]]
481485
* \_EsRelation[test][_meta_field{f}#21, emp_no{f}#15, first_name{f}#16, ..]
482486
*/
483487
public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUsedInAgg() {
@@ -501,13 +505,13 @@ public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUsedInAgg
501505
as = as(aggs.get(2), Alias.class);
502506
assertThat(Expressions.name(as.child()), is("first_name"));
503507

504-
assertThat(Expressions.names(agg.groupings()), contains("f"));
508+
assertThat(Expressions.names(agg.groupings()), contains("first_name"));
505509
}
506510

507511
/**
508512
* Expects
509513
* Limit[1000[INTEGER]]
510-
* \_Aggregate[[f{r}#7],[SUM(emp_no{f}#15) AS s, first_name{f}#16 AS f]]
514+
* \_Aggregate[[first_name{f}#16],[SUM(emp_no{f}#15) AS s, first_name{f}#16 AS f]]
511515
* \_EsRelation[test][_meta_field{f}#21, emp_no{f}#15, first_name{f}#16, ..]
512516
*/
513517
public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUnused() {
@@ -527,7 +531,7 @@ public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUnused()
527531
as = as(aggs.get(1), Alias.class);
528532
assertThat(Expressions.name(as.child()), is("first_name"));
529533

530-
assertThat(Expressions.names(agg.groupings()), contains("f"));
534+
assertThat(Expressions.names(agg.groupings()), contains("first_name"));
531535
}
532536

533537
/**
@@ -2688,6 +2692,27 @@ public void testEliminateDuplicateAggsNonCount() {
26882692
var source = as(agg.child(), EsRelation.class);
26892693
}
26902694

2695+
/**
2696+
* Expects
2697+
* Limit[1000[INTEGER]]
2698+
* \_Aggregate[[salary{f}#12],[salary{f}#12, salary{f}#12 AS x]]
2699+
* \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..]
2700+
*/
2701+
public void testEliminateDuplicateRenamedGroupings() {
2702+
var plan = plan("""
2703+
from test
2704+
| eval x = salary
2705+
| stats by salary, x
2706+
""");
2707+
2708+
var limit = as(plan, Limit.class);
2709+
var agg = as(limit.child(), Aggregate.class);
2710+
var relation = as(agg.child(), EsRelation.class);
2711+
2712+
assertThat(Expressions.names(agg.groupings()), contains("salary"));
2713+
assertThat(Expressions.names(agg.aggregates()), contains("salary", "x"));
2714+
}
2715+
26912716
/**
26922717
* Expected
26932718
* Limit[2[INTEGER]]
@@ -2734,7 +2759,7 @@ public void testRenameStatsDropGroup() {
27342759
/**
27352760
* Expected
27362761
* Limit[1000[INTEGER]]
2737-
* \_Aggregate[[a{r}#2, bar{r}#8],[COUNT([2a][KEYWORD]) AS baz, b{r}#4 AS bar]]
2762+
* \_Aggregate[[a{r}#3, b{r}#5],[COUNT([2a][KEYWORD]) AS baz, b{r}#5 AS bar]]
27382763
* \_Row[[1[INTEGER] AS a, 2[INTEGER] AS b]]
27392764
*/
27402765
public void testMultipleRenameStatsDropGroup() {
@@ -2746,15 +2771,15 @@ public void testMultipleRenameStatsDropGroup() {
27462771

27472772
var limit = as(plan, Limit.class);
27482773
var agg = as(limit.child(), Aggregate.class);
2749-
assertThat(Expressions.names(agg.groupings()), contains("a", "bar"));
2774+
assertThat(Expressions.names(agg.groupings()), contains("a", "b"));
27502775
var row = as(agg.child(), Row.class);
27512776
}
27522777

27532778
/**
27542779
* Expected
27552780
* Limit[1000[INTEGER]]
2756-
* \_Aggregate[[emp_no{f}#11, bar{r}#4],[MAX(salary{f}#16) AS baz, gender{f}#13 AS bar]]
2757-
* \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..]
2781+
* \_Aggregate[[emp_no{f}#14, gender{f}#16],[MAX(salary{f}#19) AS baz, gender{f}#16 AS bar]]
2782+
* \_EsRelation[test][_meta_field{f}#20, emp_no{f}#14, first_name{f}#15, ..]
27582783
*/
27592784
public void testMultipleRenameStatsDropGroupMultirow() {
27602785
LogicalPlan plan = optimizedPlan("""
@@ -2765,7 +2790,7 @@ public void testMultipleRenameStatsDropGroupMultirow() {
27652790

27662791
var limit = as(plan, Limit.class);
27672792
var agg = as(limit.child(), Aggregate.class);
2768-
assertThat(Expressions.names(agg.groupings()), contains("emp_no", "bar"));
2793+
assertThat(Expressions.names(agg.groupings()), contains("emp_no", "gender"));
27692794
var row = as(agg.child(), EsRelation.class);
27702795
}
27712796

@@ -2846,7 +2871,7 @@ public void testIsNotNullConstraintForStatsWithAndOnGrouping() {
28462871
/**
28472872
* Expects
28482873
* Limit[1000[INTEGER]]
2849-
* \_Aggregate[[x{r}#4],[SUM(salary{f}#13) AS sum(salary), salary{f}#13 AS x]]
2874+
* \_Aggregate[[salary{f}#13],[SUM(salary{f}#13) AS sum(salary), salary{f}#13 AS x]]
28502875
* \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..]
28512876
*/
28522877
public void testIsNotNullConstraintForStatsWithAndOnGroupingAlias() {
@@ -2858,7 +2883,7 @@ public void testIsNotNullConstraintForStatsWithAndOnGroupingAlias() {
28582883

28592884
var limit = as(plan, Limit.class);
28602885
var agg = as(limit.child(), Aggregate.class);
2861-
assertThat(Expressions.names(agg.groupings()), contains("x"));
2886+
assertThat(Expressions.names(agg.groupings()), contains("salary"));
28622887
assertThat(Expressions.names(agg.aggregates()), contains("sum(salary)", "x"));
28632888
var from = as(agg.child(), EsRelation.class);
28642889
}

0 commit comments

Comments
 (0)