Skip to content

Commit 2f75277

Browse files
Add Dependency Checker for LogicalLocalPlanOptimizer (#130409)
Add verification for LocalLogical plan The verification is skipped if there is remote enrich, similar to how it is skipped for LocalPhysical plan optimization. The skip only happens for LocalLogical and LocalPhysical plan optimizers.
1 parent fda7e56 commit 2f75277

File tree

10 files changed

+109
-13
lines changed

10 files changed

+109
-13
lines changed

docs/changelog/130409.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 130409
2+
summary: Add Dependency Checker for `LogicalLocalPlanOptimizer`
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
package org.elasticsearch.xpack.esql.optimizer;
99

10+
import org.elasticsearch.xpack.esql.VerificationException;
11+
import org.elasticsearch.xpack.esql.common.Failures;
1012
import org.elasticsearch.xpack.esql.optimizer.rules.logical.PropagateEmptyRelation;
1113
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceStatsFilteredAggWithEval;
1214
import org.elasticsearch.xpack.esql.optimizer.rules.logical.ReplaceStringCasingWithInsensitiveRegexMatch;
@@ -35,6 +37,8 @@
3537
*/
3638
public class LocalLogicalPlanOptimizer extends ParameterizedRuleExecutor<LogicalPlan, LocalLogicalOptimizerContext> {
3739

40+
private final LogicalVerifier verifier = LogicalVerifier.INSTANCE;
41+
3842
private static final List<Batch<LogicalPlan>> RULES = arrayAsArrayList(
3943
new Batch<>(
4044
"Local rewrite",
@@ -81,6 +85,12 @@ private static Batch<LogicalPlan> localOperators() {
8185
}
8286

8387
public LogicalPlan localOptimize(LogicalPlan plan) {
84-
return execute(plan);
88+
LogicalPlan optimized = execute(plan);
89+
Failures failures = verifier.verify(optimized, true);
90+
if (failures.hasFailures()) {
91+
throw new VerificationException(failures);
92+
}
93+
return optimized;
8594
}
95+
8696
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public PhysicalPlan localOptimize(PhysicalPlan plan) {
4646
}
4747

4848
PhysicalPlan verify(PhysicalPlan plan) {
49-
Failures failures = verifier.verify(plan);
49+
Failures failures = verifier.verify(plan, true);
5050
if (failures.hasFailures()) {
5151
throw new VerificationException(failures);
5252
}

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);
115+
Failures failures = verifier.verify(optimized, false);
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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware;
1111
import org.elasticsearch.xpack.esql.common.Failures;
1212
import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker;
13+
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
1314
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1415

1516
public final class LogicalVerifier {
@@ -19,10 +20,18 @@ public final class LogicalVerifier {
1920
private LogicalVerifier() {}
2021

2122
/** Verifies the optimized logical plan. */
22-
public Failures verify(LogicalPlan plan) {
23+
public Failures verify(LogicalPlan plan, boolean skipRemoteEnrichVerification) {
2324
Failures failures = new Failures();
2425
Failures dependencyFailures = new Failures();
2526

27+
if (skipRemoteEnrichVerification) {
28+
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
29+
var enriches = plan.collectFirstChildren(Enrich.class::isInstance);
30+
if (enriches.isEmpty() == false && ((Enrich) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
31+
return failures;
32+
}
33+
}
34+
2635
plan.forEachUp(p -> {
2736
PlanConsistencyChecker.checkPlan(p, dependencyFailures);
2837

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public PhysicalPlan optimize(PhysicalPlan plan) {
3838
}
3939

4040
PhysicalPlan verify(PhysicalPlan plan) {
41-
Failures failures = verifier.verify(plan);
41+
Failures failures = verifier.verify(plan, false);
4242
if (failures.hasFailures()) {
4343
throw new VerificationException(failures);
4444
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ public final class PhysicalVerifier {
2727
private PhysicalVerifier() {}
2828

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

34-
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
35-
var enriches = plan.collectFirstChildren(EnrichExec.class::isInstance);
36-
if (enriches.isEmpty() == false && ((EnrichExec) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
37-
return failures;
34+
if (skipRemoteEnrichVerification) {
35+
// AwaitsFix https://github.com/elastic/elasticsearch/issues/118531
36+
var enriches = plan.collectFirstChildren(EnrichExec.class::isInstance);
37+
if (enriches.isEmpty() == false && ((EnrichExec) enriches.get(0)).mode() == Enrich.Mode.REMOTE) {
38+
return failures;
39+
}
3840
}
3941

4042
plan.forEachDown(p -> {

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
2424
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
2525
import org.elasticsearch.xpack.esql.core.expression.Literal;
26+
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
2627
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
2728
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2829
import org.elasticsearch.xpack.esql.core.tree.Source;
2930
import org.elasticsearch.xpack.esql.core.type.DataType;
3031
import org.elasticsearch.xpack.esql.core.type.EsField;
3132
import org.elasticsearch.xpack.esql.core.type.InvalidMappedField;
33+
import org.elasticsearch.xpack.esql.expression.Order;
3234
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
35+
import org.elasticsearch.xpack.esql.expression.function.aggregate.Min;
3336
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
3437
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
3538
import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith;
@@ -49,6 +52,7 @@
4952
import org.elasticsearch.xpack.esql.plan.logical.Limit;
5053
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
5154
import org.elasticsearch.xpack.esql.plan.logical.MvExpand;
55+
import org.elasticsearch.xpack.esql.plan.logical.OrderBy;
5256
import org.elasticsearch.xpack.esql.plan.logical.Project;
5357
import org.elasticsearch.xpack.esql.plan.logical.Row;
5458
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
@@ -64,6 +68,7 @@
6468
import java.util.Map;
6569
import java.util.Set;
6670

71+
import static java.util.Arrays.asList;
6772
import static java.util.Collections.emptyMap;
6873
import static org.elasticsearch.xpack.esql.EsqlTestUtils.L;
6974
import static org.elasticsearch.xpack.esql.EsqlTestUtils.ONE;
@@ -84,6 +89,7 @@
8489
import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning;
8590
import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY;
8691
import static org.hamcrest.Matchers.contains;
92+
import static org.hamcrest.Matchers.containsString;
8793
import static org.hamcrest.Matchers.equalTo;
8894
import static org.hamcrest.Matchers.hasSize;
8995
import static org.hamcrest.Matchers.is;
@@ -774,6 +780,32 @@ public void testGroupingByMissingFields() {
774780
as(eval.child(), EsRelation.class);
775781
}
776782

783+
public void testPlanSanityCheck() throws Exception {
784+
var plan = localPlan("""
785+
from test
786+
| stats a = min(salary) by emp_no
787+
""");
788+
789+
var limit = as(plan, Limit.class);
790+
var aggregate = as(limit.child(), Aggregate.class);
791+
var min = as(Alias.unwrap(aggregate.aggregates().get(0)), Min.class);
792+
var salary = as(min.field(), NamedExpression.class);
793+
assertThat(salary.name(), is("salary"));
794+
// emulate a rule that adds an invalid field
795+
var invalidPlan = new OrderBy(
796+
limit.source(),
797+
limit,
798+
asList(new Order(limit.source(), salary, Order.OrderDirection.ASC, Order.NullsPosition.FIRST))
799+
);
800+
801+
var localContext = new LocalLogicalOptimizerContext(EsqlTestUtils.TEST_CFG, FoldContext.small(), TEST_SEARCH_STATS);
802+
LocalLogicalPlanOptimizer localLogicalPlanOptimizer = new LocalLogicalPlanOptimizer(localContext);
803+
804+
IllegalStateException e = expectThrows(IllegalStateException.class, () -> localLogicalPlanOptimizer.localOptimize(invalidPlan));
805+
assertThat(e.getMessage(), containsString("Plan [OrderBy[[Order[salary"));
806+
assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references [salary"));
807+
}
808+
777809
private IsNotNull isNotNull(Expression field) {
778810
return new IsNotNull(EMPTY, field);
779811
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.elasticsearch.xpack.esql.core.expression.Expression;
4444
import org.elasticsearch.xpack.esql.core.expression.Expressions;
4545
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
46+
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
4647
import org.elasticsearch.xpack.esql.core.expression.Literal;
4748
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
4849
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
@@ -52,9 +53,11 @@
5253
import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField;
5354
import org.elasticsearch.xpack.esql.core.util.Holder;
5455
import org.elasticsearch.xpack.esql.enrich.ResolvedEnrichPolicy;
56+
import org.elasticsearch.xpack.esql.expression.Order;
5557
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
5658
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
5759
import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
60+
import org.elasticsearch.xpack.esql.expression.function.aggregate.Min;
5861
import org.elasticsearch.xpack.esql.expression.function.fulltext.FullTextFunction;
5962
import org.elasticsearch.xpack.esql.expression.function.fulltext.Kql;
6063
import org.elasticsearch.xpack.esql.expression.function.fulltext.Match;
@@ -131,8 +134,12 @@
131134
import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.indexWithDateDateNanosUnionType;
132135
import static org.elasticsearch.xpack.esql.core.querydsl.query.Query.unscore;
133136
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
137+
import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
138+
import static org.elasticsearch.xpack.esql.core.util.TestUtils.getFieldAttribute;
139+
import static org.elasticsearch.xpack.esql.plan.physical.AbstractPhysicalPlanSerializationTests.randomEstimatedRowSize;
134140
import static org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.StatsType;
135141
import static org.hamcrest.Matchers.contains;
142+
import static org.hamcrest.Matchers.containsString;
136143
import static org.hamcrest.Matchers.equalTo;
137144
import static org.hamcrest.Matchers.hasSize;
138145
import static org.hamcrest.Matchers.instanceOf;
@@ -2056,6 +2063,37 @@ public void testToDateNanosPushDown() {
20562063
assertThat(expected.toString(), is(esQuery.query().toString()));
20572064
}
20582065

2066+
public void testVerifierOnMissingReferences() throws Exception {
2067+
2068+
PhysicalPlan plan = plannerOptimizer.plan("""
2069+
from test
2070+
| stats a = min(salary) by emp_no
2071+
""");
2072+
2073+
var limit = as(plan, LimitExec.class);
2074+
var aggregate = as(limit.child(), AggregateExec.class);
2075+
var min = as(Alias.unwrap(aggregate.aggregates().get(0)), Min.class);
2076+
var salary = as(min.field(), NamedExpression.class);
2077+
assertThat(salary.name(), is("salary"));
2078+
// emulate a rule that adds a missing attribute
2079+
FieldAttribute missingAttr = getFieldAttribute("missing attr");
2080+
List<Order> orders = List.of(new Order(plan.source(), missingAttr, Order.OrderDirection.ASC, Order.NullsPosition.FIRST));
2081+
TopNExec topNExec = new TopNExec(plan.source(), plan, orders, new Literal(Source.EMPTY, limit, INTEGER), randomEstimatedRowSize());
2082+
2083+
// We want to verify that the localOptimize detects the missing attribute.
2084+
// However, it also throws an error in one of the rules before we get to the verifier.
2085+
// So we use an implementation of LocalPhysicalPlanOptimizer that does not have any rules.
2086+
LocalPhysicalOptimizerContext context = new LocalPhysicalOptimizerContext(config, FoldContext.small(), SearchStats.EMPTY);
2087+
LocalPhysicalPlanOptimizer optimizerWithNoopExecute = new LocalPhysicalPlanOptimizer(context) {
2088+
@Override
2089+
protected List<Batch<PhysicalPlan>> batches() {
2090+
return List.of();
2091+
}
2092+
};
2093+
Exception e = expectThrows(IllegalStateException.class, () -> optimizerWithNoopExecute.localOptimize(topNExec));
2094+
assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references [missing attr"));
2095+
}
2096+
20592097
private boolean isMultiTypeEsField(Expression e) {
20602098
return e instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField;
20612099
}

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);
5557+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
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);
5608+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
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);
5664+
Failures inconsistencies = LogicalVerifier.INSTANCE.verify(optimizedPlan, false);
56655665
assertFalse(inconsistencies.hasFailures());
56665666

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

0 commit comments

Comments
 (0)