|  | @@ -14,12 +14,14 @@ import org.elasticsearch.common.util.set.Sets;
 | 
	
		
			
				|  |  |  import org.elasticsearch.core.Tuple;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.BoolQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.QueryBuilder;
 | 
	
		
			
				|  |  | +import org.elasticsearch.index.query.QueryBuilders;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.RangeQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.RegexpQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.TermQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.TermsQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.query.WildcardQueryBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.test.ESTestCase;
 | 
	
		
			
				|  |  | +import org.elasticsearch.test.junit.annotations.TestLogging;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.core.enrich.EnrichPolicy;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.EsqlTestUtils;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.analysis.Analyzer;
 | 
	
	
		
			
				|  | @@ -42,6 +44,7 @@ import org.elasticsearch.xpack.esql.plan.physical.EnrichExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec.FieldSort;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.EsSourceExec;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.EstimatesRowSize;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.EvalExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.ExchangeExec;
 | 
	
	
		
			
				|  | @@ -54,6 +57,7 @@ import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.ProjectExec;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.plan.physical.TopNExec;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xpack.esql.planner.FilterTests;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.planner.Mapper;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.planner.PhysicalVerificationException;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xpack.esql.planner.PlannerUtils;
 | 
	
	
		
			
				|  | @@ -91,6 +95,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.configuration;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.esql.EsqlTestUtils.statsForMissingField;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization;
 | 
	
		
			
				|  |  | +import static org.elasticsearch.xpack.esql.plan.physical.AggregateExec.Mode.FINAL;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.ql.expression.Expressions.name;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.ql.expression.Expressions.names;
 | 
	
		
			
				|  |  |  import static org.elasticsearch.xpack.ql.expression.Order.OrderDirection.ASC;
 | 
	
	
		
			
				|  | @@ -103,7 +108,7 @@ import static org.hamcrest.Matchers.instanceOf;
 | 
	
		
			
				|  |  |  import static org.hamcrest.Matchers.is;
 | 
	
		
			
				|  |  |  import static org.hamcrest.Matchers.nullValue;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -//@TestLogging(value = "org.elasticsearch.xpack.esql.optimizer.LocalLogicalPlanOptimizer:TRACE", reason = "debug")
 | 
	
		
			
				|  |  | +@TestLogging(value = "org.elasticsearch.xpack.esql.optimizer.LocalLogicalPlanOptimizer:TRACE", reason = "debug")
 | 
	
		
			
				|  |  |  public class PhysicalPlanOptimizerTests extends ESTestCase {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      private static final String PARAM_FORMATTING = "%1$s";
 | 
	
	
		
			
				|  | @@ -1844,7 +1849,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
 | 
	
		
			
				|  |  |          assertThat(limit.limit(), instanceOf(Literal.class));
 | 
	
		
			
				|  |  |          assertThat(limit.limit().fold(), equalTo(10000));
 | 
	
		
			
				|  |  |          var aggFinal = as(limit.child(), AggregateExec.class);
 | 
	
		
			
				|  |  | -        assertThat(aggFinal.getMode(), equalTo(AggregateExec.Mode.FINAL));
 | 
	
		
			
				|  |  | +        assertThat(aggFinal.getMode(), equalTo(FINAL));
 | 
	
		
			
				|  |  |          var aggPartial = as(aggFinal.child(), AggregateExec.class);
 | 
	
		
			
				|  |  |          assertThat(aggPartial.getMode(), equalTo(AggregateExec.Mode.PARTIAL));
 | 
	
		
			
				|  |  |          limit = as(aggPartial.child(), LimitExec.class);
 | 
	
	
		
			
				|  | @@ -1861,6 +1866,86 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
 | 
	
		
			
				|  |  |          assertThat(source.limit().fold(), equalTo(10));
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    // optimized doesn't know yet how to push down count over field
 | 
	
		
			
				|  |  | +    public void testCountOneFieldWithFilter() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where salary > 1000
 | 
	
		
			
				|  |  | +            | stats c = count(salary)
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +        assertThat(plan.anyMatch(EsQueryExec.class::isInstance), is(true));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // optimized doesn't know yet how to push down count over field
 | 
	
		
			
				|  |  | +    public void testCountOneFieldWithFilterAndLimit() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where salary > 1000
 | 
	
		
			
				|  |  | +            | limit 10
 | 
	
		
			
				|  |  | +            | stats c = count(salary)
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +        assertThat(plan.anyMatch(EsQueryExec.class::isInstance), is(true));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // optimized doesn't know yet how to break down different multi count
 | 
	
		
			
				|  |  | +    public void testCountMultipleFieldsWithFilter() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where salary > 1000 and emp_no > 10010
 | 
	
		
			
				|  |  | +            | stats cs = count(salary), ce = count(emp_no)
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +        assertThat(plan.anyMatch(EsQueryExec.class::isInstance), is(true));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testCountAllWithFilter() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where emp_no > 10010
 | 
	
		
			
				|  |  | +            | stats c = count()
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        var limit = as(plan, LimitExec.class);
 | 
	
		
			
				|  |  | +        var agg = as(limit.child(), AggregateExec.class);
 | 
	
		
			
				|  |  | +        assertThat(agg.getMode(), is(FINAL));
 | 
	
		
			
				|  |  | +        assertThat(Expressions.names(agg.aggregates()), contains("c"));
 | 
	
		
			
				|  |  | +        var exchange = as(agg.child(), ExchangeExec.class);
 | 
	
		
			
				|  |  | +        var esStatsQuery = as(exchange.child(), EsStatsQueryExec.class);
 | 
	
		
			
				|  |  | +        assertThat(esStatsQuery.limit(), is(nullValue()));
 | 
	
		
			
				|  |  | +        assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen"));
 | 
	
		
			
				|  |  | +        var expected = wrapWithSingleQuery(QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no");
 | 
	
		
			
				|  |  | +        assertThat(expected.toString(), is(esStatsQuery.query().toString()));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @AwaitsFix(bugUrl = "intermediateAgg does proper reduction but the agg itself does not - the optimizer needs to improve")
 | 
	
		
			
				|  |  | +    public void testMultiCountAllWithFilter() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where emp_no > 10010
 | 
	
		
			
				|  |  | +            | stats c = count(), call = count(*), c_literal = count(1)
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        var limit = as(plan, LimitExec.class);
 | 
	
		
			
				|  |  | +        var agg = as(limit.child(), AggregateExec.class);
 | 
	
		
			
				|  |  | +        assertThat(agg.getMode(), is(FINAL));
 | 
	
		
			
				|  |  | +        assertThat(Expressions.names(agg.aggregates()), contains("c", "call", "c_literal"));
 | 
	
		
			
				|  |  | +        var exchange = as(agg.child(), ExchangeExec.class);
 | 
	
		
			
				|  |  | +        var esStatsQuery = as(exchange.child(), EsStatsQueryExec.class);
 | 
	
		
			
				|  |  | +        assertThat(esStatsQuery.limit(), is(nullValue()));
 | 
	
		
			
				|  |  | +        assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen"));
 | 
	
		
			
				|  |  | +        var expected = wrapWithSingleQuery(QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no");
 | 
	
		
			
				|  |  | +        assertThat(expected.toString(), is(esStatsQuery.query().toString()));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // optimized doesn't know yet how to break down different multi count
 | 
	
		
			
				|  |  | +    public void testCountFieldsAndAllWithFilter() {
 | 
	
		
			
				|  |  | +        var plan = optimizedPlan(physicalPlan("""
 | 
	
		
			
				|  |  | +            from test
 | 
	
		
			
				|  |  | +            | where emp_no > 10010
 | 
	
		
			
				|  |  | +            | stats c = count(), cs = count(salary), ce = count(emp_no)
 | 
	
		
			
				|  |  | +            """));
 | 
	
		
			
				|  |  | +        assertThat(plan.anyMatch(EsQueryExec.class::isInstance), is(true));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private static EsQueryExec source(PhysicalPlan plan) {
 | 
	
		
			
				|  |  |          if (plan instanceof ExchangeExec exchange) {
 | 
	
		
			
				|  |  |              plan = exchange.child();
 | 
	
	
		
			
				|  | @@ -1915,4 +2000,8 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
 | 
	
		
			
				|  |  |          assertThat(sv.field(), equalTo(fieldName));
 | 
	
		
			
				|  |  |          return sv.next();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private QueryBuilder wrapWithSingleQuery(QueryBuilder inner, String fieldName) {
 | 
	
		
			
				|  |  | +        return FilterTests.singleValueQuery(inner, fieldName);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  }
 |