Skip to content

Commit 5bd41dd

Browse files
Add checks that optimizers do not modify the layout
1 parent 88aecdb commit 5bd41dd

File tree

9 files changed

+53
-26
lines changed

9 files changed

+53
-26
lines changed

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,16 @@ public String nodeString() {
134134
}
135135

136136
protected abstract String label();
137+
138+
public static boolean semanticEquals(List<Attribute> left, List<Attribute> right) {
139+
if (left.size() != right.size()) {
140+
return false;
141+
}
142+
for (int i = 0; i < left.size(); i++) {
143+
if (left.get(i).semanticEquals(right.get(i)) == false) {
144+
return false;
145+
}
146+
}
147+
return true;
148+
}
137149
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private static Batch<LogicalPlan> localOperators() {
8686

8787
public LogicalPlan localOptimize(LogicalPlan plan) {
8888
LogicalPlan optimized = execute(plan);
89-
Failures failures = verifier.verify(optimized, true);
89+
Failures failures = verifier.verify(optimized, true, plan);
9090
if (failures.hasFailures()) {
9191
throw new VerificationException(failures);
9292
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ public LocalPhysicalPlanOptimizer(LocalPhysicalOptimizerContext context) {
4242
}
4343

4444
public PhysicalPlan localOptimize(PhysicalPlan plan) {
45-
return verify(execute(plan));
45+
return verify(execute(plan), plan);
4646
}
4747

48-
PhysicalPlan verify(PhysicalPlan plan) {
49-
Failures failures = verifier.verify(plan, true);
48+
PhysicalPlan verify(PhysicalPlan planAfter, PhysicalPlan planBefore) {
49+
Failures failures = verifier.verify(planAfter, true, planBefore);
5050
if (failures.hasFailures()) {
5151
throw new VerificationException(failures);
5252
}
53-
return plan;
53+
return planAfter;
5454
}
5555

5656
@Override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public LogicalPlanOptimizer(LogicalOptimizerContext optimizerContext) {
112112
public LogicalPlan optimize(LogicalPlan verified) {
113113
var optimized = execute(verified);
114114

115-
Failures failures = verifier.verify(optimized, false);
115+
Failures failures = verifier.verify(optimized, false, verified);
116116
if (failures.hasFailures()) {
117117
throw new VerificationException(failures);
118118
}

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,29 @@
1313
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
1414
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1515

16+
import static org.elasticsearch.xpack.esql.common.Failure.fail;
17+
import static org.elasticsearch.xpack.esql.core.expression.Attribute.semanticEquals;
18+
1619
public final class LogicalVerifier {
1720

1821
public static final LogicalVerifier INSTANCE = new LogicalVerifier();
1922

2023
private LogicalVerifier() {}
2124

2225
/** Verifies the optimized logical plan. */
23-
public Failures verify(LogicalPlan plan, boolean skipRemoteEnrichVerification) {
26+
public Failures verify(LogicalPlan planAfter, boolean skipRemoteEnrichVerification, LogicalPlan planBefore) {
2427
Failures failures = new Failures();
2528
Failures dependencyFailures = new Failures();
2629

2730
if (skipRemoteEnrichVerification) {
2831
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
29-
var enriches = plan.collectFirstChildren(Enrich.class::isInstance);
32+
var enriches = planAfter.collectFirstChildren(Enrich.class::isInstance);
3033
if (enriches.isEmpty() == false && ((Enrich) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
3134
return failures;
3235
}
3336
}
3437

35-
plan.forEachUp(p -> {
38+
planAfter.forEachUp(p -> {
3639
PlanConsistencyChecker.checkPlan(p, dependencyFailures);
3740

3841
if (failures.hasFailures() == false) {
@@ -47,6 +50,12 @@ public Failures verify(LogicalPlan plan, boolean skipRemoteEnrichVerification) {
4750
}
4851
});
4952

53+
if (semanticEquals(planBefore.output(), planAfter.output()) == false) {
54+
failures.add(
55+
fail(planAfter, "Layout has changed from [{}] to [{}]. ", planBefore.output().toString(), planAfter.output().toString())
56+
);
57+
}
58+
5059
if (dependencyFailures.hasFailures()) {
5160
throw new IllegalStateException(dependencyFailures.toString());
5261
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ public PhysicalPlanOptimizer(PhysicalOptimizerContext context) {
3434
}
3535

3636
public PhysicalPlan optimize(PhysicalPlan plan) {
37-
return verify(execute(plan));
37+
return verify(execute(plan), plan);
3838
}
3939

40-
PhysicalPlan verify(PhysicalPlan plan) {
41-
Failures failures = verifier.verify(plan, false);
40+
PhysicalPlan verify(PhysicalPlan planAfter, PhysicalPlan planBefore) {
41+
Failures failures = verifier.verify(planAfter, false, planBefore);
4242
if (failures.hasFailures()) {
4343
throw new VerificationException(failures);
4444
}
45-
return plan;
45+
return planAfter;
4646
}
4747

4848
@Override

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ public final class PhysicalVerifier {
2727
private PhysicalVerifier() {}
2828

2929
/** Verifies the physical plan. */
30-
public Failures verify(PhysicalPlan plan, boolean skipRemoteEnrichVerification) {
30+
public Failures verify(PhysicalPlan planAfter, boolean skipRemoteEnrichVerification, PhysicalPlan planBefore) {
3131
Failures failures = new Failures();
3232
Failures depFailures = new Failures();
3333

3434
if (skipRemoteEnrichVerification) {
3535
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
36-
var enriches = plan.collectFirstChildren(EnrichExec.class::isInstance);
36+
var enriches = planAfter.collectFirstChildren(EnrichExec.class::isInstance);
3737
if (enriches.isEmpty() == false && ((EnrichExec) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
3838
return failures;
3939
}
4040
}
4141

42-
plan.forEachDown(p -> {
42+
planAfter.forEachDown(p -> {
4343
if (p instanceof FieldExtractExec fieldExtractExec) {
4444
Attribute sourceAttribute = fieldExtractExec.sourceAttribute();
4545
if (sourceAttribute == null) {
@@ -67,6 +67,12 @@ public Failures verify(PhysicalPlan plan, boolean skipRemoteEnrichVerification)
6767
}
6868
});
6969

70+
if (planBefore.output().equals(planAfter.output()) == false) {
71+
failures.add(
72+
fail(planAfter, "Layout has changed from [{}] to [{}]. ", planBefore.output().toString(), planAfter.output().toString())
73+
);
74+
}
75+
7076
if (depFailures.hasFailures()) {
7177
throw new IllegalStateException(depFailures.toString());
7278
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5554,7 +5554,7 @@ public void testPushShadowingGeneratingPlanPastProject() {
55545554
List<Attribute> initialGeneratedExprs = ((GeneratingPlan) initialPlan).generatedAttributes();
55555555
LogicalPlan optimizedPlan = testCase.rule.apply(initialPlan);
55565556

5557-
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
5557+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false, initialPlan);
55585558
assertFalse(inconsistencies.hasFailures());
55595559

55605560
Project project = as(optimizedPlan, Project.class);
@@ -5605,7 +5605,7 @@ public void testPushShadowingGeneratingPlanPastRenamingProject() {
56055605
List<Attribute> initialGeneratedExprs = ((GeneratingPlan) initialPlan).generatedAttributes();
56065606
LogicalPlan optimizedPlan = testCase.rule.apply(initialPlan);
56075607

5608-
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
5608+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false, initialPlan);
56095609
assertFalse(inconsistencies.hasFailures());
56105610

56115611
Project project = as(optimizedPlan, Project.class);
@@ -5661,7 +5661,7 @@ public void testPushShadowingGeneratingPlanPastRenamingProjectWithResolution() {
56615661

56625662
// This ensures that our generating plan doesn't use invalid references, resp. that any rename from the Project has
56635663
// been propagated into the generating plan.
5664-
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
5664+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false, initialPlan);
56655665
assertFalse(inconsistencies.hasFailures());
56665666

56675667
Project project = as(optimizedPlan, Project.class);

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,7 +2865,7 @@ public void testFieldExtractWithoutSourceAttributes() {
28652865
)
28662866
);
28672867

2868-
var e = expectThrows(VerificationException.class, () -> physicalPlanOptimizer.verify(badPlan));
2868+
var e = expectThrows(VerificationException.class, () -> physicalPlanOptimizer.verify(badPlan, verifiedPlan));
28692869
assertThat(
28702870
e.getMessage(),
28712871
containsString(
@@ -2880,7 +2880,7 @@ public void testVerifierOnMissingReferences() {
28802880
| stats s = sum(salary) by emp_no
28812881
| where emp_no > 10
28822882
""");
2883-
2883+
final var planBeforeModification = plan;
28842884
plan = plan.transformUp(
28852885
AggregateExec.class,
28862886
a -> new AggregateExec(
@@ -2894,7 +2894,7 @@ public void testVerifierOnMissingReferences() {
28942894
)
28952895
);
28962896
final var finalPlan = plan;
2897-
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan));
2897+
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan, planBeforeModification));
28982898
assertThat(e.getMessage(), containsString(" > 10[INTEGER]]] optimized incorrectly due to missing references [emp_no{f}#"));
28992899
}
29002900

@@ -2912,7 +2912,7 @@ public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception {
29122912

29132913
var planWithInvalidJoinLeftSide = plan.transformUp(LookupJoinExec.class, join -> join.replaceChildren(join.right(), join.right()));
29142914

2915-
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinLeftSide));
2915+
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinLeftSide, plan));
29162916
assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from left hand side [languages"));
29172917

29182918
var planWithInvalidJoinRightSide = plan.transformUp(
@@ -2929,7 +2929,7 @@ public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception {
29292929
)
29302930
);
29312931

2932-
e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinRightSide));
2932+
e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinRightSide, plan));
29332933
assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from right hand side [language_code"));
29342934
}
29352935

@@ -2939,7 +2939,7 @@ public void testVerifierOnDuplicateOutputAttributes() {
29392939
| stats s = sum(salary) by emp_no
29402940
| where emp_no > 10
29412941
""");
2942-
2942+
final var planBeforeModification = plan;
29432943
plan = plan.transformUp(AggregateExec.class, a -> {
29442944
List<Attribute> intermediates = new ArrayList<>(a.intermediateAttributes());
29452945
intermediates.add(intermediates.get(0));
@@ -2954,7 +2954,7 @@ public void testVerifierOnDuplicateOutputAttributes() {
29542954
);
29552955
});
29562956
final var finalPlan = plan;
2957-
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan));
2957+
var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan, planBeforeModification));
29582958
assertThat(
29592959
e.getMessage(),
29602960
containsString("Plan [LimitExec[1000[INTEGER],null]] optimized incorrectly due to duplicate output attribute emp_no{f}#")

0 commit comments

Comments
 (0)