Skip to content

Commit 7ec9efc

Browse files
Verify the layout is not changed during optimizations
1 parent baf4f2f commit 7ec9efc

File tree

10 files changed

+62
-27
lines changed

10 files changed

+62
-27
lines changed

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

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

136136
protected abstract String label();
137+
138+
/**
139+
* Compares the size and datatypes of two lists of attributes for equality.
140+
*/
141+
public static boolean datatypeEquals(List<Attribute> left, List<Attribute> right) {
142+
if (left.size() != right.size()) {
143+
return false;
144+
}
145+
for (int i = 0; i < left.size(); i++) {
146+
if (left.get(i).dataType().equals(right.get(i).dataType()) == false) {
147+
return false;
148+
}
149+
}
150+
return true;
151+
}
137152
}

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.datatypeEquals;
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 (datatypeEquals(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: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker;
1515
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
1616
import org.elasticsearch.xpack.esql.plan.physical.EnrichExec;
17+
import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec;
1718
import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec;
1819
import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan;
1920

21+
import static org.elasticsearch.index.IndexMode.LOOKUP;
2022
import static org.elasticsearch.xpack.esql.common.Failure.fail;
23+
import static org.elasticsearch.xpack.esql.core.expression.Attribute.datatypeEquals;
2124

2225
/** Physical plan verifier. */
2326
public final class PhysicalVerifier {
@@ -27,19 +30,19 @@ public final class PhysicalVerifier {
2730
private PhysicalVerifier() {}
2831

2932
/** Verifies the physical plan. */
30-
public Failures verify(PhysicalPlan plan, boolean skipRemoteEnrichVerification) {
33+
public Failures verify(PhysicalPlan planAfter, boolean skipRemoteEnrichVerification, PhysicalPlan planBefore) {
3134
Failures failures = new Failures();
3235
Failures depFailures = new Failures();
3336

3437
if (skipRemoteEnrichVerification) {
3538
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
36-
var enriches = plan.collectFirstChildren(EnrichExec.class::isInstance);
39+
var enriches = planAfter.collectFirstChildren(EnrichExec.class::isInstance);
3740
if (enriches.isEmpty() == false && ((EnrichExec) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
3841
return failures;
3942
}
4043
}
4144

42-
plan.forEachDown(p -> {
45+
planAfter.forEachDown(p -> {
4346
if (p instanceof FieldExtractExec fieldExtractExec) {
4447
Attribute sourceAttribute = fieldExtractExec.sourceAttribute();
4548
if (sourceAttribute == null) {
@@ -67,6 +70,14 @@ public Failures verify(PhysicalPlan plan, boolean skipRemoteEnrichVerification)
6770
}
6871
});
6972

73+
if (datatypeEquals(planBefore.output(), planAfter.output()) == false) {
74+
if ((planAfter instanceof EsQueryExec esQueryExec && esQueryExec.indexMode() == LOOKUP) == false) {
75+
failures.add(
76+
fail(planAfter, "Layout has changed from [{}] to [{}]. ", planBefore.output().toString(), planAfter.output().toString())
77+
);
78+
}
79+
}
80+
7081
if (depFailures.hasFailures()) {
7182
throw new IllegalStateException(depFailures.toString());
7283
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/rule/RuleExecutor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ protected final ExecutionInfo executeWithInfo(TreeType plan) {
175175
if (tf.hasChanged()) {
176176
hasChanged = true;
177177
if (log.isTraceEnabled()) {
178-
log.trace("Rule {} applied\n{}", rule, NodeUtils.diffString(tf.before, tf.after));
178+
log.trace("Rule {} applied with change\n{}", rule, NodeUtils.diffString(tf.before, tf.after));
179179
}
180180
} else {
181181
if (log.isTraceEnabled()) {

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)