Skip to content

Commit d08a6ea

Browse files
committed
refactor(esql): Improve MvExpand push-down logic in logical optimizer
- Enhance PushDownMvExpandPastProject rule to handle more complex query scenarios - Add checks to prevent duplicate output attributes when pushing down MvExpand - Support pushing down MvExpand for queries with aliased expressions - Handle cases where target is an alias or referenced in an alias - Add test case to verify complex MvExpand push-down behavior Resolves edge cases in multi-stage query optimization where MvExpand interactions with Project and Eval nodes were previously limited.
1 parent 471124c commit d08a6ea

File tree

2 files changed

+46
-16
lines changed

2 files changed

+46
-16
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownMvExpandPastProject.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,54 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.List;
20+
import java.util.Set;
21+
import java.util.stream.Collectors;
2022

2123
public final class PushDownMvExpandPastProject extends OptimizerRules.OptimizerRule<MvExpand> {
2224
@Override
2325
protected LogicalPlan rule(MvExpand mvExpand) {
2426
if (mvExpand.child() instanceof Project pj) {
2527
List<NamedExpression> projections = new ArrayList<>(pj.projections());
2628

29+
// Skip if any projection alias shadows an input field name, as injecting an Eval would cause
30+
// duplicate output attributes. This can happen with aliases generated by ResolveUnionTypesInUnionAll.
31+
// Example:
32+
// MvExpand[salary{r}#168,salary{r}#175]
33+
// \_Project[[$$salary$converted_to$keyword{r$}#178 AS salary#168]]
34+
// \_Limit[1000[INTEGER],false,false]
35+
// \_UnionAll[[salary{r}#174, $$salary$converted_to$keyword{r$}#178]]
36+
Set<String> inputNames = pj.inputSet().stream().map(NamedExpression::name).collect(Collectors.toSet());
37+
if (projections.stream().anyMatch(e -> e instanceof Alias alias && inputNames.contains(alias.toAttribute().name()))) {
38+
return mvExpand;
39+
}
40+
2741
// Find if the target is aliased in the project and create an alias with temporary names for it.
2842
for (int i = 0; i < projections.size(); i++) {
2943
NamedExpression projection = projections.get(i);
30-
if (projection instanceof Alias alias && alias.toAttribute().equals(mvExpand.target().toAttribute())) {
31-
Alias aliasAlias = new Alias(
32-
alias.source(),
33-
TemporaryNameUtils.temporaryName(alias.child(), alias.toAttribute(), 0),
34-
alias.child()
35-
);
36-
projections.set(i, mvExpand.expanded());
37-
pj = new Project(pj.source(), new Eval(aliasAlias.source(), pj.child(), List.of(aliasAlias)), projections);
38-
mvExpand = new MvExpand(mvExpand.source(), pj, aliasAlias.toAttribute(), mvExpand.expanded());
39-
break;
44+
if (projection instanceof Alias alias) {
45+
if (alias.toAttribute().semanticEquals(mvExpand.target().toAttribute())) {
46+
// for query like: row a = 2 | eval b = a | keep * | mv_expand b
47+
Alias aliasAlias = new Alias(
48+
alias.source(),
49+
TemporaryNameUtils.temporaryName(alias.child(), alias.toAttribute(), 0),
50+
alias.child()
51+
);
52+
projections.set(i, mvExpand.expanded());
53+
pj = new Project(pj.source(), new Eval(aliasAlias.source(), pj.child(), List.of(aliasAlias)), projections);
54+
mvExpand = new MvExpand(mvExpand.source(), pj, aliasAlias.toAttribute(), mvExpand.expanded());
55+
break;
56+
} else if (alias.child().semanticEquals(mvExpand.target().toAttribute())) {
57+
// for query like: row a = 2 | eval b = a | keep * | mv_expand a
58+
Alias aliasAlias = new Alias(
59+
alias.source(),
60+
TemporaryNameUtils.temporaryName(alias.child(), alias.toAttribute(), 0),
61+
alias.child()
62+
);
63+
projections.set(i, alias.replaceChild(aliasAlias.toAttribute()));
64+
pj = new Project(pj.source(), new Eval(aliasAlias.source(), pj.child(), List.of(aliasAlias)), projections);
65+
mvExpand = mvExpand.replaceChild(pj);
66+
break;
67+
}
4068
}
4169
}
4270

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3149,13 +3149,14 @@ public void testPushDownMvExpandPastProject() {
31493149
* <pre>{@code
31503150
* Project[[a{r}#13, b{r}#14]]
31513151
* \_Limit[1000[INTEGER],true,false]
3152-
* \_MvExpand[$$a$b$0{r}#15,b{r}#14]
3153-
* \_Eval[[a{r}#13 AS $$a$b$0#15]]
3152+
* \_MvExpand[$$a$b$0$b$0{r}#16,b{r}#14]
3153+
* \_Eval[[$$a$b$0{r}#15 AS $$a$b$0$b$0#16]]
31543154
* \_Limit[1000[INTEGER],true,false]
31553155
* \_MvExpand[a{r}#6,a{r}#13]
3156-
* \_Limit[1000[INTEGER],false,false]
3157-
* \_Aggregate[[],[COUNT(*[KEYWORD],true[BOOLEAN]) AS a#6]]
3158-
* \_LocalRelation[[a{r}#4],Page{blocks=[IntVectorBlock[vector=ConstantIntVector[positions=1, value=1]]]}]
3156+
* \_Eval[[a{r}#6 AS $$a$b$0#15]]
3157+
* \_Limit[1000[INTEGER],false,false]
3158+
* \_Aggregate[[],[COUNT(*[KEYWORD],true[BOOLEAN],PT0S[TIME_DURATION]) AS a#6]]
3159+
* \_LocalRelation[[a{r}#4],Page{blocks=[IntVectorBlock[vector=ConstantIntVector[positions=1, value=1]]]}]]
31593160
* }</pre>
31603161
*/
31613162
public void testPushDownMvExpandPastProject2() {
@@ -3171,7 +3172,8 @@ public void testPushDownMvExpandPastProject2() {
31713172
var eval = as(mvExpand.child(), Eval.class);
31723173
limit = asLimit(eval.child(), 1000, true);
31733174
mvExpand = as(limit.child(), MvExpand.class);
3174-
limit = asLimit(mvExpand.child(), 1000, false);
3175+
eval = as(mvExpand.child(), Eval.class);
3176+
limit = asLimit(eval.child(), 1000, false);
31753177
var agg = as(limit.child(), Aggregate.class);
31763178
as(agg.child(), LocalRelation.class);
31773179
}

0 commit comments

Comments
 (0)