diff --git a/docs/changelog/104586.yaml b/docs/changelog/104586.yaml new file mode 100644 index 0000000000000..db1d01c22eff6 --- /dev/null +++ b/docs/changelog/104586.yaml @@ -0,0 +1,6 @@ +pr: 104586 +summary: Reduce the number of Evals `ReplaceMissingFieldWithNull` creates +area: ES|QL +type: bug +issues: + - 104583 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index eca8beb06576b..c42a7b7f8d48c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -143,13 +143,21 @@ public static EnrichResolution emptyPolicyResolution() { return new EnrichResolution(Set.of(), Set.of()); } + public static SearchStats statsForExistingField(String... names) { + return fieldMatchingExistOrMissing(true, names); + } + public static SearchStats statsForMissingField(String... names) { + return fieldMatchingExistOrMissing(false, names); + } + + private static SearchStats fieldMatchingExistOrMissing(boolean exists, String... names) { return new TestSearchStats() { - private final Set missingFields = Set.of(names); + private final Set fields = Set.of(names); @Override public boolean exists(String field) { - return missingFields.contains(field) == false; + return fields.contains(field) == exists; } }; } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java index e3c296171a28a..83401cef79dc8 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -1287,7 +1288,7 @@ public void testStatsNestFields() { } } - public void testStatsMissingFields() { + public void testStatsMissingFieldWithStats() { final String node1, node2; if (randomBoolean()) { internalCluster().ensureAtLeastNumDataNodes(2); @@ -1326,6 +1327,39 @@ public void testStatsMissingFields() { } } + public void testStatsMissingFieldKeepApp() { + final String node1, node2; + if (randomBoolean()) { + internalCluster().ensureAtLeastNumDataNodes(2); + node1 = randomDataNode().getName(); + node2 = randomValueOtherThan(node1, () -> randomDataNode().getName()); + } else { + node1 = randomDataNode().getName(); + node2 = randomDataNode().getName(); + } + assertAcked( + client().admin() + .indices() + .prepareCreate("foo-index") + .setSettings(Settings.builder().put("index.routing.allocation.require._name", node1)) + .setMapping("foo_int", "type=integer", "foo_long", "type=long", "foo_float", "type=float", "foo_double", "type=double") + ); + assertAcked( + client().admin() + .indices() + .prepareCreate("bar-index") + .setSettings(Settings.builder().put("index.routing.allocation.require._name", node2)) + .setMapping("bar_int", "type=integer", "bar_long", "type=long", "bar_float", "type=float", "bar_double", "type=double") + ); + String command = String.format(Locale.ROOT, "from foo-index,bar-index"); + try (var resp = run(command)) { + var valuesList = getValuesList(resp); + assertEquals(8, resp.columns().size()); + assertEquals(0, valuesList.size()); + assertEquals(Collections.emptyList(), valuesList); + } + } + public void testCountTextField() { assertAcked(client().admin().indices().prepareCreate("test_count").setMapping("name", "type=text")); int numDocs = between(10, 1000); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java index 25477e501645d..bd777f208b3fb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java @@ -106,6 +106,8 @@ public Object fold() { @Override public ExpressionEvaluator.Factory toEvaluator(Function toEvaluator) { + // force datatype initialization + var dataType = dataType(); ExpressionEvaluator.Factory[] factories = children().stream() .map(e -> toEvaluator.apply(new MvMax(e.source(), e))) .toArray(ExpressionEvaluator.Factory[]::new); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java index e7456b0871b7c..015c5390dfabd 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java @@ -106,6 +106,9 @@ public Object fold() { @Override public ExpressionEvaluator.Factory toEvaluator(Function toEvaluator) { + // force datatype initialization + var dataType = dataType(); + ExpressionEvaluator.Factory[] factories = children().stream() .map(e -> toEvaluator.apply(new MvMin(e.source(), e))) .toArray(ExpressionEvaluator.Factory[]::new); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java index 39ce7b98afa5e..ef7dfca248e59 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.optimizer; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.BlockUtils; @@ -17,6 +18,7 @@ import org.elasticsearch.xpack.esql.planner.AbstractPhysicalOperationProviders; import org.elasticsearch.xpack.esql.planner.PlannerUtils; import org.elasticsearch.xpack.esql.stats.SearchStats; +import org.elasticsearch.xpack.esql.type.EsqlDataTypes; import org.elasticsearch.xpack.ql.expression.Alias; import org.elasticsearch.xpack.ql.expression.Attribute; import org.elasticsearch.xpack.ql.expression.FieldAttribute; @@ -37,6 +39,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import static java.util.Arrays.asList; import static org.elasticsearch.xpack.esql.optimizer.LogicalPlanOptimizer.cleanup; @@ -115,24 +118,35 @@ private LogicalPlan missingToNull(LogicalPlan plan, SearchStats stats) { else if (plan instanceof Project project) { var projections = project.projections(); List newProjections = new ArrayList<>(projections.size()); - List literals = new ArrayList<>(); + Map nullLiteral = Maps.newLinkedHashMapWithExpectedSize(EsqlDataTypes.types().size()); for (NamedExpression projection : projections) { if (projection instanceof FieldAttribute f && stats.exists(f.qualifiedName()) == false) { - var alias = new Alias(f.source(), f.name(), null, Literal.of(f, null), f.id()); - literals.add(alias); - newProjections.add(alias.toAttribute()); - } else { - newProjections.add(projection); + DataType dt = f.dataType(); + Alias nullAlias = nullLiteral.get(f.dataType()); + // save the first field as null (per datatype) + if (nullAlias == null) { + Alias alias = new Alias(f.source(), f.name(), null, Literal.of(f, null), f.id()); + nullLiteral.put(dt, alias); + projection = alias.toAttribute(); + } + // otherwise point to it + else { + // since avoids creating field copies + projection = new Alias(f.source(), f.name(), f.qualifier(), nullAlias.toAttribute(), f.id()); + } } + + newProjections.add(projection); } - if (literals.size() > 0) { - plan = new Eval(project.source(), project.child(), literals); + // add the first found field as null + if (nullLiteral.size() > 0) { + plan = new Eval(project.source(), project.child(), new ArrayList<>(nullLiteral.values())); plan = new Project(project.source(), plan, newProjections); - } else { - plan = project; } - } else { + } + // otherwise transform fields in place + else { plan = plan.transformExpressionsOnlyUp( FieldAttribute.class, f -> stats.exists(f.qualifiedName()) ? f : Literal.of(f, null) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java index 1a390a9b30ddf..722a707716a79 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java @@ -405,17 +405,25 @@ static class PropagateEvalFoldables extends Rule { @Override public LogicalPlan apply(LogicalPlan plan) { var collectRefs = new AttributeMap(); - // collect aliases + + java.util.function.Function replaceReference = r -> collectRefs.resolve(r, r); + + // collect aliases bottom-up plan.forEachExpressionUp(Alias.class, a -> { var c = a.child(); - if (c.foldable()) { - collectRefs.put(a.toAttribute(), c); + boolean shouldCollect = c.foldable(); + // try to resolve the expression based on an existing foldables + if (shouldCollect == false) { + c = c.transformUp(ReferenceAttribute.class, replaceReference); + shouldCollect = c.foldable(); + } + if (shouldCollect) { + collectRefs.put(a.toAttribute(), Literal.of(c)); } }); if (collectRefs.isEmpty()) { return plan; } - java.util.function.Function replaceReference = r -> collectRefs.resolve(r, r); plan = plan.transformUp(p -> { // Apply the replacement inside Filter and Eval (which shouldn't make a difference) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java index 8bb17006d05ef..fed887c6fe99a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java @@ -642,6 +642,11 @@ public String describe() { Stream.of(sinkOperatorFactory) ).map(Describable::describe).collect(joining("\n\\_", "\\_", "")); } + + @Override + public String toString() { + return describe(); + } } /** diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java index bc46189e13827..7c67e11359adc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.optimizer; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.analysis.Analyzer; @@ -29,18 +30,24 @@ import org.elasticsearch.xpack.ql.plan.logical.Project; import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.type.EsField; +import org.hamcrest.Matchers; import org.junit.BeforeClass; import java.util.List; +import java.util.Locale; import java.util.Map; +import static java.util.Collections.emptyMap; import static org.elasticsearch.xpack.esql.EsqlTestUtils.L; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_SEARCH_STATS; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_VERIFIER; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.statsForExistingField; import static org.elasticsearch.xpack.esql.EsqlTestUtils.statsForMissingField; import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -263,6 +270,48 @@ public void testMissingFieldInFilterNoProjection() { ); } + public void testSparseDocument() throws Exception { + var query = """ + from large + | keep field00* + | limit 10 + """; + + int size = 256; + Map large = Maps.newLinkedHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) { + var name = String.format(Locale.ROOT, "field%03d", i); + large.put(name, new EsField(name, DataTypes.INTEGER, emptyMap(), true, false)); + } + + SearchStats searchStats = statsForExistingField("field000", "field001", "field002", "field003", "field004"); + + EsIndex index = new EsIndex("large", large); + IndexResolution getIndexResult = IndexResolution.valid(index); + var logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG)); + + var analyzer = new Analyzer( + new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResult, EsqlTestUtils.emptyPolicyResolution()), + TEST_VERIFIER + ); + + var analyzed = analyzer.analyze(parser.createStatement(query)); + var optimized = logicalOptimizer.optimize(analyzed); + var localContext = new LocalLogicalOptimizerContext(EsqlTestUtils.TEST_CFG, searchStats); + var plan = new LocalLogicalPlanOptimizer(localContext).localOptimize(optimized); + + var project = as(plan, Project.class); + assertThat(project.projections(), hasSize(10)); + assertThat( + Expressions.names(project.projections()), + contains("field000", "field001", "field002", "field003", "field004", "field005", "field006", "field007", "field008", "field009") + ); + var eval = as(project.child(), Eval.class); + var field = eval.fields().get(0); + assertThat(Expressions.name(field), is("field005")); + assertThat(field.child().fold(), Matchers.nullValue()); + } + private LocalRelation asEmptyRelation(Object o) { var empty = as(o, LocalRelation.class); assertThat(empty.supplier(), is(LocalSupplier.EMPTY)); @@ -285,6 +334,10 @@ private LogicalPlan localPlan(LogicalPlan plan, SearchStats searchStats) { return localPlan; } + private LogicalPlan localPlan(String query) { + return localPlan(plan(query), TEST_SEARCH_STATS); + } + @Override protected List filteredWarnings() { return withDefaultLimitWarning(super.filteredWarnings()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index bb600690dbca4..44328de1c6f58 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.Stat; import org.elasticsearch.xpack.esql.plan.physical.EstimatesRowSize; import org.elasticsearch.xpack.esql.plan.physical.ExchangeExec; +import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; import org.elasticsearch.xpack.esql.plan.physical.LimitExec; import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -44,7 +45,9 @@ import org.elasticsearch.xpack.esql.stats.SearchStats; import org.elasticsearch.xpack.esql.type.EsqlDataTypes; import org.elasticsearch.xpack.ql.expression.Alias; +import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.Expressions; +import org.elasticsearch.xpack.ql.expression.ReferenceAttribute; import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; @@ -66,6 +69,7 @@ import static org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.StatsType; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -381,28 +385,94 @@ public boolean exists(String field) { assertThat(Expressions.names(localSource.output()), contains("count", "seen")); } + /** + * Expects + * LimitExec[500[INTEGER]] + * \_ExchangeExec[[],false] + * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n + * ame{f}#7, long_noidx{f}#12, salary{f}#8]] + * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] + * \_EsQueryExec[test], query[{"exists":{"field":"emp_no","boost":1.0}}][_doc{f}#13], limit[500], sort[] estimatedRowSize[324] + */ public void testIsNotNullPushdownFilter() { var plan = plan("from test | where emp_no is not null"); var limit = as(plan, LimitExec.class); var exchange = as(limit.child(), ExchangeExec.class); - var query = as(exchange.child(), EsQueryExec.class); + var project = as(exchange.child(), ProjectExec.class); + var field = as(project.child(), FieldExtractExec.class); + var query = as(field.child(), EsQueryExec.class); assertThat(query.limit().fold(), is(500)); var expected = QueryBuilders.existsQuery("emp_no"); assertThat(query.query().toString(), is(expected.toString())); } + /** + * Expects + * LimitExec[500[INTEGER]] + * \_ExchangeExec[[],false] + * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n + * ame{f}#7, long_noidx{f}#12, salary{f}#8]] + * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] + * \_EsQueryExec[test], query[{"bool":{"must_not":[{"exists":{"field":"emp_no","boost":1.0}}],"boost":1.0}}][_doc{f}#13], + * limit[500], sort[] estimatedRowSize[324] + */ public void testIsNullPushdownFilter() { var plan = plan("from test | where emp_no is null"); var limit = as(plan, LimitExec.class); var exchange = as(limit.child(), ExchangeExec.class); - var query = as(exchange.child(), EsQueryExec.class); + var project = as(exchange.child(), ProjectExec.class); + var field = as(project.child(), FieldExtractExec.class); + var query = as(field.child(), EsQueryExec.class); assertThat(query.limit().fold(), is(500)); var expected = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("emp_no")); assertThat(query.query().toString(), is(expected.toString())); } + /** + * Expects + * LimitExec[500[INTEGER]] + * \_ExchangeExec[[],false] + * \_ProjectExec[[_meta_field{f}#8, emp_no{r}#2, first_name{r}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, first_n + * ame{r}#3 AS last_name, long_noidx{f}#11, emp_no{r}#2 AS salary]] + * \_FieldExtractExec[_meta_field{f}#8, gender{f}#4, job{f}#9, job.raw{f}..] + * \_EvalExec[[null[INTEGER] AS emp_no, null[KEYWORD] AS first_name]] + * \_EsQueryExec[test], query[][_doc{f}#12], limit[500], sort[] estimatedRowSize[270] + */ + public void testMissingFieldsDoNotGetExtracted() { + var stats = EsqlTestUtils.statsForMissingField("first_name", "last_name", "emp_no", "salary"); + + var plan = plan("from test", stats); + var limit = as(plan, LimitExec.class); + var exchange = as(limit.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var projections = project.projections(); + assertThat( + Expressions.names(projections), + contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary") + ); + // emp_no + assertThat(projections.get(1), instanceOf(ReferenceAttribute.class)); + // first_name + assertThat(projections.get(2), instanceOf(ReferenceAttribute.class)); + + // last_name --> first_name + var nullAlias = unwrapAlias(projections.get(7)); + assertThat(Expressions.name(nullAlias), is("first_name")); + // salary --> emp_no + nullAlias = unwrapAlias(projections.get(9)); + assertThat(Expressions.name(nullAlias), is("emp_no")); + // check field extraction is skipped and that evaled fields are not extracted anymore + var field = as(project.child(), FieldExtractExec.class); + var fields = field.attributesToExtract(); + assertThat(Expressions.names(fields), contains("_meta_field", "gender", "job", "job.raw", "languages", "long_noidx")); + } + + private Expression unwrapAlias(Expression exp) { + return exp instanceof Alias as ? as.child() : exp; + } + private QueryBuilder wrapWithSingleQuery(QueryBuilder inner, String fieldName, Source source) { return FilterTests.singleValueQuery(inner, fieldName, source); } @@ -429,15 +499,15 @@ private PhysicalPlan plan(String query, SearchStats stats) { private PhysicalPlan optimizedPlan(PhysicalPlan plan, SearchStats searchStats) { // System.out.println("* Physical Before\n" + plan); - var p = EstimatesRowSize.estimateRowSize(0, physicalPlanOptimizer.optimize(plan)); - // System.out.println("* Physical After\n" + p); + var physicalPlan = EstimatesRowSize.estimateRowSize(0, physicalPlanOptimizer.optimize(plan)); + // System.out.println("* Physical After\n" + physicalPlan); // the real execution breaks the plan at the exchange and then decouples the plan // this is of no use in the unit tests, which checks the plan as a whole instead of each // individually hence why here the plan is kept as is var logicalTestOptimizer = new LocalLogicalPlanOptimizer(new LocalLogicalOptimizerContext(config, searchStats)); var physicalTestOptimizer = new TestLocalPhysicalPlanOptimizer(new LocalPhysicalOptimizerContext(config, searchStats), true); - var l = PlannerUtils.localPlan(plan, logicalTestOptimizer, physicalTestOptimizer); + var l = PlannerUtils.localPlan(physicalPlan, logicalTestOptimizer, physicalTestOptimizer); // handle local reduction alignment l = PhysicalPlanOptimizerTests.localRelationshipAlignment(l);