Skip to content

Commit 437952c

Browse files
authored
ESQL: Fix ReplaceAliasingEvalWithProject in case of shadowing (#137025) (#137286)
Fix #137019: a bug that happened when the Eval has (non-aliasing) fields that happen to overwrite the attributes that we try to alias in a subsequent Project.
1 parent da9669b commit 437952c

File tree

6 files changed

+607
-23
lines changed

6 files changed

+607
-23
lines changed

docs/changelog/137025.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 137025
2+
summary: Fix `ReplaceAliasingEvalWithProject` in case of shadowing
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 137019

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import org.elasticsearch.xpack.esql.core.type.DataType;
8181
import org.elasticsearch.xpack.esql.core.type.EsField;
8282
import org.elasticsearch.xpack.esql.core.util.DateUtils;
83+
import org.elasticsearch.xpack.esql.core.util.Holder;
8384
import org.elasticsearch.xpack.esql.core.util.StringUtils;
8485
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
8586
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash;
@@ -88,6 +89,7 @@
8889
import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike;
8990
import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike;
9091
import org.elasticsearch.xpack.esql.expression.predicate.Range;
92+
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul;
9193
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
9294
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan;
9395
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThanOrEqual;
@@ -156,6 +158,7 @@
156158
import java.util.Map;
157159
import java.util.Set;
158160
import java.util.TreeMap;
161+
import java.util.function.Predicate;
159162
import java.util.jar.JarInputStream;
160163
import java.util.zip.ZipEntry;
161164

@@ -230,6 +233,27 @@ public static GreaterThanOrEqual greaterThanOrEqualOf(Expression left, Expressio
230233
return new GreaterThanOrEqual(EMPTY, left, right, ESTestCase.randomZone());
231234
}
232235

236+
public static FieldAttribute findFieldAttribute(LogicalPlan plan, String name) {
237+
return findFieldAttribute(plan, name, (unused) -> true);
238+
}
239+
240+
public static FieldAttribute findFieldAttribute(LogicalPlan plan, String name, Predicate<EsRelation> inThisRelation) {
241+
Holder<FieldAttribute> result = new Holder<>();
242+
plan.forEachDown(EsRelation.class, relation -> {
243+
if (inThisRelation.test(relation) == false) {
244+
return;
245+
}
246+
for (Attribute attr : relation.output()) {
247+
if (attr.name().equals(name)) {
248+
assertNull("Multiple matching field attributes found", result.get());
249+
result.set((FieldAttribute) attr);
250+
return;
251+
}
252+
}
253+
});
254+
return result.get();
255+
}
256+
233257
public static FieldAttribute getFieldAttribute() {
234258
return getFieldAttribute("a");
235259
}
@@ -272,6 +296,14 @@ public static ReferenceAttribute referenceAttribute(String name, DataType type)
272296
return new ReferenceAttribute(EMPTY, name, type);
273297
}
274298

299+
public static Alias alias(String name, Expression child) {
300+
return new Alias(EMPTY, name, child);
301+
}
302+
303+
public static Mul mul(Expression left, Expression right) {
304+
return new Mul(EMPTY, left, right);
305+
}
306+
275307
public static Range rangeOf(Expression value, Expression lower, boolean includeLower, Expression upper, boolean includeUpper) {
276308
return new Range(EMPTY, value, lower, includeLower, upper, includeUpper, randomZone());
277309
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,28 @@ year:integer
805805
12
806806
12
807807
;
808+
809+
// Bugfixes for ReplaceAliasingEvalWithProject not accounting for shadowing
810+
chainOfAliases
811+
required_capability: fix_replace_aliasing_eval_with_project_shadowing
812+
813+
row x = 1
814+
| eval y = x, z = y, x = 2, y = 3
815+
| keep *
816+
;
817+
818+
z:integer | x:integer | y:integer
819+
1 | 2 | 3
820+
;
821+
822+
shadowingOfAliases
823+
required_capability: fix_replace_aliasing_eval_with_project_shadowing
824+
825+
row x = 1
826+
| eval y = "foo", z = x, x = 9
827+
| rename y as a
828+
;
829+
830+
a:keyword | z:integer | x:integer
831+
foo | 1 | 9
832+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,13 @@ public enum Cap {
16441644
LOOKUP_JOIN_SEMANTIC_FILTER_DEDUP,
16451645

16461646
FIX_MV_CONSTANT_EQUALS_FIELD,
1647+
1648+
/**
1649+
* {@link org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceAliasingEvalWithProject} did not fully account for shadowing.
1650+
* https://github.com/elastic/elasticsearch/issues/137019.
1651+
*/
1652+
FIX_REPLACE_ALIASING_EVAL_WITH_PROJECT_SHADOWING,
1653+
16471654
// Last capability should still have a comma for fewer merge conflicts when adding new ones :)
16481655
// This comment prevents the semicolon from being on the previous capability when Spotless formats the file.
16491656
;

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
import org.elasticsearch.xpack.esql.rule.Rule;
2121

2222
import java.util.ArrayList;
23+
import java.util.HashSet;
2324
import java.util.List;
25+
import java.util.Set;
26+
27+
import static org.elasticsearch.xpack.esql.optimizer.rules.logical.TemporaryNameUtils.locallyUniqueTemporaryName;
2428

2529
/**
2630
* Replace aliasing evals (eval x=a) with a projection which can be further combined / simplified.
@@ -53,47 +57,63 @@ public LogicalPlan apply(LogicalPlan logicalPlan) {
5357
private LogicalPlan rule(Eval eval) {
5458
LogicalPlan plan = eval;
5559

56-
// holds simple aliases such as b = a, c = b, d = c
57-
AttributeMap.Builder<Expression> basicAliasesBuilder = AttributeMap.builder();
58-
// same as above but keeps the original expression
59-
AttributeMap.Builder<NamedExpression> basicAliasSourcesBuilder = AttributeMap.builder();
60+
// Mostly, holds simple aliases from the eval, such as b = a, c = b, d = c, so we can resolve them in subsequent eval fields
61+
AttributeMap.Builder<Expression> renamesToPropagate = AttributeMap.builder();
62+
// the aliases for the final projection - mostly, same as above but holds the final aliases rather than the original attributes
63+
AttributeMap.Builder<NamedExpression> projectionAliases = AttributeMap.builder();
64+
// The names of attributes that are required to perform the aliases in a subsequent projection - if the next eval field
65+
// shadows one of these names, the subsequent projection won't work, so we need to perform a temporary rename.
66+
Set<String> namesRequiredForProjectionAliases = new HashSet<>();
6067

61-
List<Alias> keptFields = new ArrayList<>();
68+
List<Alias> newEvalFields = new ArrayList<>();
6269

6370
var fields = eval.fields();
64-
for (int i = 0, size = fields.size(); i < size; i++) {
65-
Alias field = fields.get(i);
71+
for (Alias alias : fields) {
72+
// propagate all previous aliases into the current field
73+
Alias field = (Alias) alias.transformUp(e -> renamesToPropagate.build().resolve(e, e));
74+
String name = field.name();
75+
Attribute attribute = field.toAttribute();
6676
Expression child = field.child();
67-
var attribute = field.toAttribute();
68-
// put the aliases in a separate map to separate the underlying resolve from other aliases
69-
if (child instanceof Attribute) {
70-
basicAliasesBuilder.put(attribute, child);
71-
basicAliasSourcesBuilder.put(attribute, field);
77+
78+
if (child instanceof Attribute renamedAttribute) {
79+
// Basic renaming - let's do that in the subsequent projection
80+
renamesToPropagate.put(attribute, renamedAttribute);
81+
projectionAliases.put(attribute, field);
82+
namesRequiredForProjectionAliases.add(renamedAttribute.name());
83+
} else if (namesRequiredForProjectionAliases.contains(name)) {
84+
// Not a basic renaming, needs to remain in the eval.
85+
// The field may shadow one of the attributes that we will need to correctly perform the subsequent projection.
86+
// So, rename it in the eval!
87+
88+
Alias newField = new Alias(field.source(), locallyUniqueTemporaryName(name), child, null, true);
89+
Attribute newAttribute = newField.toAttribute();
90+
Alias reRenamedField = new Alias(field.source(), name, newAttribute, field.id(), field.synthetic());
91+
projectionAliases.put(attribute, reRenamedField);
92+
// the renaming also needs to be propagated to eval fields to the right
93+
renamesToPropagate.put(attribute, newAttribute);
94+
95+
newEvalFields.add(newField);
7296
} else {
73-
// be lazy and start replacing name aliases only if needed
74-
if (basicAliasesBuilder.build().size() > 0) {
75-
// update the child through the field
76-
field = (Alias) field.transformUp(e -> basicAliasesBuilder.build().resolve(e, e));
77-
}
78-
keptFields.add(field);
97+
// still not a basic renaming, but no risk of shadowing
98+
newEvalFields.add(field);
7999
}
80100
}
81101

82102
// at least one alias encountered, move it into a project
83-
if (basicAliasesBuilder.build().size() > 0) {
103+
if (renamesToPropagate.build().size() > 0) {
84104
// preserve the eval output (takes care of shadowing and order) but replace the basic aliases
85105
List<NamedExpression> projections = new ArrayList<>(eval.output());
86-
var basicAliasSources = basicAliasSourcesBuilder.build();
106+
var projectionAliasesMap = projectionAliases.build();
87107
// replace the removed aliases with their initial definition - however use the output to preserve the shadowing
88108
for (int i = projections.size() - 1; i >= 0; i--) {
89109
NamedExpression project = projections.get(i);
90-
projections.set(i, basicAliasSources.getOrDefault(project, project));
110+
projections.set(i, projectionAliasesMap.getOrDefault(project, project));
91111
}
92112

93113
LogicalPlan child = eval.child();
94-
if (keptFields.size() > 0) {
114+
if (newEvalFields.size() > 0) {
95115
// replace the eval with just the kept fields
96-
child = new Eval(eval.source(), eval.child(), keptFields);
116+
child = new Eval(eval.source(), eval.child(), newEvalFields);
97117
}
98118
// put the projection in place
99119
plan = new Project(eval.source(), child, projections);

0 commit comments

Comments
 (0)