Quellcode durchsuchen

ESQL: Support filters on inlinestats (#132934)

Andrei Stefan vor 1 Monat
Ursprung
Commit
b968ec95c9

+ 5 - 0
docs/changelog/132934.yaml

@@ -0,0 +1,5 @@
+pr: 132934
+summary: Support filters on inlinestats
+area: ES|QL
+type: enhancement
+issues: []

+ 2 - 2
x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java

@@ -51,7 +51,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.ENABLE_LOOKUP_JOIN_ON_REMOTE;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.FORK_V9;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS;
-import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V9;
+import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V10;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V12;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_PLANNING_V1;
 import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.METADATA_FIELDS_REMOTE_TEST;
@@ -137,7 +137,7 @@ public class MultiClusterSpecIT extends EsqlSpecTestCase {
         assumeTrue("Test " + testName + " is skipped on " + oldVersion, isEnabled(testName, instructions, oldVersion));
         assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS.capabilityName()));
         assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_PLANNING_V1.capabilityName()));
-        assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V9.capabilityName()));
+        assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V10.capabilityName()));
         if (testCase.requiredCapabilities.contains(JOIN_LOOKUP_V12.capabilityName())) {
             assumeTrue(
                 "LOOKUP JOIN not yet supported in CCS",

+ 1023 - 89
x-pack/plugin/esql/qa/testFixtures/src/main/resources/inlinestats.csv-spec

@@ -1,9 +1,5 @@
-//
-// TODO: re-enable the commented tests once the Join functionality stabilizes
-//
-
 allFieldsReturned
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM hosts METADATA _index
 | INLINESTATS c = COUNT(*) BY host_group
@@ -16,7 +12,7 @@ eth0           |epsilon gw instance|epsilon        |[fe80::cae2:65ff:fece:feb9,
 ;
 
 maxOfInt
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 // tag::max-languages[]
 FROM employees
 | KEEP emp_no, languages
@@ -38,7 +34,7 @@ emp_no:integer | languages:integer | max_lang:integer
 ;
 
 maxOfIntByKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender
@@ -56,7 +52,7 @@ emp_no:integer | languages:integer | max_lang:integer | gender:keyword
 ;
 
 maxOfLongByKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, avg_worked_seconds, gender
@@ -71,7 +67,7 @@ emp_no:integer | avg_worked_seconds:long | max_avg_worked_seconds:long | gender:
 ;
 
 maxOfLong
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, avg_worked_seconds, gender
@@ -84,7 +80,7 @@ emp_no:integer | avg_worked_seconds:long | gender:keyword | max_avg_worked_secon
 ;
 
 maxOfLongByCalculatedKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 // tag::longest-tenured-by-first[]
 FROM employees
@@ -107,7 +103,7 @@ emp_no:integer | avg_worked_seconds:long | last_name:keyword | max_avg_worked_se
 ;
 
 maxOfLongByCalculatedNamedKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, avg_worked_seconds, last_name
@@ -126,7 +122,7 @@ emp_no:integer | avg_worked_seconds:long | last_name:keyword | max_avg_worked_se
 ;
 
 maxOfLongByCalculatedDroppedKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS max_avg_worked_seconds = MAX(avg_worked_seconds) BY l = SUBSTRING(last_name, 0, 1)
@@ -145,7 +141,7 @@ emp_no:integer | avg_worked_seconds:long | last_name:keyword | max_avg_worked_se
 ;
 
 maxOfLongByEvaledKeyword
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | EVAL l = SUBSTRING(last_name, 0, 1)
@@ -165,7 +161,7 @@ emp_no:integer | avg_worked_seconds:long | max_avg_worked_seconds:long | l:keywo
 ;
 
 maxOfLongByInt
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, avg_worked_seconds, languages
@@ -183,7 +179,7 @@ emp_no:integer | avg_worked_seconds:long | max_avg_worked_seconds:long | languag
 ;
 
 maxOfLongByIntDouble
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, avg_worked_seconds, languages, height
@@ -201,7 +197,7 @@ emp_no:integer | avg_worked_seconds:long | max_avg_worked_seconds:long | languag
 ;
 
 two
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, avg_worked_seconds, gender
@@ -225,8 +221,7 @@ emp_no:integer |avg_worked_seconds:long|avg_avg_worked_seconds:double|languages:
 ;
 
 three
-required_capability: inlinestats_v9
-// used to fail with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, avg_worked_seconds, gender
@@ -253,7 +248,7 @@ emp_no:integer |avg_worked_seconds:long|avg_avg_worked_seconds:double|languages:
 
 // TODO: INLINESTATS unit test needed for this one
 pushDownSort_To_LeftSideOnly
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | sort emp_no
@@ -271,7 +266,7 @@ from employees
 ;
 
 byMultivaluedSimple
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 // tag::mv-group[]
 FROM airports
@@ -289,7 +284,7 @@ abbrev:keyword |  type:keyword   | scalerank:integer | min_scalerank:integer
 ;
 
 byMultivaluedMvExpand
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 // tag::mv-expand[]
 FROM airports
@@ -309,7 +304,7 @@ GWL            |9                  |4                      |military
 ;
 
 byMvExpand
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 // tag::extreme-airports[]
 FROM airports
@@ -338,7 +333,7 @@ FROM airports
 ;
 
 mvMinMvExpand
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | EVAL original_type = type
@@ -361,7 +356,7 @@ ZAR            |Zaria          |POINT (7.7 11.0667)     |Nigeria        |POINT (
 ;
 
 afterStats
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | STATS count=COUNT(*) BY country
@@ -384,7 +379,7 @@ count:long | country:keyword | avg:double
 ;
 
 afterWhere
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | WHERE country != "United States"
@@ -402,7 +397,7 @@ abbrev:keyword | country:keyword | count:long
 ;
 
 afterLookup
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 required_capability: join_lookup_v12
 
 FROM airports
@@ -426,7 +421,7 @@ ZNZ            |4                       |German
 ;
 
 afterEnrich
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 required_capability: enrich_load
 
 FROM airports
@@ -447,7 +442,7 @@ abbrev:keyword | city:keyword | "COUNT(*)":long |       region:text
 ;
 
 beforeStats
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | EVAL lat = ST_Y(location)
@@ -460,7 +455,7 @@ northern:long | southern:long
 ;
 
 beforeKeepSort
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS max_salary = MAX(salary) by languages
@@ -475,7 +470,7 @@ emp_no:integer | languages:integer | max_salary:integer
 ;
 
 beforeKeepWhere
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS max_salary = MAX(salary) by languages
@@ -488,7 +483,7 @@ emp_no:integer | languages:integer | max_salary:integer
 ;
 
 beforeEnrich
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 required_capability: enrich_load
 
 FROM airports
@@ -507,7 +502,7 @@ ACA            |Acapulco de Juárez|385            |major          |Acapulco de
 ;
 
 beforeAndAfterEnrich
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 required_capability: enrich_load
 
 FROM airports
@@ -530,7 +525,7 @@ ALL            |Albenga        |499            |mid            |1
 ;
 
 shadowing
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right"
 | INLINESTATS env = VALUES(right) BY client_ip
@@ -541,7 +536,7 @@ left         | right          | right       | 172.21.0.5
 ;
 
 shadowingMulti
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW left = "left", airport = "Zurich Airport ZRH", city = "Zürich", middle = "middle", region = "North-East Switzerland", right = "right"
 | INLINESTATS airport=VALUES(left), region=VALUES(left), city_boundary=VALUES(left) BY city
@@ -552,7 +547,7 @@ left         | middle         | right         |            left |           left
 ;
 
 shadowingSelf
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW city = "Raleigh"
 | INLINESTATS city = COUNT(city)
@@ -563,7 +558,7 @@ city:long
 ;
 
 shadowingSelfBySelf
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW city = "Raleigh"
 | INLINESTATS city = COUNT(city) BY city
@@ -575,7 +570,7 @@ Raleigh
 ;
 
 shadowingInternal
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW city = "Zürich"
 | INLINESTATS x = VALUES(city), x = VALUES(city)
@@ -587,7 +582,7 @@ Zürich       | Zürich
 ;
 
 multiInlinestatsWithRow
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 row x = 1
 | inlinestats x = max(x) + min(x)
@@ -601,7 +596,7 @@ row x = 1
 ;
 
 ignoreUnusedEvaledValue_AndInlineStats
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW x = 1
 | INLINESTATS max(x)
@@ -614,7 +609,7 @@ x:integer
 ;
 
 ignoreUnusedEvaledValue_AndInlineStats2
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW x = 1, z = 2
 | INLINESTATS max(x)
@@ -627,7 +622,7 @@ x:integer | z:integer
 ;
 
 ignoreUnusedEvaledValue_AndInlineStats3
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | inlinestats max(salary)
@@ -642,7 +637,7 @@ from employees
 ;
 
 ignoreUnusedEvaledValue_AndInlineStats4
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | inlinestats max(salary), m = min(salary) by gender
@@ -657,7 +652,7 @@ emp_no:integer
 ;
 
 ignoreUnusedEvaledValue_AndInlineStats5
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | inlinestats max(salary), m = min(salary) by gender
@@ -672,7 +667,7 @@ emp_no:integer
 ;
 
 shadowEntireInlinestats
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS x = avg(salary), y = min(salary) BY emp_no
@@ -687,7 +682,7 @@ x:integer |y:integer |emp_no:integer
 ;
 
 byConstant
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages
@@ -706,7 +701,7 @@ emp_no:integer | languages:integer | max_lang:integer | y:integer
 ;
 
 aggConstant
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no
@@ -724,7 +719,7 @@ one:integer | emp_no:integer
 ;
 
 percentile
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, salary
@@ -743,7 +738,7 @@ emp_no:integer | salary:integer | ninety_fifth_salary:double
 ;
 
 byTwoCalculated
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | WHERE abbrev IS NOT NULL
@@ -763,7 +758,7 @@ abbrev:keyword | scalerank:integer |             location:geo_point
 
 byTwoCalculatedSecondOverwrites
 required_capability: stats_alias_collision_warnings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | WHERE abbrev IS NOT NULL
@@ -784,7 +779,7 @@ abbrev:keyword | scalerank:integer |             location:geo_point
 
 byTwoCalculatedSecondOverwritesReferencingFirst
 required_capability: stats_alias_collision_warnings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | WHERE abbrev IS NOT NULL
@@ -807,7 +802,7 @@ abbrev:keyword | scalerank:integer |             location:geo_point
 
 groupShadowsAgg
 required_capability: stats_alias_collision_warnings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM airports
 | WHERE abbrev IS NOT NULL
@@ -827,7 +822,7 @@ abbrev:keyword | scalerank:integer |             location:geo_point
 ;
 
 groupShadowsField
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
   FROM employees
 | KEEP emp_no, salary, hire_date
@@ -846,7 +841,7 @@ emp_no:integer | salary:integer | avg_salary:double |  hire_date:datetime
 ;
 
 groupByExpression_And_ExistentField
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 FROM employees
 | KEEP emp_no, languages, gender
 | EVAL x = "ABC"
@@ -864,7 +859,7 @@ emp_no:integer | languages:integer | x:keyword | max_lang:integer | y:keyword |
 ;
 
 groupByRenamedColumn
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 FROM employees
 | KEEP emp_no, languages, gender
 | INLINESTATS max_lang = MAX(languages) BY y = gender
@@ -881,9 +876,8 @@ emp_no:integer | languages:integer | gender:keyword | max_lang:integer | y:keywo
          10014 |                 5 | null           | 5                | null
 ;
 
-// fails with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
 groupByMultipleRenamedColumns_AndOneExpression_Last
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, first_name
@@ -905,9 +899,8 @@ emp_no:integer | languages:integer | gender:keyword|first_name:keyword|max_lang:
 10010          |4                  |null           |Duangkaew         |4               |null           |4              |D              
 ;
 
-// fails with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
 groupByMultipleRenamedColumns_AndTwoExpressions
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, first_name
@@ -929,9 +922,8 @@ emp_no:integer | languages:integer | gender:keyword|first_name:keyword|max_lang:
 10010          |4                  |null           |Duangkaew         |4               |D              |null           |D              |4              
 ;
 
-// fails with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
 groupByMultipleRenamedColumns_AndMultipleRenames
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, first_name
@@ -954,9 +946,8 @@ emp_no:integer | languages:integer | gender:keyword|    f:keyword     |max_lang:
 10010          |4                  |null           |Duangkaew         |4               |null           |4              |D              
 ;
 
-// fails with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
 groupByMultipleRenamedColumns_AndSameNameExpressionGroupingOverride
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, first_name
@@ -980,7 +971,7 @@ emp_no:integer | languages:integer | gender:keyword|max_lang:integer|  y:keyword
 ;
 
 twoAggregatesGroupedBy_AField_And_AnExpression
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, last_name
@@ -1002,7 +993,7 @@ emp_no:integer |languages:integer|last_name:keyword|max_lang:integer|min_lang:in
 ;
 
 groupByMultipleRenamedColumns_InversedOrder
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, still_hired, gender
@@ -1020,7 +1011,7 @@ emp_no:integer |languages:integer|still_hired:boolean| gender:keyword|max_lang:i
 ;
 
 groupByMultipleRenamedColumns_InversedOrder_ComplexEval
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, still_hired, gender
@@ -1039,7 +1030,7 @@ emp_no:integer |languages:integer|still_hired:boolean| gender:keyword|multilingu
 ;
 
 groupByMultipleRenamedColumns_AndComplexEval
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, still_hired, gender
@@ -1057,9 +1048,8 @@ emp_no:integer |languages:integer|still_hired:boolean| gender:keyword|multilingu
 10005          |1                |true               |M              |monolingual         |1               |M              |true           |monolingual
 ;
 
-// fails with AssertionError at org.elasticsearch.xpack.esql.plan.logical.Limit.writeTo(Limit.java:70)
 groupByMultipleRenamedColumns_AndConstantValue
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, first_name
@@ -1083,7 +1073,7 @@ emp_no:integer |languages:integer|gender:keyword |first_name:keyword  |   x:keyw
 ;
 
 groupByRenamedExpression
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP emp_no, languages, gender, last_name
@@ -1105,7 +1095,7 @@ emp_no:integer |languages:integer|last_name:keyword|max_lang:integer|min_lang:in
 ;
 
 doubleFilterOnLeftAndRight_InlineStats_Sides
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
   
 FROM employees
 | INLINESTATS max_salary = MAX(salary), min_salary = MIN(salary) by languages
@@ -1126,7 +1116,7 @@ emp_no:integer |languages:integer|salary:integer |max_salary:integer|min_salary:
 ;
 
 filterOnInlineStatsAggs
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS max_salary = MAX(salary), min_salary = MIN(salary) by languages
@@ -1145,7 +1135,7 @@ emp_no:integer |languages:integer|salary:integer |max_salary:integer|min_salary:
 ;
 
 filterOnInlineStatsAggsValues_And_Groupings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS max_salary = MAX(salary), min_salary = MIN(salary) by languages
@@ -1164,7 +1154,7 @@ emp_no:integer |languages:integer|salary:integer |max_salary:integer|min_salary:
 ;
 
 inlineStatsOverrideEVALed_FieldWithSameName
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM hosts METADATA _index
 | EVAL x = ip1
@@ -1178,7 +1168,7 @@ beta k8s server |beta           |127.0.0.1      |hosts           |127.0.0.2|2
 ;
 
 doubleShadowing
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS salary = min(salary) BY gender
@@ -1197,7 +1187,7 @@ salary:integer |gender:keyword
 ;
 
 doubleShadowing_WithIntertwinedFilters
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | WHERE salary > 30000
@@ -1222,7 +1212,7 @@ salary:integer |gender:keyword
 ;
 
 shadowingAggregateByNextGrouping
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP gender, languages, emp_no, salary
@@ -1239,7 +1229,7 @@ emp_no:integer |salary:integer |languages:integer|avg(salary):double|gender:long
 ;
 
 doubleShadowingWithEval
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | eval salary = salary/100
@@ -1259,7 +1249,7 @@ salary:integer|gender:keyword
 ;
 
 doubleShadowingWithDoubleStats
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 from employees
 | stats salary=min(salary) by gender
@@ -1276,7 +1266,7 @@ M              |25324
 ;
 
 renamingGroupingWithItself
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | EVAL x = gender
@@ -1295,7 +1285,7 @@ salary:integer |x:keyword|gender:keyword |min_sl:integer |emp_no:integer
 ;
 
 overridingGroupings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS min_sl = MIN(salary) BY x = gender, x = languages
@@ -1314,7 +1304,7 @@ salary:integer |x:integer      |gender:keyword |min_sl:integer |emp_no:integer
 ;
 
 overridingExpressionGroupings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | INLINESTATS min_sl = MIN(salary) BY x = TO_LOWER(gender), x = CONCAT(gender, gender)
@@ -1333,7 +1323,7 @@ salary:integer |x:keyword      |gender:keyword |min_sl:integer |emp_no:integer
 ;
 
 reusingEvalExpressions_UsedInGroupings
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | KEEP salary, gender, emp_no
@@ -1352,7 +1342,7 @@ salary:integer |gender:keyword |emp_no:integer |min_sl:integer |       x:keyword
 ;
 
 statsBeforeInlinestatsWithTopAndBucket1
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM books
 | STATS avg_rating = AVG(ratings) BY decade = BUCKET(year, 10)
@@ -1372,7 +1362,7 @@ avg_rating:double | decade:double |    decades:double
 ;
 
 statsBeforeInlinestatsWithTopAndBucket2
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM sample_data
 | STATS total_duration = SUM(event_duration) BY day = BUCKET(@timestamp, 1 HOUR)
@@ -1388,7 +1378,7 @@ total_duration:long |       day:date         |                     days:date
 
 
 evalBeforeInlinestatsAndKeepAfter1
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | WHERE still_hired == false
@@ -1408,7 +1398,7 @@ emp_no:integer |still_hired:boolean|totalK:long|count:long
 ;
 
 evalBeforeInlinestatsAndKeepAfter2
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | EVAL salaryK = salary/1000
@@ -1428,7 +1418,7 @@ emp_no:integer |still_hired:boolean|total:long|count:long
 ;
 
 evalBeforeInlinestatsAndKeepAfter3
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | EVAL salaryK = salary/1000
@@ -1447,7 +1437,7 @@ emp_no:integer |still_hired:boolean|total:long
 ;
 
 evalBeforeInlinestatsAndKeepAfter4
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 FROM employees
 | EVAL salaryK = salary/1000
@@ -1466,7 +1456,7 @@ emp_no:integer |still_hired:boolean|count:long
 ;
 
 evalBeforeInlinestatsAndKeepAfter5
-required_capability: inlinestats_v9
+required_capability: inlinestats_v10
 
 ROW salary = 12300, emp_no = 5, gender = "F"
 | EVAL salaryK = salary/1000
@@ -1477,3 +1467,947 @@ ROW salary = 12300, emp_no = 5, gender = "F"
 emp_no:integer
 5
 ;
+///////////////////////////
+// inlinestats with filters
+///////////////////////////
+
+doubleFilterOnInlineStats
+required_capability: inlinestats_v10
+
+from employees
+| keep salary, gender
+| inlinestats max1 = max(salary) where salary < 60000,
+              max2 = max(salary) where salary < 70000 AND salary >= 60000,
+              max3 = max(salary) where salary >= 70000
+  by gender
+| sort salary desc
+| limit 10
+;
+
+salary:integer |  max1:integer |  max2:integer |  max3:integer | gender:keyword     
+74999          |58121          |68547          |74999          |M              
+74970          |58121          |68547          |74999          |M              
+74572          |56415          |69904          |74572          |F              
+73851          |56415          |69904          |74572          |F              
+73717          |58715          |61358          |73717          |null           
+73578          |56415          |69904          |74572          |F              
+71165          |58121          |68547          |74999          |M              
+70011          |58121          |68547          |74999          |M              
+69904          |56415          |69904          |74572          |F              
+68547          |58121          |68547          |74999          |M
+;
+
+inlinestatsWithFiltering
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary), max_f = max(salary) where salary < 50000, max_a = max(salary) where salary > 100,
+        min = min(salary), min_f = min(salary) where salary > 50000, min_a = min(salary) where salary > 100
+| keep max*, min*, salary
+| sort salary asc
+| limit 3
+;
+
+max:integer |max_f:integer |max_a:integer | min:integer | min_f:integer | min_a:integer | salary:integer
+74999       |49818         |74999         |25324        |50064          |25324          |25324          
+74999       |49818         |74999         |25324        |50064          |25324          |25945          
+74999       |49818         |74999         |25324        |50064          |25324          |25976          
+;
+
+inlinestatsWithEverythingFiltered
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary), max_a = max(salary) where salary < 100,
+        min = min(salary), min_a = min(salary) where salary > 99999
+| keep max*, min*, emp_no*, salary
+| sort emp_no desc
+| limit 5
+;
+
+max:integer |max_a:integer|min:integer | min_a:integer | emp_no:integer|salary:integer
+74999       |null         |25324       |null           |10100          |68431          
+74999       |null         |25324       |null           |10099          |73578          
+74999       |null         |25324       |null           |10098          |44817          
+74999       |null         |25324       |null           |10097          |71165          
+74999       |null         |25324       |null           |10096          |43889
+;
+
+inlinestatsWithNullFilter
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary), max_a = max(salary) where null,
+        min = min(salary), min_a = min(salary) where to_string(null) == "abc"
+| keep max*, min*, emp_no
+| sort emp_no
+| limit 3
+;
+
+max:integer |max_a:integer|min:integer | min_a:integer | emp_no:integer
+74999       |null         |25324       |null           |10001          
+74999       |null         |25324       |null           |10002          
+74999       |null         |25324       |null           |10003        
+;
+
+inlinestatsWithAllFiltersFalse
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(height.float) where false,
+        min = min(height.float) where to_string(null) == "abc",
+        count = count(height.float) where false,
+        count_distinct = count_distinct(salary) where to_string(null) == "def"
+| sort emp_no desc
+| keep emp_no, salary, max, min, count*
+| limit 3
+;
+
+emp_no:integer|salary:integer| max:double |min:double |count:long |count_distinct:long
+10100         |68431         |null        |null       |0          |0
+10099         |73578         |null        |null       |0          |0              
+10098         |44817         |null        |null       |0          |0
+;
+
+inlinestatsWithAllFiltersFalse_GroupByOneField
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(height.float) where false, 
+        min = min(height.float) where to_string(null) == "abc", 
+        count = count(height.float) where false, 
+        count_distinct = count_distinct(salary) where to_string(null) == "def" by gender
+| sort emp_no desc
+| keep emp_no, salary, max, min, count*, gender
+| limit 5
+;
+
+    emp_no:i   |    salary:i   |      max:d    |      min:d    |     count:l   |count_distinct:l|    gender:s     
+10100          |68431          |null           |null           |0              |0               |F              
+10099          |73578          |null           |null           |0              |0               |F              
+10098          |44817          |null           |null           |0              |0               |F              
+10097          |71165          |null           |null           |0              |0               |M              
+10096          |43889          |null           |null           |0              |0               |M              
+;
+
+inlinestatsWithAllFiltersFalse_GroupByTwoFields
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(height.float) where false, 
+        min = min(height.float) where to_string(null) == "abc", 
+        count = count(height.float) where false, 
+        count_distinct = count_distinct(salary) where to_string(null) == "def" by gender, languages
+| sort emp_no desc
+| keep emp_no, salary, max, min, count*, gender, languages
+| limit 5
+;
+
+    emp_no:i   |    salary:i   |      max:d    |      min:d    |     count:l   |count_distinct:l|    gender:s  |languages:i   
+10100          |68431          |null           |null           |0              |0               |F              |4              
+10099          |73578          |null           |null           |0              |0               |F              |2              
+10098          |44817          |null           |null           |0              |0               |F              |4              
+10097          |71165          |null           |null           |0              |0               |M              |3              
+10096          |43889          |null           |null           |0              |0               |M              |4              
+;
+
+prunedInlinestatsFollowedByInlinestats_GroupByOneFieldEach_DifferentFields
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(my_length) where false,
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0 by languages
+| keep emp_no, first_name, *length, count, values, languages, gender
+| inlinestats count_distinct = count_distinct(count) by gender
+| sort emp_no
+| limit 3
+;
+
+    emp_no:i   |  first_name:s |   my_length:i |     count:l   |    values:s   |   languages:i |count_distinct:l|    gender:s     
+10001          |Georgi         |null           |0              |null           |2              |1               |M              
+10002          |Bezalel        |null           |0              |null           |5              |1               |F              
+10003          |Parto          |null           |0              |null           |4              |1               |M              
+;
+
+prunedInlinestatsFollowedByInlinestats_GroupByOneFieldEach_SameFields
+required_capability: inlinestats_v10
+from employees 
+| eval my_length = length(concat(first_name, null)) 
+| inlinestats count = count(my_length) where false,
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0 by languages
+| keep emp_no, first_name, *length, count, values, languages, gender
+| inlinestats count_distinct = count_distinct(count) by languages
+| sort emp_no
+| limit 3
+;
+
+    emp_no:i   |  first_name:s |   my_length:i |     count:l   |    values:s   |   gender:s    |count_distinct:l|   languages:i   
+10001          |Georgi         |null           |0              |null           |M              |1               |2              
+10002          |Bezalel        |null           |0              |null           |F              |1               |5              
+10003          |Parto          |null           |0              |null           |M              |1               |4             
+;
+
+prunedInlinestatsFollowedByInlinestats_GroupByOneFieldOnSecondInlinestats-Ignore
+// values doesn't end up as null
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(my_length) where false, 
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0
+| keep emp_no, first_name, *length, count, values, languages, gender
+| inlinestats count_distinct = count_distinct(count) by languages
+| sort emp_no
+| limit 3
+; 
+
+    emp_no:i   |  first_name:s |   my_length:i |     count:l   |    values:s |    gender:s   |count_distinct:l|   languages:i   
+10001          |Georgi         |null           |0              |null         |M              |1               |2              
+10002          |Bezalel        |null           |0              |null         |F              |1               |5              
+10003          |Parto          |null           |0              |null         |M              |1               |4             
+; 
+
+partial_PrunedInlinestatsFollowedByInlinestats_GroupByOneFieldOnFirstInlinestats
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(is_rehired) where true, 
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0 by gender
+| keep emp_no, first_name, *length, count, values, languages, gender
+| inlinestats count_distinct = count_distinct(count)
+| sort emp_no
+| limit 10
+; 
+
+    emp_no:i   |  first_name:s |   my_length:i |     count:l   |    values:s   |   languages:i |    gender:s   |count_distinct:l 
+10001          |Georgi         |null           |111            |null           |2              |M              |3              
+10002          |Bezalel        |null           |65             |null           |5              |F              |3              
+10003          |Parto          |null           |111            |null           |4              |M              |3              
+10004          |Chirstian      |null           |111            |null           |5              |M              |3              
+10005          |Kyoichi        |null           |111            |null           |1              |M              |3              
+10006          |Anneke         |null           |65             |null           |3              |F              |3              
+10007          |Tzvetan        |null           |65             |null           |4              |F              |3              
+10008          |Saniya         |null           |111            |null           |2              |M              |3              
+10009          |Sumant         |null           |65             |null           |1              |F              |3              
+10010          |Duangkaew      |null           |28             |null           |4              |null           |3              
+;
+
+
+partial_PrunedInlinestatsFollowedByInlinestats_GroupByOneFieldOnFirstInlinestats2
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(is_rehired) where true, 
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0,
+        count_d = count_distinct(is_rehired) by gender
+| keep emp_no, first_name, *length, count*, values, languages, gender
+| inlinestats count_distinct = count_distinct(count)
+| sort emp_no
+| limit 10
+; 
+
+    emp_no:i   |  first_name:s |   my_length:i |     count:l   |    count_d:l  |    values:s   |   languages:i |    gender:s   |count_distinct:l 
+10001          |Georgi         |null           |111            |2              |null           |2              |M              |3              
+10002          |Bezalel        |null           |65             |2              |null           |5              |F              |3              
+10003          |Parto          |null           |111            |2              |null           |4              |M              |3              
+10004          |Chirstian      |null           |111            |2              |null           |5              |M              |3              
+10005          |Kyoichi        |null           |111            |2              |null           |1              |M              |3              
+10006          |Anneke         |null           |65             |2              |null           |3              |F              |3              
+10007          |Tzvetan        |null           |65             |2              |null           |4              |F              |3              
+10008          |Saniya         |null           |111            |2              |null           |2              |M              |3              
+10009          |Sumant         |null           |65             |2              |null           |1              |F              |3              
+10010          |Duangkaew      |null           |28             |2              |null           |4              |null           |3              
+;
+
+
+inlinestatsWithExpressionsAllFiltersFalse
+required_capability: inlinestats_v10
+from employees
+| keep height.f*, emp_no
+| sort emp_no desc
+| inlinestats max = max(height.float + 1) where null,
+        count = count(height.float) + 2 where false,
+        mix = min(height.float + 1) + count_distinct(emp_no) + 2 where length(null) == 3
+| limit 3
+| drop height.float
+;
+
+    emp_no:i   |      max:d    |     count:l   |      mix:d      
+10100          |null           |2              |null           
+10099          |null           |2              |null           
+10098          |null           |2              |null           
+;
+
+inlinestatsWithFalseFilterAndGroup
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(height.float + 1) where null,
+        count = count(height.float) + 2 where false
+  by job_positions
+| sort emp_no desc
+| limit 4
+| keep emp_no, job_positions, height.float, max, count
+;
+
+    emp_no:i   |          job_positions:s                |   height.float:d |      max:d    |     count:l     
+10100          |Purchase Manager                         |1.7699999809265137|null           |2              
+10099          |null                                     |1.809999942779541 |null           |2              
+10098          |[Architect, Internship, Senior Team Lead]|2.0               |null           |[2, 2, 2]      
+10097          |[Reporting Analyst, Tech Lead]           |1.5299999713897705|null           |[2, 2]         
+;
+
+inlinestatsWithFalseFiltersAndGroups
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count_distinct = count_distinct(height.float + 1) where null,
+        count = count(height.float) + 2 where false,
+        values = values(first_name) where my_length > 3
+  by job_positions, is_rehired
+| sort emp_no desc
+| limit 10
+| keep emp_no, my_length, count*, values, job_positions, is_rehired
+;
+
+ emp_no:integer|my_length:integer|count_distinct:long|     count:long   |values:keyword |                            job_positions:keyword                           |    is_rehired:boolean        
+10100          |null             |[0, 0]             |[2, 2]            |null           |Purchase Manager                                                            |[false, false, true, true]
+10099          |null             |0                  |2                 |null           |null                                                                        |[true, true]              
+10098          |null             |[0, 0, 0]          |[2, 2, 2]         |null           |[Architect, Internship, Senior Team Lead]                                   |false                     
+10097          |null             |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Reporting Analyst, Tech Lead]                                              |[false, true]             
+10096          |null             |[0, 0]             |[2, 2]            |null           |[Architect, Reporting Analyst]                                              |[false, false, false]     
+10095          |null             |[0, 0]             |[2, 2]            |null           |null                                                                        |[false, false, true, true]
+10094          |null             |[0, 0, 0, 0, 0, 0] |[2, 2, 2, 2, 2, 2]|null           |[Accountant, Principal Support Engineer, Senior Python Developer]           |[false, true, true]       
+10093          |null             |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Principal Support Engineer, Purchase Manager, Reporting Analyst, Tech Lead]|null                      
+10092          |null             |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Accountant, Junior Developer]                                              |[false, false, true, true]
+10091          |null             |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Python Developer, Reporting Analyst]                                       |[false, false, true, true]
+;
+
+inlinestatsWithFalseFiltersAndGroups_DropEvaledValue
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count_distinct = count_distinct(height.float + 1) where null,
+        count = count(height.float) + 2 where false,
+        values = values(first_name) where my_length > 3
+  by job_positions, is_rehired
+| sort emp_no desc
+| limit 10
+| keep emp_no, count*, values, job_positions, is_rehired
+;
+
+
+ emp_no:integer|count_distinct:long|     count:long   |values:keyword |                            job_positions:keyword                           |    is_rehired:boolean        
+10100          |[0, 0]             |[2, 2]            |null           |Purchase Manager                                                            |[false, false, true, true]
+10099          |0                  |2                 |null           |null                                                                        |[true, true]              
+10098          |[0, 0, 0]          |[2, 2, 2]         |null           |[Architect, Internship, Senior Team Lead]                                   |false                     
+10097          |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Reporting Analyst, Tech Lead]                                              |[false, true]             
+10096          |[0, 0]             |[2, 2]            |null           |[Architect, Reporting Analyst]                                              |[false, false, false]     
+10095          |[0, 0]             |[2, 2]            |null           |null                                                                        |[false, false, true, true]
+10094          |[0, 0, 0, 0, 0, 0] |[2, 2, 2, 2, 2, 2]|null           |[Accountant, Principal Support Engineer, Senior Python Developer]           |[false, true, true]       
+10093          |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Principal Support Engineer, Purchase Manager, Reporting Analyst, Tech Lead]|null                      
+10092          |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Accountant, Junior Developer]                                              |[false, false, true, true]
+10091          |[0, 0, 0, 0]       |[2, 2, 2, 2]      |null           |[Python Developer, Reporting Analyst]                                       |[false, false, true, true]
+;
+
+inlinestatsWithMixedFiltersAndGroup
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(my_length) where false,
+        values = mv_slice(mv_sort(values(first_name)), 0, 1)
+  by job_positions
+| sort emp_no
+| limit 4
+| keep *length, count, values, job_positions, emp_no
+;
+
+my_length:integer| count:long    |                            values:keyword                             |                        job_positions:keyword                         |emp_no:integer     
+null             |[0, 0]         |[Arumugam, Bojan, Arumugam, Basil]                                     |[Accountant, Senior Python Developer]                                 |10001          
+null             |0              |[Alejandro, Anneke]                                                    |Senior Team Lead                                                      |10002          
+null             |0              |[Gino, Heping]                                                         |null                                                                  |10003          
+null             |[0, 0, 0, 0]   |[Berni, Chirstian, Amabile, Berni, Bojan, Chirstian, Anneke, Chirstian]|[Head Human Resources, Reporting Analyst, Support Engineer, Tech Lead]|10004          
+;
+
+prunedInlinestatsFollowedByinlinestats-Ignore
+// values doesn't end up as null
+required_capability: inlinestats_v10
+from employees
+| eval my_length = length(concat(first_name, null))
+| inlinestats count = count(my_length) where false,
+        values = mv_slice(values(first_name), 0, 1) where my_length > 0
+| keep emp_no, first_name, *length, count, values
+| inlinestats count_distinct = count_distinct(count)
+| sort emp_no
+| limit 3
+;
+
+    emp_no:i   |  first_name:s |  my_length:i  |     count:l   | values:s |count_distinct:l
+10001          |Georgi         |null           |0              |null      |1              
+10002          |Bezalel        |null           |0              |null      |1              
+10003          |Parto          |null           |0              |null      |1              
+;
+
+inlinestatsWithFalseFiltersFromRow
+// is this result correct? The stats variant of this query has this result:
+//  c:integer |b:integer
+//  null      |2
+//  null      |3
+//  null      |4
+required_capability: inlinestats_v10
+row x = null, a = 1, b = [2,3,4]
+| inlinestats c=max(a) where x
+  by b
+;
+
+    x:null     |   a:integer   |   c:integer   |  b:integer       
+null           |1              |null           |[2, 3, 4]
+;
+
+inlinestatsWithBasicExpressionFiltered
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary), max_f = max(salary) where salary < 50000,
+        min = min(salary), min_f = min(salary) where salary > 50000,
+        exp_p = max(salary) + 10000 where salary < 50000,
+        exp_m = min(salary) % 10000 where salary > 50000
+| keep max*, min*, exp*, emp_no | sort emp_no | limit 3
+;
+
+      max:i    |     max_f:i   |      min:i    |     min_f:i   |     exp_p:i   |     exp_m:i   |    emp_no:i     
+74999          |49818          |25324          |50064          |59818          |64             |10001          
+74999          |49818          |25324          |50064          |59818          |64             |10002          
+74999          |49818          |25324          |50064          |59818          |64             |10003          
+;
+
+inlinestatsWithExpressionOverFilters
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary), max_f = max(salary) where salary < 50000,
+        min = min(salary), min_f = min(salary) where salary > 50000,
+        exp_gt = max(salary) - min(salary) where salary > 50000,
+        exp_lt = max(salary) - min(salary) where salary < 50000
+| keep max*, min*, exp*, emp_no
+| sort emp_no
+| limit 3          
+;
+
+      max:i    |     max_f:i   |      min:i    |     min_f:i   |    exp_gt:i   |    exp_lt:i   |    emp_no:i     
+74999          |49818          |25324          |50064          |24935          |24494          |10001          
+74999          |49818          |25324          |50064          |24935          |24494          |10002          
+74999          |49818          |25324          |50064          |24935          |24494          |10003          
+;
+
+
+inlinestatsWithExpressionOfExpressionsOverFilters
+required_capability: inlinestats_v10
+from employees
+| inlinestats max = max(salary + 1), max_f = max(salary + 2) where salary < 50000,
+        min = min(salary - 1), min_f = min(salary - 2) where salary > 50000,
+        exp_gt = max(salary + 3) - min(salary - 3) where salary > 50000,
+        exp_lt = max(salary + 4) - min(salary - 4) where salary < 50000
+| keep max*, min*, exp*, emp_no
+| sort emp_no
+| limit 3
+;
+
+      max:i    |     max_f:i   |      min:i    |     min_f:i   |    exp_gt:i   |    exp_lt:i   |    emp_no:i     
+75000          |49820          |25323          |50062          |24941          |24502          |10001          
+75000          |49820          |25323          |50062          |24941          |24502          |10002          
+75000          |49820          |25323          |50062          |24941          |24502          |10003          
+;
+
+inlinestatsWithSubstitutedExpressionOverFilters
+required_capability: inlinestats_v10
+from employees
+| inlinestats sum = sum(salary), s_l = sum(salary) where salary < 50000, s_u = sum(salary) where salary > 50000,
+        count = count(salary), c_l = count(salary) where salary < 50000, c_u = count(salary) where salary > 50000, 
+        avg = round(avg(salary), 2), a_l = round(avg(salary), 2) where salary < 50000, a_u = round(avg(salary),2) where salary > 50000
+| keep sum, s_*, count, c_*, avg, a_*, emp_no
+| sort emp_no
+| limit 3
+;
+
+      sum:l    |      s_l:l    |      s_u:l    |     count:l   |      c_l:l    |      c_u:l    |      avg:d    |      a_l:d    |      a_u:d    |    emp_no:i
+4824855        |2220951        |2603904        |100            |58             |42             |48248.55       |38292.26       |61997.71       |10001          
+4824855        |2220951        |2603904        |100            |58             |42             |48248.55       |38292.26       |61997.71       |10002          
+4824855        |2220951        |2603904        |100            |58             |42             |48248.55       |38292.26       |61997.71       |10003          
+;
+
+
+inlinestatsWithFilterAndGroupBy
+required_capability: inlinestats_v10
+from employees
+| keep height, gender, is_rehired, emp_no
+| inlinestats m = max(height), m_f = max(height + 1) where gender == "M" OR is_rehired is null BY gender, is_rehired
+| sort emp_no
+| drop height
+| limit 10
+;
+
+    emp_no:i   |       m:d     |      m_f:d    |    gender:s   |        is_rehired:boolean         
+10001          |[2.1, 2.1]     |[3.1, 3.1]     |M              |[false, true]              
+10002          |2.1            |null           |F              |[false, false]             
+10003          |2.01           |3.01           |M              |null                       
+10004          |2.1            |3.1            |M              |true                       
+10005          |[2.1, 2.1]     |[3.1, 3.1]     |M              |[false, false, false, true]
+10006          |1.85           |2.85           |F              |null                       
+10007          |[2.1, 2.1]     |null           |F              |[false, false, true, true] 
+10008          |[2.1, 2.1]     |[3.1, 3.1]     |M              |[false, true]              
+10009          |1.85           |2.85           |F              |null                       
+10010          |[2.06, 1.97]   |null           |null           |[false, false, true, true] 
+;
+
+
+inlinestatsWithFilterOnGroupBy
+required_capability: inlinestats_v10
+from employees
+| inlinestats m_f = max(height) where gender == "M" BY gender
+| sort emp_no
+| keep gender, m_f
+| limit 10
+;
+
+    gender:s   |      m_f:d      
+M              |2.1            
+F              |null           
+M              |2.1            
+M              |2.1            
+M              |2.1            
+F              |null           
+F              |null           
+M              |2.1            
+F              |null           
+null           |null           
+;
+
+inlinestatsWithGroupByLiteral
+required_capability: inlinestats_v10
+from employees
+| inlinestats m = max(languages) by salary = 2
+| sort salary
+| keep m, salary
+| limit 3
+;
+
+       m:i     |    salary:i
+5              |2              
+5              |2              
+5              |2              
+;
+
+
+inlinestatsWithFilterOnSameColumn
+required_capability: inlinestats_v10
+from employees
+| inlinestats m = max(languages), m_f = max(languages) where salary > 50000 by salary = 2
+| sort salary
+| keep m, m_f, salary
+| limit 3
+;
+
+       m:i     |      m_f:i    |    salary:i
+5              |null           |2              
+5              |null           |2              
+5              |null           |2              
+;
+
+inlinestatsWithFilteringAndGrouping
+required_capability: inlinestats_v10
+from employees
+| inlinestats c = count(), c_f = count(languages) where l > 1, 
+        m_f = max(height) where salary > 50000 
+        by l = languages
+| sort salary
+| keep c, *_f, l, salary
+| limit 10
+;
+
+       c:l     |      c_f:l    |      m_f:d    |       l:i     |    salary:i     
+21             |21             |2.1            |5              |25324          
+21             |21             |2.1            |5              |25945          
+15             |0              |2.06           |1              |25976          
+17             |17             |2.1            |3              |26436          
+18             |18             |1.83           |4              |27215          
+15             |0              |2.06           |1              |28035          
+10             |0              |2.08           |null           |28336          
+17             |17             |2.1            |3              |28941          
+19             |19             |2.03           |2              |29175          
+17             |17             |2.1            |3              |30404
+;          
+
+ 
+multiinlinestatsWithFiltering
+required_capability: inlinestats_v10
+from employees
+| inlinestats c = count(), c_f = count(languages) where l > 1, 
+        m_f = max(height) where salary > 50000 
+        by l = languages
+| inlinestats c2 = count(), c2_f = count() where m_f > 2.06 , m2 = max(l), m2_f = max(l) where l > 1 by c
+| sort emp_no
+| where emp_no >= 10029 and emp_no <= 10039
+| keep c, *_f, l, c2, m2
+;
+
+     c:l       |      c_f:l    |      m_f:d    |     c2_f:l    |     m2_f:i    |       l:i     |      c2:l     |      m2:i       
+10             |0              |2.08           |10             |null           |null           |10             |null           
+17             |17             |2.1            |17             |3              |3              |17             |3              
+18             |18             |1.83           |0              |4              |4              |18             |4              
+17             |17             |2.1            |17             |3              |3              |17             |3              
+15             |0              |2.06           |0              |null           |1              |15             |1              
+15             |0              |2.06           |0              |null           |1              |15             |1              
+21             |21             |2.1            |21             |5              |5              |21             |5              
+18             |18             |1.83           |0              |4              |4              |18             |4              
+19             |19             |2.03           |0              |2              |2              |19             |2              
+18             |18             |1.83           |0              |4              |4              |18             |4              
+19             |19             |2.03           |0              |2              |2              |19             |2
+;              
+
+
+simpleCountOnFieldWithFilteringAndNoGrouping
+required_capability: inlinestats_v10
+from employees
+| inlinestats c1 = count(emp_no) where emp_no < 10042
+| keep emp_no, c1
+| sort emp_no
+| limit 3
+;
+
+emp_no:integer |  c1:long       
+10001          |41             
+10002          |41             
+10003          |41             
+;
+
+simpleCountOnFieldWithFilteringOnDifferentFieldAndNoGrouping
+required_capability: inlinestats_v10
+from employees
+| inlinestats c1 = count(hire_date) where emp_no < 10042
+| keep emp_no, c1
+| sort emp_no
+| limit 3
+;
+
+emp_no:integer |  c1:long       
+10001          |41             
+10002          |41             
+10003          |41             
+;
+
+simpleCountOnStarWithFilteringAndNoGrouping
+required_capability: inlinestats_v10
+from employees
+| inlinestats c1 = count(*) where emp_no < 10042
+| keep emp_no, c1
+| sort emp_no
+| limit 3
+;
+
+emp_no:integer |  c1:long       
+10001          |41             
+10002          |41             
+10003          |41             
+;
+
+simpleCountWithFilteringAndNoGroupingOnFieldWithNulls
+required_capability: inlinestats_v10
+from employees
+| inlinestats c1 = count(birth_date) where emp_no <= 10050
+| keep emp_no, c1
+| sort emp_no
+| limit 3
+;
+
+emp_no:integer |  c1:long
+10001          |40             
+10002          |40             
+10003          |40             
+;
+
+
+simpleCountWithFilteringAndNoGroupingOnFieldWithMultivalues
+required_capability: inlinestats_v10
+from employees
+| inlinestats c1 = count(job_positions) where emp_no <= 10003
+| keep emp_no, c1
+| sort emp_no
+| limit 3
+;
+
+ emp_no:integer|  c1:long       
+10001          |3              
+10002          |3              
+10003          |3              
+;
+
+commonFilterExtractionWithAliasing
+required_capability: inlinestats_v10
+from employees
+| eval eno = emp_no
+| drop emp_no
+| inlinestats min_sal = min(salary) where eno <= 10010,
+        min_hei = min(height) where eno <= 10010
+| keep eno, min_*
+| sort eno
+| limit 3
+;
+
+ eno:integer   |min_sal:integer|min_hei:double    
+10001          |36174          |1.56           
+10002          |36174          |1.56           
+10003          |36174          |1.56           
+;
+
+commonFilterExtractionWithAliasAndOriginal
+required_capability: inlinestats_v10
+from employees
+| eval eno = emp_no
+| inlinestats min_sal = min(salary) where eno <= 10010,
+        min_hei = min(height) where emp_no <= 10010
+| keep emp_no, min_*
+| sort emp_no
+| limit 3
+;
+
+ emp_no:integer|min_sal:integer|min_hei:double    
+10001          |36174          |1.56           
+10002          |36174          |1.56           
+10003          |36174          |1.56           
+;
+
+
+commonFilterExtractionWithAliasAndOriginalNeedingNormalization
+required_capability: inlinestats_v10
+from employees
+| eval eno = emp_no
+| inlinestats min_sal = min(salary) where eno <= 10010,
+        min_hei = min(height) where emp_no <= 10010,
+        max_hei = max(height) where 10010 >= emp_no
+| keep eno, min*, max*
+| sort eno
+| limit 5
+;
+
+ eno:integer   |min_sal:integer|min_hei:double |max_hei:double    
+10001          |36174          |1.56           |2.1            
+10002          |36174          |1.56           |2.1            
+10003          |36174          |1.56           |2.1            
+10004          |36174          |1.56           |2.1            
+10005          |36174          |1.56           |2.1            
+;
+
+commonFilterExtractionWithAliasAndOriginalNeedingNormalizationAndSimplification
+required_capability: inlinestats_v10
+from employees
+| eval eno = emp_no
+| inlinestats min_sal = min(salary) where eno <= 10010,
+        min_hei = min(height) where not (emp_no > 10010),
+        max_hei = max(height) where 10010 >= emp_no
+| keep eno, min*, max*
+| sort eno
+| limit 5
+;
+
+ eno:integer   |min_sal:integer|min_hei:double |max_hei:double    
+10001          |36174          |1.56           |2.1            
+10002          |36174          |1.56           |2.1            
+10003          |36174          |1.56           |2.1            
+10004          |36174          |1.56           |2.1            
+10005          |36174          |1.56           |2.1            
+;
+
+filterIsAlwaysTrue
+required_capability: inlinestats_v10
+FROM employees
+| inlinestats max = max(salary) WHERE salary > 0
+| keep max, salary, emp_no
+| sort emp_no
+| limit 3
+;
+
+  max:integer  |salary:integer | emp_no:integer     
+74999          |57305          |10001          
+74999          |56371          |10002          
+74999          |61805          |10003          
+;
+
+filterIsAlwaysFalse
+required_capability: inlinestats_v10
+FROM employees
+| inlinestats max = max(salary) WHERE first_name == ""
+| sort emp_no
+| keep max, first_name
+| limit 10
+;
+
+   max:integer |first_name:keyword
+null           |Georgi         
+null           |Bezalel        
+null           |Parto          
+null           |Chirstian      
+null           |Kyoichi        
+null           |Anneke         
+null           |Tzvetan        
+null           |Saniya         
+null           |Sumant         
+null           |Duangkaew      
+;
+
+
+filterSometimesMatches
+required_capability: inlinestats_v10
+FROM employees
+| inlinestats max = max(salary) WHERE first_name IS NULL
+| sort emp_no
+| keep max, first_name
+| limit 10
+;
+
+   max:integer |first_name:keyword
+70011          |Georgi         
+70011          |Bezalel        
+70011          |Parto          
+70011          |Chirstian      
+70011          |Kyoichi        
+70011          |Anneke         
+70011          |Tzvetan        
+70011          |Saniya         
+70011          |Sumant         
+70011          |Duangkaew      
+;
+
+groupingFilterIsAlwaysTrue
+required_capability: inlinestats_v10
+FROM employees
+| MV_EXPAND job_positions
+| inlinestats max = max(salary) WHERE salary > 0 BY job_positions = SUBSTRING(job_positions, 1, 1)
+| SORT job_positions, emp_no
+| LIMIT 4
+| keep max, salary, first_name, job_positions, emp_no
+;
+
+  max:integer  |salary:integer |first_name:keyword| job_positions:keyword |    emp_no:integer     
+74970          |57305          |Georgi            |A                      |10001          
+74970          |45797          |Duangkaew         |A                      |10010          
+74970          |31120          |Mary              |A                      |10011          
+74970          |48942          |Patricio          |A                      |10012          
+;
+
+
+groupingFilterIsAlwaysFalse
+required_capability: inlinestats_v10
+FROM employees
+| MV_EXPAND job_positions
+| inlinestats max = max(salary) WHERE first_name == "" BY job_positions = SUBSTRING(job_positions, 1, 1)
+| SORT job_positions, emp_no
+| LIMIT 4
+| keep max, salary, first_name, job_positions, emp_no
+;
+
+  max:integer  |salary:integer |first_name:keyword| job_positions:keyword |    emp_no:integer     
+null           |57305          |Georgi            |A                      |10001          
+null           |45797          |Duangkaew         |A                      |10010          
+null           |31120          |Mary              |A                      |10011          
+null           |48942          |Patricio          |A                      |10012
+;          
+
+
+groupingFilterSometimesMatches
+required_capability: inlinestats_v10
+FROM employees
+| MV_EXPAND job_positions
+| inlinestats max = max(salary) WHERE first_name IS NULL BY job_positions = SUBSTRING(job_positions, 1, 1)
+| SORT job_positions, emp_no
+| LIMIT 4
+| keep max, salary, first_name, job_positions, emp_no
+;
+
+  max:integer  |salary:integer |first_name:keyword| job_positions:keyword | emp_no:integer     
+62233          |57305          |Georgi            |A                      |10001          
+62233          |45797          |Duangkaew         |A                      |10010          
+62233          |31120          |Mary              |A                      |10011          
+62233          |48942          |Patricio          |A                      |10012          
+;
+
+groupingByOrdinalsFilterIsAlwaysTrue
+required_capability: inlinestats_v10
+FROM employees
+| inlinestats max = max(salary) WHERE salary > 0 BY job_positions
+| SORT job_positions ASC, salary DESC
+| LIMIT 4
+| keep max, salary, job_positions, emp_no
+;
+
+        max:integer         |salary:integer |                     job_positions:keyword                                  |    emp_no:integer
+[74970, 74970, 74999, 74970]|74970          |[Accountant, Junior Developer, Principal Support Engineer, Purchase Manager]|10045          
+[74970, 74999, 74999]       |66817          |[Accountant, Principal Support Engineer, Senior Python Developer]           |10094          
+[74970, 74970, 65030, 71165]|61358          |[Accountant, Purchase Manager, Python Developer, Reporting Analyst]         |10016          
+[74970, 58121, 74970]       |58121          |[Accountant, Business Analyst, Purchase Manager]                            |10051           
+;
+
+groupingByOrdinalsFilterIsAlwaysFalse
+required_capability: inlinestats_v10
+
+FROM employees
+| inlinestats max = max(salary) WHERE first_name == "" BY job_positions
+| SORT job_positions DESC NULLS last, emp_no
+| LIMIT 4
+| keep first_name, salary, emp_no, job_positions, max
+;
+
+first_name:s   |salary:i       | emp_no:i      |                       job_positions:s                                | max:i      
+Chirstian      |36174          |10004          |[Head Human Resources, Reporting Analyst, Support Engineer, Tech Lead]|null           
+Anneke         |60335          |10006          |[Principal Support Engineer, Senior Team Lead, Tech Lead]             |null           
+Duangkaew      |45797          |10010          |[Architect, Purchase Manager, Reporting Analyst, Tech Lead]           |null           
+Mary           |31120          |10011          |[Architect, Reporting Analyst, Senior Team Lead, Tech Lead]           |null           
+;
+
+groupingByOrdinalsFilterSometimesMatches
+required_capability: inlinestats_v10
+
+FROM employees
+| keep salary, first_name, job_positions, emp_no
+| inlinestats max = max(salary) WHERE first_name IS NULL BY job_positions
+| SORT emp_no
+| LIMIT 4
+;
+
+ salary:i      |first_name:s   | emp_no:i      |  max:i        |                    job_positions:s                           
+57305          |Georgi         |10001          |[39878, 62233] |[Accountant, Senior Python Developer]                                 
+56371          |Bezalel        |10002          |67492          |Senior Team Lead                                                      
+61805          |Parto          |10003          |70011          |null                                                                  
+36174          |Chirstian      |10004          |[35222, 67492] |[Head Human Resources, Reporting Analyst, Support Engineer, Tech Lead]                              
+;
+
+stdDevFilter
+required_capability: inlinestats_v10
+FROM employees
+| inlinestats greater_than = STD_DEV(salary_change) WHERE languages > 3
+, less_than = STD_DEV(salary_change) WHERE languages <= 3
+, salary = STD_DEV(salary * 2)
+, count = COUNT(*) BY gender
+| keep emp_no, gender, languages, *than, salary, count
+| SORT emp_no asc
+| limit 10
+;
+
+ emp_no:integer|gender:keyword |languages:integer|greater_than:double|less_than:double |  salary:double   | count:long     
+10001          |M              |2                |6.975232333891946  |6.604807075547775|26171.331109641273|57             
+10002          |F              |5                |6.4543266953142835 |7.57786788789264 |29045.770666969744|33             
+10003          |M              |4                |6.975232333891946  |6.604807075547775|26171.331109641273|57             
+10004          |M              |5                |6.975232333891946  |6.604807075547775|26171.331109641273|57             
+10005          |M              |1                |6.975232333891946  |6.604807075547775|26171.331109641273|57             
+10006          |F              |3                |6.4543266953142835 |7.57786788789264 |29045.770666969744|33             
+10007          |F              |4                |6.4543266953142835 |7.57786788789264 |29045.770666969744|33             
+10008          |M              |2                |6.975232333891946  |6.604807075547775|26171.331109641273|57             
+10009          |F              |1                |6.4543266953142835 |7.57786788789264 |29045.770666969744|33             
+10010          |null           |4                |6.949207097931448  |7.127229475750027|27921.220736207077|10             
+;
+
+twoConsecutiveInlinestatsWithFalseFilters
+required_capability: inlinestats_v10
+from employees
+| keep emp_no
+| sort emp_no
+| limit 3
+| inlinestats count = count(*) where false
+| inlinestats cc = count_distinct(emp_no) where false
+;
+
+    emp_no:i   |     count:l   |      cc:l       
+10001          |0              |0              
+10002          |0              |0              
+10003          |0              |0              
+;

+ 1 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -972,7 +972,7 @@ public class EsqlCapabilities {
          * Fixes a series of issues with inlinestats which had an incomplete implementation after lookup and inlinestats
          * were refactored.
          */
-        INLINESTATS_V9(EsqlPlugin.INLINESTATS_FEATURE_FLAG),
+        INLINESTATS_V10(EsqlPlugin.INLINESTATS_FEATURE_FLAG),
 
         /**
          * Support partial_results

+ 84 - 31
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceStatsFilteredAggWithEval.java

@@ -14,6 +14,7 @@ import org.elasticsearch.xpack.esql.core.expression.Attribute;
 import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
 import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.util.Holder;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.CountDistinct;
@@ -21,6 +22,7 @@ import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
 import org.elasticsearch.xpack.esql.plan.logical.Eval;
 import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
 import org.elasticsearch.xpack.esql.plan.logical.Project;
+import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
 import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;
 import org.elasticsearch.xpack.esql.plan.logical.local.LocalSupplier;
 import org.elasticsearch.xpack.esql.planner.PlannerUtils;
@@ -31,49 +33,101 @@ import java.util.List;
 /**
  * Replaces an aggregation function having a false/null filter with an EVAL node.
  * <pre>
- *     ... | STATS x = someAgg(y) WHERE FALSE {BY z} | ...
+ *     ... | STATS/INLINESTATS x = someAgg(y) WHERE FALSE {BY z} | ...
  *     =>
- *     ... | STATS x = someAgg(y) {BY z} > | EVAL x = NULL | KEEP x{, z} | ...
+ *     ... | STATS/INLINESTATS x = someAgg(y) {BY z} > | EVAL x = NULL | KEEP x{, z} | ...
  * </pre>
+ *
+ * This rule is applied to both STATS' {@link Aggregate} and {@link InlineJoin} right-hand side {@link Aggregate} plans.
+ * The logic is common for both, but the handling of the {@link InlineJoin} is slightly different when it comes to pruning
+ * its right-hand side {@link Aggregate}.
  */
-public class ReplaceStatsFilteredAggWithEval extends OptimizerRules.OptimizerRule<Aggregate> {
+public class ReplaceStatsFilteredAggWithEval extends OptimizerRules.OptimizerRule<LogicalPlan> {
     @Override
-    protected LogicalPlan rule(Aggregate aggregate) {
-        int oldAggSize = aggregate.aggregates().size();
-        List<NamedExpression> newAggs = new ArrayList<>(oldAggSize);
-        List<Alias> newEvals = new ArrayList<>(oldAggSize);
-        List<NamedExpression> newProjections = new ArrayList<>(oldAggSize);
+    protected LogicalPlan rule(LogicalPlan plan) {
+        Aggregate aggregate;
+        InlineJoin ij = null;
+        if (plan instanceof Aggregate a) {
+            aggregate = a;
+        } else if (plan instanceof InlineJoin inlineJoin) {
+            ij = inlineJoin;
+            Holder<Aggregate> aggHolder = new Holder<>();
+            inlineJoin.right().forEachDown(Aggregate.class, aggHolder::setIfAbsent);
+            aggregate = aggHolder.get();
+        } else {
+            return plan; // not an Aggregate or InlineJoin, nothing to do
+        }
+
+        if (aggregate != null) {
+            int oldAggSize = aggregate.aggregates().size();
+            List<NamedExpression> newAggs = new ArrayList<>(oldAggSize);
+            List<Alias> newEvals = new ArrayList<>(oldAggSize);
+            List<NamedExpression> newProjections = new ArrayList<>(oldAggSize);
 
-        for (var ne : aggregate.aggregates()) {
-            if (ne instanceof Alias alias
-                && alias.child() instanceof AggregateFunction aggFunction
-                && aggFunction.hasFilter()
-                && aggFunction.filter() instanceof Literal literal
-                && Boolean.FALSE.equals(literal.value())) {
+            for (var ne : aggregate.aggregates()) {
+                if (ne instanceof Alias alias
+                    && alias.child() instanceof AggregateFunction aggFunction
+                    && aggFunction.hasFilter()
+                    && aggFunction.filter() instanceof Literal literal
+                    && Boolean.FALSE.equals(literal.value())) {
 
-                Object value = aggFunction instanceof Count || aggFunction instanceof CountDistinct ? 0L : null;
-                Alias newAlias = alias.replaceChild(Literal.of(aggFunction, value));
-                newEvals.add(newAlias);
-                newProjections.add(newAlias.toAttribute());
-            } else {
-                newAggs.add(ne); // agg function unchanged or grouping key
-                newProjections.add(ne.toAttribute());
+                    Object value = aggFunction instanceof Count || aggFunction instanceof CountDistinct ? 0L : null;
+                    Alias newAlias = alias.replaceChild(Literal.of(aggFunction, value));
+                    newEvals.add(newAlias);
+                    newProjections.add(newAlias.toAttribute());
+                } else {
+                    newAggs.add(ne); // agg function unchanged or grouping key
+                    newProjections.add(ne.toAttribute());
+                }
             }
-        }
 
-        LogicalPlan plan = aggregate;
-        if (newEvals.isEmpty() == false) {
-            if (newAggs.isEmpty()) { // the Aggregate node is pruned
-                plan = localRelation(aggregate.source(), newEvals);
-            } else {
-                plan = aggregate.with(aggregate.child(), aggregate.groupings(), newAggs);
-                plan = new Eval(aggregate.source(), plan, newEvals);
-                plan = new Project(aggregate.source(), plan, newProjections);
+            if (newEvals.isEmpty() == false) {
+                if (newAggs.isEmpty()) { // the Aggregate node is pruned
+                    if (ij != null) { // this is an Aggregate part of right-hand side of an InlineJoin
+                        final LogicalPlan leftHandSide = ij.left(); // final so we can use it in the lambda below
+                        // the aggregate becomes a simple Eval since it's not needed anymore (it was replaced with Literals)
+                        var newRight = ij.right()
+                            .transformDown(
+                                Aggregate.class,
+                                agg -> agg == aggregate ? new Eval(aggregate.source(), aggregate.child(), newEvals) : agg
+                            );
+                        // Remove the StubRelation since the right-hand side of the join is now part of the main plan
+                        // and it won't be executed separately by the EsqlSession inlinestats planning.
+                        newRight = InlineJoin.replaceStub(leftHandSide, newRight);
+
+                        // project the correct output (the one of the former inlinejoin) and remove the InlineJoin altogether,
+                        // replacing it with its right-hand side followed by its left-hand side
+                        plan = new Project(ij.source(), newRight, ij.output());
+                    } else { // this is a standalone Aggregate
+                        plan = localRelation(aggregate.source(), newEvals);
+                    }
+                } else {
+                    if (ij != null) { // this is an Aggregate part of right-hand side of an InlineJoin
+                        plan = ij.replaceRight(
+                            ij.right().transformUp(Aggregate.class, agg -> updateAggregate(agg, newAggs, newEvals, newProjections))
+                        );
+                    } else { // this is a standalone Aggregate
+                        plan = updateAggregate(aggregate, newAggs, newEvals, newProjections);
+                    }
+                }
             }
         }
         return plan;
     }
 
+    private static LogicalPlan updateAggregate(
+        Aggregate agg,
+        List<NamedExpression> newAggs,
+        List<Alias> newEvals,
+        List<NamedExpression> newProjections
+    ) {
+        // only update the Aggregate and add an Eval for the removed aggregations
+        LogicalPlan newAgg = agg.with(agg.child(), agg.groupings(), newAggs);
+        newAgg = new Eval(agg.source(), newAgg, newEvals);
+        newAgg = new Project(agg.source(), newAgg, newProjections);
+        return newAgg;
+    }
+
     private static LocalRelation localRelation(Source source, List<Alias> newEvals) {
         Block[] blocks = new Block[newEvals.size()];
         List<Attribute> attributes = new ArrayList<>(newEvals.size());
@@ -83,6 +137,5 @@ public class ReplaceStatsFilteredAggWithEval extends OptimizerRules.OptimizerRul
             blocks[i] = BlockUtils.constantBlock(PlannerUtils.NON_BREAKING_BLOCK_FACTORY, ((Literal) alias.child()).value(), 1);
         }
         return new LocalRelation(source, attributes, LocalSupplier.of(blocks));
-
     }
 }

+ 18 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java

@@ -77,20 +77,31 @@ public class InlineJoin extends Join {
 
     /**
      * Replaces the stubbed source with the actual source.
-     * NOTE: this will replace all {@link StubRelation}s found with the source and the method is meant to be used to replace one node only
-     * when being called on a plan that has only ONE StubRelation in it.
+     * NOTE: this will replace the first {@link StubRelation}s found with the source and the method is meant to be used to replace one node
+     * only when being called on a plan that has only ONE StubRelation in it.
      */
-    public static LogicalPlan replaceStub(LogicalPlan source, LogicalPlan stubbed) {
+    public static LogicalPlan replaceStub(LogicalPlan stubReplacement, LogicalPlan stubbedPlan) {
         // here we could have used stubbed.transformUp(StubRelation.class, stubRelation -> source)
         // but transformUp skips changing a node if its transformed variant is equal to its original variant.
         // A StubRelation can contain in its output ReferenceAttributes which do not use NameIds for equality, but only names and
         // two ReferenceAttributes with the same name are equal and the transformation will not be applied.
-        return stubbed.transformUp(UnaryPlan.class, up -> {
+        Holder<Boolean> doneReplacing = new Holder<>(false);
+        var result = stubbedPlan.transformUp(UnaryPlan.class, up -> {
             if (up.child() instanceof StubRelation) {
-                return up.replaceChild(source);
+                if (doneReplacing.get() == false) {
+                    doneReplacing.set(true);
+                    return up.replaceChild(stubReplacement);
+                }
+                throw new IllegalStateException("Expected to replace a single StubRelation in the plan, but found more than one");
             }
             return up;
         });
+
+        if (doneReplacing.get() == false) {
+            throw new IllegalStateException("Expected to replace a single StubRelation in the plan, but none found");
+        }
+
+        return result;
     }
 
     /**
@@ -161,7 +172,8 @@ public class InlineJoin extends Join {
         LogicalPlan left = in.readNamedWriteable(LogicalPlan.class);
         LogicalPlan right = in.readNamedWriteable(LogicalPlan.class);
         JoinConfig config = new JoinConfig(in);
-        return new InlineJoin(source, left, replaceStub(left, right), config);
+        // return new InlineJoin(source, left, replaceStub(left, right), config);
+        return new InlineJoin(source, left, right, config);
     }
 
     @Override

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

@@ -4236,7 +4236,7 @@ public class AnalyzerTests extends ESTestCase {
     }
 
     public void testGroupingOverridesInInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         verifyUnsupported("""
             from test
             | inlinestats MIN(salary) BY x = languages, x = x + 1

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

@@ -12,8 +12,6 @@ import org.elasticsearch.Build;
 import org.elasticsearch.common.logging.LoggerMessageFormat;
 import org.elasticsearch.common.lucene.BytesRefs;
 import org.elasticsearch.compute.aggregation.QuantileStates;
-import org.elasticsearch.compute.data.Block;
-import org.elasticsearch.compute.data.LongVectorBlock;
 import org.elasticsearch.compute.test.TestBlockFactory;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.dissect.DissectParser;
@@ -176,7 +174,6 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
 import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
 import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
 import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
-import static org.elasticsearch.xpack.esql.core.type.DataType.LONG;
 import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT;
 import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.BinaryComparisonOperation.EQ;
 import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.BinaryComparisonOperation.GT;
@@ -199,7 +196,6 @@ import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
-import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.startsWith;
 
 //@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug")
@@ -536,349 +532,6 @@ public class LogicalPlanOptimizerTests extends AbstractLogicalPlanOptimizerTests
         assertThat(Expressions.names(agg.aggregates()), contains("sum(salary)", "sum(salary) WheRe last_name ==   \"Doe\""));
     }
 
-    /**
-     * <pre>{@code
-     * Limit[1000[INTEGER]]
-     * \_LocalRelation[[sum(salary) where false{r}#26],[ConstantNullBlock[positions=1]]]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalSingleAgg() {
-        var plan = plan("""
-            from test
-            | stats sum(salary) where false
-            """);
-
-        var project = as(plan, Limit.class);
-        var source = as(project.child(), LocalRelation.class);
-        assertThat(Expressions.names(source.output()), contains("sum(salary) where false"));
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        assertThat(blocks[0].getPositionCount(), is(1));
-        assertTrue(blocks[0].areAllValuesNull());
-    }
-
-    /**
-     * <pre>{@code
-     * Project[[sum(salary) + 1 where false{r}#68]]
-     * \_Eval[[$$SUM$sum(salary)_+_1$0{r$}#79 + 1[INTEGER] AS sum(salary) + 1 where false]]
-     *   \_Limit[1000[INTEGER]]
-     *     \_LocalRelation[[$$SUM$sum(salary)_+_1$0{r$}#79],[ConstantNullBlock[positions=1]]]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalSingleAggWithExpression() {
-        var plan = plan("""
-            from test
-            | stats sum(salary) + 1 where false
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(Expressions.names(project.projections()), contains("sum(salary) + 1 where false"));
-
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(1));
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertThat(alias.name(), is("sum(salary) + 1 where false"));
-        var add = as(alias.child(), Add.class);
-        var literal = as(add.right(), Literal.class);
-        assertThat(literal.value(), is(1));
-
-        var limit = as(eval.child(), Limit.class);
-        var source = as(limit.child(), LocalRelation.class);
-
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        assertThat(blocks[0].getPositionCount(), is(1));
-        assertTrue(blocks[0].areAllValuesNull());
-    }
-
-    /**
-     * <pre>{@code
-     * Project[[sum(salary) + 1 where false{r}#4, sum(salary) + 2{r}#6, emp_no{f}#7]]
-     * \_Eval[[null[LONG] AS sum(salary) + 1 where false, $$SUM$sum(salary)_+_2$1{r$}#18 + 2[INTEGER] AS sum(salary) + 2]]
-     *   \_Limit[1000[INTEGER]]
-     *     \_Aggregate[STANDARD,[emp_no{f}#7],[SUM(salary{f}#12,true[BOOLEAN]) AS $$SUM$sum(salary)_+_2$1, emp_no{f}#7]]
-     *       \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalMixedFilterAndNoFilter() {
-        var plan = plan("""
-            from test
-            | stats sum(salary) + 1 where false,
-                    sum(salary) + 2
-              by emp_no
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(Expressions.names(project.projections()), contains("sum(salary) + 1 where false", "sum(salary) + 2", "emp_no"));
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(2));
-
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertTrue(alias.child().foldable());
-        assertThat(alias.child().fold(FoldContext.small()), nullValue());
-        assertThat(alias.child().dataType(), is(LONG));
-
-        alias = as(eval.fields().getLast(), Alias.class);
-        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 2"));
-
-        var limit = as(eval.child(), Limit.class);
-        var aggregate = as(limit.child(), Aggregate.class);
-        var source = as(aggregate.child(), EsRelation.class);
-    }
-
-    /**
-     * <pre>{@code
-     * Project[[sum(salary) + 1 where false{r}#3, sum(salary) + 3{r}#5, sum(salary) + 2 where null{r}#7,
-     * sum(salary) + 4 where not true{r}#9]]
-     * \_Eval[[null[LONG] AS sum(salary) + 1 where false#3, $$SUM$sum(salary)_+_3$1{r$}#22 + 3[INTEGER] AS sum(salary) + 3#5
-     * , null[LONG] AS sum(salary) + 2 where null#7, null[LONG] AS sum(salary) + 4 where not true#9]]
-     *   \_Limit[1000[INTEGER],false]
-     *     \_Aggregate[[],[SUM(salary{f}#15,true[BOOLEAN],compensated[KEYWORD]) AS $$SUM$sum(salary)_+_3$1#22]]
-     *       \_EsRelation[test][_meta_field{f}#16, emp_no{f}#10, first_name{f}#11, ..]
-     * }</pre>
-     *
-     */
-    public void testReplaceStatsFilteredAggWithEvalFilterFalseAndNull() {
-        var plan = plan("""
-            from test
-            | stats sum(salary) + 1 where false,
-                    sum(salary) + 3,
-                    sum(salary) + 2 where null,
-                    sum(salary) + 4 where not true
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(
-            Expressions.names(project.projections()),
-            contains("sum(salary) + 1 where false", "sum(salary) + 3", "sum(salary) + 2 where null", "sum(salary) + 4 where not true")
-        );
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(4));
-
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertTrue(alias.child().foldable());
-        assertThat(alias.child().fold(FoldContext.small()), nullValue());
-        assertThat(alias.child().dataType(), is(LONG));
-
-        alias = as(eval.fields().get(0), Alias.class);
-        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 1"));
-
-        alias = as(eval.fields().get(1), Alias.class);
-        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 3"));
-
-        alias = as(eval.fields().get(2), Alias.class);
-        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 2"));
-
-        alias = as(eval.fields().get(3), Alias.class);
-        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 4"));
-
-        alias = as(eval.fields().getLast(), Alias.class);
-        assertTrue(alias.child().foldable());
-        assertThat(alias.child().fold(FoldContext.small()), nullValue());
-        assertThat(alias.child().dataType(), is(LONG));
-
-        var limit = as(eval.child(), Limit.class);
-        var aggregate = as(limit.child(), Aggregate.class);
-        var source = as(aggregate.child(), EsRelation.class);
-    }
-
-    /*
-     * Limit[1000[INTEGER]]
-     * \_LocalRelation[[count(salary) where false{r}#3],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
-     */
-    public void testReplaceStatsFilteredAggWithEvalNotTrue() {
-        var plan = plan("""
-            from test
-            | stats count(salary) where not true
-            """);
-
-        var limit = as(plan, Limit.class);
-        var source = as(limit.child(), LocalRelation.class);
-        assertThat(Expressions.names(source.output()), contains("count(salary) where not true"));
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        var block = as(blocks[0], LongVectorBlock.class);
-        assertThat(block.getPositionCount(), is(1));
-        assertThat(block.asVector().getLong(0), is(0L));
-    }
-
-    /*
-     * Limit[1000[INTEGER],false]
-     * \_Aggregate[[],[COUNT(salary{f}#10,true[BOOLEAN]) AS m1#4]]
-     * \_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..]
-     */
-    public void testReplaceStatsFilteredAggWithEvalNotFalse() {
-        var plan = plan("""
-            from test
-            | stats m1 = count(salary) where not false
-            """);
-        var limit = as(plan, Limit.class);
-        var aggregate = as(limit.child(), Aggregate.class);
-        assertEquals(1, aggregate.aggregates().size());
-        assertEquals(1, aggregate.aggregates().get(0).children().size());
-        assertTrue(aggregate.aggregates().get(0).children().get(0) instanceof Count);
-        assertEquals("true", (((Count) aggregate.aggregates().get(0).children().get(0)).filter().toString()));
-        assertThat(Expressions.names(aggregate.aggregates()), contains("m1"));
-        var source = as(aggregate.child(), EsRelation.class);
-    }
-
-    /**
-     * <pre>{@code
-     * Limit[1000[INTEGER]]
-     * \_LocalRelation[[count(salary) where false{r}#3],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalCount() {
-        var plan = plan("""
-            from test
-            | stats count(salary) where false
-            """);
-
-        var limit = as(plan, Limit.class);
-        var source = as(limit.child(), LocalRelation.class);
-        assertThat(Expressions.names(source.output()), contains("count(salary) where false"));
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        var block = as(blocks[0], LongVectorBlock.class);
-        assertThat(block.getPositionCount(), is(1));
-        assertThat(block.asVector().getLong(0), is(0L));
-    }
-
-    /**
-     * <pre>{@code
-     * Project[[count_distinct(salary + 2) + 3 where false{r}#3]]
-     * \_Eval[[$$COUNTDISTINCT$count_distinct(>$0{r$}#15 + 3[INTEGER] AS count_distinct(salary + 2) + 3 where false]]
-     *   \_Limit[1000[INTEGER]]
-     *     \_LocalRelation[[$$COUNTDISTINCT$count_distinct(>$0{r$}#15],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalCountDistinctInExpression() {
-        var plan = plan("""
-            from test
-            | stats count_distinct(salary + 2) + 3 where false
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(Expressions.names(project.projections()), contains("count_distinct(salary + 2) + 3 where false"));
-
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(1));
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertThat(alias.name(), is("count_distinct(salary + 2) + 3 where false"));
-        var add = as(alias.child(), Add.class);
-        var literal = as(add.right(), Literal.class);
-        assertThat(literal.value(), is(3));
-
-        var limit = as(eval.child(), Limit.class);
-        var source = as(limit.child(), LocalRelation.class);
-
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        var block = as(blocks[0], LongVectorBlock.class);
-        assertThat(block.getPositionCount(), is(1));
-        assertThat(block.asVector().getLong(0), is(0L));
-    }
-
-    /**
-     * <pre>{@code
-     * Project[[max{r}#91, max_a{r}#94, min{r}#97, min_a{r}#100, emp_no{f}#101]]
-     * \_Eval[[null[INTEGER] AS max_a, null[INTEGER] AS min_a]]
-     *   \_Limit[1000[INTEGER]]
-     *     \_Aggregate[STANDARD,[emp_no{f}#101],[MAX(salary{f}#106,true[BOOLEAN]) AS max, MIN(salary{f}#106,true[BOOLEAN]) AS min, emp_
-     * no{f}#101]]
-     *       \_EsRelation[test][_meta_field{f}#107, emp_no{f}#101, first_name{f}#10..]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalSameAggWithAndWithoutFilter() {
-        var plan = plan("""
-            from test
-            | stats max = max(salary), max_a = max(salary) where null,
-                    min = min(salary), min_a = min(salary) where to_string(null) == "abc"
-              by emp_no
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(Expressions.names(project.projections()), contains("max", "max_a", "min", "min_a", "emp_no"));
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(2));
-
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertThat(Expressions.name(alias), containsString("max_a"));
-        assertTrue(alias.child().foldable());
-        assertThat(alias.child().fold(FoldContext.small()), nullValue());
-        assertThat(alias.child().dataType(), is(INTEGER));
-
-        alias = as(eval.fields().getLast(), Alias.class);
-        assertThat(Expressions.name(alias), containsString("min_a"));
-        assertTrue(alias.child().foldable());
-        assertThat(alias.child().fold(FoldContext.small()), nullValue());
-        assertThat(alias.child().dataType(), is(INTEGER));
-
-        var limit = as(eval.child(), Limit.class);
-
-        var aggregate = as(limit.child(), Aggregate.class);
-        assertThat(Expressions.names(aggregate.aggregates()), contains("max", "min", "emp_no"));
-
-        var source = as(aggregate.child(), EsRelation.class);
-    }
-
-    /**
-     * <pre>{@code
-     * Limit[1000[INTEGER]]
-     * \_LocalRelation[[count{r}#7],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
-     * }</pre>
-     */
-    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/100634") // i.e. PropagateEvalFoldables applicability to Aggs
-    public void testReplaceStatsFilteredAggWithEvalFilterUsingEvaledValue() {
-        var plan = plan("""
-            from test
-            | eval my_length = length(concat(first_name, null))
-            | stats count = count(my_length) where my_length > 0
-            """);
-
-        var limit = as(plan, Limit.class);
-        var source = as(limit.child(), LocalRelation.class);
-        assertThat(Expressions.names(source.output()), contains("count"));
-        Block[] blocks = source.supplier().get();
-        assertThat(blocks.length, is(1));
-        var block = as(blocks[0], LongVectorBlock.class);
-        assertThat(block.getPositionCount(), is(1));
-        assertThat(block.asVector().getLong(0), is(0L));
-    }
-
-    /**
-     *
-     * <pre>{@code
-     * Project[[c{r}#67, emp_no{f}#68]]
-     * \_Eval[[0[LONG] AS c]]
-     *   \_Limit[1000[INTEGER]]
-     *     \_Aggregate[STANDARD,[emp_no{f}#68],[emp_no{f}#68]]
-     *       \_EsRelation[test][_meta_field{f}#74, emp_no{f}#68, first_name{f}#69, ..]
-     * }</pre>
-     */
-    public void testReplaceStatsFilteredAggWithEvalSingleAggWithGroup() {
-        var plan = plan("""
-            from test
-            | stats c = count(emp_no) where false
-              by emp_no
-            """);
-
-        var project = as(plan, Project.class);
-        assertThat(Expressions.names(project.projections()), contains("c", "emp_no"));
-
-        var eval = as(project.child(), Eval.class);
-        assertThat(eval.fields().size(), is(1));
-        var alias = as(eval.fields().getFirst(), Alias.class);
-        assertThat(Expressions.name(alias), containsString("c"));
-
-        var limit = as(eval.child(), Limit.class);
-
-        var aggregate = as(limit.child(), Aggregate.class);
-        assertThat(Expressions.names(aggregate.aggregates()), contains("emp_no"));
-
-        var source = as(aggregate.child(), EsRelation.class);
-    }
-
     public void testExtractStatsCommonFilter() {
         var plan = plan("""
             from test

+ 2 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PropagateInlineEvalsTests.java

@@ -83,7 +83,7 @@ public class PropagateInlineEvalsTests extends ESTestCase {
      *     \_StubRelation[[emp_no{f}#11, languages{f}#14, gender{f}#13, y{r}#10]]
      */
     public void testGroupingAliasingMoved_To_LeftSideOfJoin() {
-        assumeTrue("Requires INLINESTATS", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("Requires INLINESTATS", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         var plan = plan("""
             from test
             | keep emp_no, languages, gender
@@ -126,7 +126,7 @@ public class PropagateInlineEvalsTests extends ESTestCase {
      * {r}#21]]
      */
     public void testGroupingAliasingMoved_To_LeftSideOfJoin_WithExpression() {
-        assumeTrue("Requires INLINESTATS", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("Requires INLINESTATS", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         var plan = plan("""
             from test
             | keep emp_no, languages, gender, last_name, first_name

+ 774 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceStatsFilteredAggWithEvalTests.java

@@ -0,0 +1,774 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.optimizer.rules.logical;
+
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.LongVectorBlock;
+import org.elasticsearch.xpack.esql.core.expression.Alias;
+import org.elasticsearch.xpack.esql.core.expression.Expressions;
+import org.elasticsearch.xpack.esql.core.expression.FoldContext;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
+import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
+import org.elasticsearch.xpack.esql.optimizer.AbstractLogicalPlanOptimizerTests;
+import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
+import org.elasticsearch.xpack.esql.plan.logical.EsRelation;
+import org.elasticsearch.xpack.esql.plan.logical.Eval;
+import org.elasticsearch.xpack.esql.plan.logical.Limit;
+import org.elasticsearch.xpack.esql.plan.logical.Project;
+import org.elasticsearch.xpack.esql.plan.logical.TopN;
+import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
+import org.elasticsearch.xpack.esql.plan.logical.join.StubRelation;
+import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject;
+import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;
+
+import static org.elasticsearch.test.ListMatcher.matchesList;
+import static org.elasticsearch.test.MapMatcher.assertMap;
+import static org.elasticsearch.xpack.esql.EsqlTestUtils.as;
+import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
+import static org.elasticsearch.xpack.esql.core.type.DataType.LONG;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+
+public class ReplaceStatsFilteredAggWithEvalTests extends AbstractLogicalPlanOptimizerTests {
+
+    /**
+     * <pre>{@code
+     * Limit[1000[INTEGER]]
+     * \_LocalRelation[[sum(salary) where false{r}#26],[ConstantNullBlock[positions=1]]]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalSingleAgg() {
+        var plan = plan("""
+            from test
+            | stats sum(salary) where false
+            """);
+
+        var project = as(plan, Limit.class);
+        var source = as(project.child(), LocalRelation.class);
+        assertThat(Expressions.names(source.output()), contains("sum(salary) where false"));
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        assertThat(blocks[0].getPositionCount(), is(1));
+        assertTrue(blocks[0].areAllValuesNull());
+    }
+
+    /**
+     * <pre>{@code
+     * Project[[sum(salary) + 1 where false{r}#68]]
+     * \_Eval[[$$SUM$sum(salary)_+_1$0{r$}#79 + 1[INTEGER] AS sum(salary) + 1 where false]]
+     *   \_Limit[1000[INTEGER]]
+     *     \_LocalRelation[[$$SUM$sum(salary)_+_1$0{r$}#79],[ConstantNullBlock[positions=1]]]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalSingleAggWithExpression() {
+        var plan = plan("""
+            from test
+            | stats sum(salary) + 1 where false
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(Expressions.names(project.projections()), contains("sum(salary) + 1 where false"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(alias.name(), is("sum(salary) + 1 where false"));
+        var add = as(alias.child(), Add.class);
+        var literal = as(add.right(), Literal.class);
+        assertThat(literal.value(), is(1));
+
+        var limit = as(eval.child(), Limit.class);
+        var source = as(limit.child(), LocalRelation.class);
+
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        assertThat(blocks[0].getPositionCount(), is(1));
+        assertTrue(blocks[0].areAllValuesNull());
+    }
+
+    /**
+     * <pre>{@code
+     * Project[[sum(salary) + 1 where false{r}#4, sum(salary) + 2{r}#6, emp_no{f}#7]]
+     * \_Eval[[null[LONG] AS sum(salary) + 1 where false, $$SUM$sum(salary)_+_2$1{r$}#18 + 2[INTEGER] AS sum(salary) + 2]]
+     *   \_Limit[1000[INTEGER]]
+     *     \_Aggregate[STANDARD,[emp_no{f}#7],[SUM(salary{f}#12,true[BOOLEAN]) AS $$SUM$sum(salary)_+_2$1, emp_no{f}#7]]
+     *       \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalMixedFilterAndNoFilter() {
+        var plan = plan("""
+            from test
+            | stats sum(salary) + 1 where false,
+                    sum(salary) + 2
+              by emp_no
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(Expressions.names(project.projections()), contains("sum(salary) + 1 where false", "sum(salary) + 2", "emp_no"));
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(2));
+
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+
+        alias = as(eval.fields().getLast(), Alias.class);
+        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 2"));
+
+        var limit = as(eval.child(), Limit.class);
+        var aggregate = as(limit.child(), Aggregate.class);
+        var source = as(aggregate.child(), EsRelation.class);
+    }
+
+    /**
+     * <pre>{@code
+     * Project[[sum(salary) + 1 where false{r}#3, sum(salary) + 3{r}#5, sum(salary) + 2 where null{r}#7,
+     * sum(salary) + 4 where not true{r}#9]]
+     * \_Eval[[null[LONG] AS sum(salary) + 1 where false#3, $$SUM$sum(salary)_+_3$1{r$}#22 + 3[INTEGER] AS sum(salary) + 3#5
+     * , null[LONG] AS sum(salary) + 2 where null#7, null[LONG] AS sum(salary) + 4 where not true#9]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_Aggregate[[],[SUM(salary{f}#15,true[BOOLEAN],compensated[KEYWORD]) AS $$SUM$sum(salary)_+_3$1#22]]
+     *       \_EsRelation[test][_meta_field{f}#16, emp_no{f}#10, first_name{f}#11, ..]
+     * }</pre>
+     *
+     */
+    public void testReplaceStatsFilteredAggWithEvalFilterFalseAndNull() {
+        var plan = plan("""
+            from test
+            | stats sum(salary) + 1 where false,
+                    sum(salary) + 3,
+                    sum(salary) + 2 where null,
+                    sum(salary) + 4 where not true
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(
+            Expressions.names(project.projections()),
+            contains("sum(salary) + 1 where false", "sum(salary) + 3", "sum(salary) + 2 where null", "sum(salary) + 4 where not true")
+        );
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(4));
+
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+
+        alias = as(eval.fields().get(0), Alias.class);
+        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 1"));
+
+        alias = as(eval.fields().get(1), Alias.class);
+        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 3"));
+
+        alias = as(eval.fields().get(2), Alias.class);
+        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 2"));
+
+        alias = as(eval.fields().get(3), Alias.class);
+        assertThat(Expressions.name(alias.child()), containsString("sum(salary) + 4"));
+
+        alias = as(eval.fields().getLast(), Alias.class);
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+
+        var limit = as(eval.child(), Limit.class);
+        var aggregate = as(limit.child(), Aggregate.class);
+        var source = as(aggregate.child(), EsRelation.class);
+    }
+
+    /*
+     * Limit[1000[INTEGER]]
+     * \_LocalRelation[[count(salary) where false{r}#3],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
+     */
+    public void testReplaceStatsFilteredAggWithEvalNotTrue() {
+        var plan = plan("""
+            from test
+            | stats count(salary) where not true
+            """);
+
+        var limit = as(plan, Limit.class);
+        var source = as(limit.child(), LocalRelation.class);
+        assertThat(Expressions.names(source.output()), contains("count(salary) where not true"));
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        var block = as(blocks[0], LongVectorBlock.class);
+        assertThat(block.getPositionCount(), is(1));
+        assertThat(block.asVector().getLong(0), is(0L));
+    }
+
+    /*
+     * Limit[1000[INTEGER],false]
+     * \_Aggregate[[],[COUNT(salary{f}#10,true[BOOLEAN]) AS m1#4]]
+     * \_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..]
+     */
+    public void testReplaceStatsFilteredAggWithEvalNotFalse() {
+        var plan = plan("""
+            from test
+            | stats m1 = count(salary) where not false
+            """);
+        var limit = as(plan, Limit.class);
+        var aggregate = as(limit.child(), Aggregate.class);
+        assertEquals(1, aggregate.aggregates().size());
+        assertEquals(1, aggregate.aggregates().get(0).children().size());
+        assertTrue(aggregate.aggregates().get(0).children().get(0) instanceof Count);
+        assertEquals("true", (((Count) aggregate.aggregates().get(0).children().get(0)).filter().toString()));
+        assertThat(Expressions.names(aggregate.aggregates()), contains("m1"));
+        var source = as(aggregate.child(), EsRelation.class);
+    }
+
+    /**
+     * <pre>{@code
+     * Limit[1000[INTEGER]]
+     * \_LocalRelation[[count(salary) where false{r}#3],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalCount() {
+        var plan = plan("""
+            from test
+            | stats count(salary) where false
+            """);
+
+        var limit = as(plan, Limit.class);
+        var source = as(limit.child(), LocalRelation.class);
+        assertThat(Expressions.names(source.output()), contains("count(salary) where false"));
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        var block = as(blocks[0], LongVectorBlock.class);
+        assertThat(block.getPositionCount(), is(1));
+        assertThat(block.asVector().getLong(0), is(0L));
+    }
+
+    /**
+     * <pre>{@code
+     * Project[[count_distinct(salary + 2) + 3 where false{r}#3]]
+     * \_Eval[[$$COUNTDISTINCT$count_distinct(>$0{r$}#15 + 3[INTEGER] AS count_distinct(salary + 2) + 3 where false]]
+     *   \_Limit[1000[INTEGER]]
+     *     \_LocalRelation[[$$COUNTDISTINCT$count_distinct(>$0{r$}#15],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalCountDistinctInExpression() {
+        var plan = plan("""
+            from test
+            | stats count_distinct(salary + 2) + 3 where false
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(Expressions.names(project.projections()), contains("count_distinct(salary + 2) + 3 where false"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(alias.name(), is("count_distinct(salary + 2) + 3 where false"));
+        var add = as(alias.child(), Add.class);
+        var literal = as(add.right(), Literal.class);
+        assertThat(literal.value(), is(3));
+
+        var limit = as(eval.child(), Limit.class);
+        var source = as(limit.child(), LocalRelation.class);
+
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        var block = as(blocks[0], LongVectorBlock.class);
+        assertThat(block.getPositionCount(), is(1));
+        assertThat(block.asVector().getLong(0), is(0L));
+    }
+
+    /**
+     * <pre>{@code
+     * Project[[max{r}#91, max_a{r}#94, min{r}#97, min_a{r}#100, emp_no{f}#101]]
+     * \_Eval[[null[INTEGER] AS max_a, null[INTEGER] AS min_a]]
+     *   \_Limit[1000[INTEGER]]
+     *     \_Aggregate[STANDARD,[emp_no{f}#101],[MAX(salary{f}#106,true[BOOLEAN]) AS max, MIN(salary{f}#106,true[BOOLEAN]) AS min, emp_
+     * no{f}#101]]
+     *       \_EsRelation[test][_meta_field{f}#107, emp_no{f}#101, first_name{f}#10..]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalSameAggWithAndWithoutFilter() {
+        var plan = plan("""
+            from test
+            | stats max = max(salary), max_a = max(salary) where null,
+                    min = min(salary), min_a = min(salary) where to_string(null) == "abc"
+              by emp_no
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(Expressions.names(project.projections()), contains("max", "max_a", "min", "min_a", "emp_no"));
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(2));
+
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(Expressions.name(alias), containsString("max_a"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(INTEGER));
+
+        alias = as(eval.fields().getLast(), Alias.class);
+        assertThat(Expressions.name(alias), containsString("min_a"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(INTEGER));
+
+        var limit = as(eval.child(), Limit.class);
+
+        var aggregate = as(limit.child(), Aggregate.class);
+        assertThat(Expressions.names(aggregate.aggregates()), contains("max", "min", "emp_no"));
+
+        var source = as(aggregate.child(), EsRelation.class);
+    }
+
+    /**
+     * <pre>{@code
+     * Limit[1000[INTEGER]]
+     * \_LocalRelation[[count{r}#7],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]]
+     * }</pre>
+     */
+    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/100634") // i.e. PropagateEvalFoldables applicability to Aggs
+    public void testReplaceStatsFilteredAggWithEvalFilterUsingEvaledValue() {
+        var plan = plan("""
+            from test
+            | eval my_length = length(concat(first_name, null))
+            | stats count = count(my_length) where my_length > 0
+            """);
+
+        var limit = as(plan, Limit.class);
+        var source = as(limit.child(), LocalRelation.class);
+        assertThat(Expressions.names(source.output()), contains("count"));
+        Block[] blocks = source.supplier().get();
+        assertThat(blocks.length, is(1));
+        var block = as(blocks[0], LongVectorBlock.class);
+        assertThat(block.getPositionCount(), is(1));
+        assertThat(block.asVector().getLong(0), is(0L));
+    }
+
+    /**
+     *
+     * <pre>{@code
+     * Project[[c{r}#67, emp_no{f}#68]]
+     * \_Eval[[0[LONG] AS c]]
+     *   \_Limit[1000[INTEGER]]
+     *     \_Aggregate[STANDARD,[emp_no{f}#68],[emp_no{f}#68]]
+     *       \_EsRelation[test][_meta_field{f}#74, emp_no{f}#68, first_name{f}#69, ..]
+     * }</pre>
+     */
+    public void testReplaceStatsFilteredAggWithEvalSingleAggWithGroup() {
+        var plan = plan("""
+            from test
+            | stats c = count(emp_no) where false
+              by emp_no
+            """);
+
+        var project = as(plan, Project.class);
+        assertThat(Expressions.names(project.projections()), contains("c", "emp_no"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(Expressions.name(alias), containsString("c"));
+
+        var limit = as(eval.child(), Limit.class);
+
+        var aggregate = as(limit.child(), Aggregate.class);
+        assertThat(Expressions.names(aggregate.aggregates()), contains("emp_no"));
+
+        var source = as(aggregate.child(), EsRelation.class);
+    }
+
+    /**
+     * Project[[_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, gender{f}#6, hire_date{f}#11, job{f}#12, job.raw{f}#13, lang
+     * uages{f}#7, last_name{f}#8, long_noidx{f}#14, salary{f}#9, sum(salary) where false{r}#3]]
+     * \_Eval[[null[LONG] AS sum(salary) where false#3]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, ge..]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalSingleAgg() {
+        var plan = plan("""
+            from test
+            | inlinestats sum(salary) where false
+            """);
+
+        var project = as(plan, Project.class);
+        assertMap(
+            Expressions.names(project.projections()).stream().map(Object::toString).toList(),
+            matchesList().item(startsWith("_meta_field"))
+                .item(startsWith("emp_no"))
+                .item(startsWith("first_name"))
+                .item(startsWith("gender"))
+                .item(startsWith("hire_date"))
+                .item(startsWith("job"))
+                .item(startsWith("job.raw"))
+                .item(startsWith("languages"))
+                .item(startsWith("last_name"))
+                .item(startsWith("long_noidx"))
+                .item(startsWith("salary"))
+                .item(containsString("sum(salary) where false"))
+        );
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(Expressions.name(alias), containsString("sum(salary) where false"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+        var limit = as(eval.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+    }
+
+    /**
+     * Project[[_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, gender{f}#6, hire_date{f}#11, job{f}#12, job.raw{f}#13, lang
+     * uages{f}#7, last_name{f}#8, long_noidx{f}#14, salary{f}#9, sum(salary) + 1 where false{r}#3]]
+     * \_Eval[[null[LONG] AS sum(salary) + 1 where false#3]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, ge..]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalSingleAggWithExpression() {
+        var plan = plan("""
+            from test
+            | inlinestats sum(salary) + 1 where false
+            """);
+
+        var project = as(plan, Project.class);
+        assertMap(
+            Expressions.names(project.projections()).stream().map(Object::toString).toList(),
+            matchesList().item(startsWith("_meta_field"))
+                .item(startsWith("emp_no"))
+                .item(startsWith("first_name"))
+                .item(startsWith("gender"))
+                .item(startsWith("hire_date"))
+                .item(startsWith("job"))
+                .item(startsWith("job.raw"))
+                .item(startsWith("languages"))
+                .item(startsWith("last_name"))
+                .item(startsWith("long_noidx"))
+                .item(startsWith("salary"))
+                .item(containsString("sum(salary) + 1 where false"))
+        );
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(Expressions.name(alias), containsString("sum(salary) + 1 where false"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+        var limit = as(eval.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+    }
+
+    /**
+     * Limit[1000[INTEGER],true]
+     * \_InlineJoin[LEFT,[emp_no{f}#9],[emp_no{f}#9],[emp_no{r}#9]]
+     *   |_EsqlProject[[salary{f}#14, emp_no{f}#9]]
+     *   | \_Limit[1000[INTEGER],false]
+     *   |   \_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..]
+     *   \_Project[[sum(salary)   1 where false{r}#5, sum(salary)   2{r}#7, emp_no{f}#9]]
+     *     \_Eval[[null[LONG] AS sum(salary)   1 where false#5, $$SUM$sum(salary)_ _2$1{r$}#21   2[INTEGER] AS sum(salary)   2#7]]
+     *       \_Aggregate[[emp_no{f}#9],[SUM(salary{f}#14,true[BOOLEAN],compensated[KEYWORD]) AS $$SUM$sum(salary)_ _2$1#21, emp_no{f}#9]]
+     *         \_StubRelation[[salary{f}#14, emp_no{f}#9]]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalMixedFilterAndNoFilter() {
+        var plan = plan("""
+            from test
+            | keep salary, emp_no
+            | inlinestats sum(salary) + 1 where false,
+                    sum(salary) + 2
+              by emp_no
+            """);
+
+        var limit = as(plan, Limit.class);
+        var ij = as(limit.child(), InlineJoin.class);
+        var left = as(ij.left(), EsqlProject.class);
+        assertThat(Expressions.names(left.projections()), contains("salary", "emp_no"));
+        limit = as(left.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+        var right = as(ij.right(), Project.class);
+        assertMap(
+            Expressions.names(right.projections()).stream().map(Object::toString).toList(),
+            matchesList().item(startsWith("sum(salary) + 1 where false")).item(startsWith("sum(salary) + 2")).item(startsWith("emp_no"))
+        );
+        var eval = as(right.child(), Eval.class);
+        assertThat(eval.fields().size(), is(2));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), nullValue());
+        assertThat(alias.child().dataType(), is(LONG));
+        assertThat(Expressions.name(alias), containsString("sum(salary) + 1 where false"));
+        alias = as(eval.fields().get(1), Alias.class);
+        assertFalse(alias.child().foldable());
+        assertThat(alias.child().dataType(), is(LONG));
+        assertThat(Expressions.name(alias), containsString("sum(salary) + 2"));
+        assertThat(alias.child() instanceof Add, is(true));
+        var add = as(alias.child(), Add.class);
+        assertThat(add.left().toString(), containsString("$$SUM$sum(salary)_+_2$1"));
+        var agg = as(eval.child(), Aggregate.class);
+        assertThat(Expressions.names(agg.aggregates()), contains("$$SUM$sum(salary)_+_2$1", "emp_no"));
+        var source = as(agg.child(), StubRelation.class);
+        assertThat(Expressions.names(source.output()), contains("salary", "emp_no"));
+    }
+
+    /**
+     * Limit[1000[INTEGER],true]
+     * \_InlineJoin[LEFT,[],[],[]]
+     *   |_EsqlProject[[salary{f}#16]]
+     *   | \_Limit[1000[INTEGER],false]
+     *   |   \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..]
+     *   \_Project[[sum(salary)   1 where false{r}#4, sum(salary)   3{r}#6, sum(salary)   2 where null{r}#8, sum(salary)   4 wher
+     * e not true{r}#10]]
+     *     \_Eval[[null[LONG] AS sum(salary)   1 where false#4, $$SUM$sum(salary)_ _3$1{r$}#23   3[INTEGER] AS sum(salary)   3#6
+     * , null[LONG] AS sum(salary)   2 where null#8, null[LONG] AS sum(salary)   4 where not true#10]]
+     *       \_Aggregate[[],[SUM(salary{f}#16,true[BOOLEAN],compensated[KEYWORD]) AS $$SUM$sum(salary)_ _3$1#23]]
+     *         \_StubRelation[[salary{f}#16]]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalFilterFalseAndNull() {
+        var plan = plan("""
+            from test
+            | keep salary
+            | inlinestats sum(salary) + 1 where false,
+                    sum(salary) + 3,
+                    sum(salary) + 2 where null,
+                    sum(salary) + 4 where not true
+            """);
+        var limit = as(plan, Limit.class);
+        var ij = as(limit.child(), InlineJoin.class);
+
+        // Check right side Project node
+        var right = as(ij.right(), Project.class);
+        assertThat(
+            Expressions.names(right.projections()),
+            contains("sum(salary) + 1 where false", "sum(salary) + 3", "sum(salary) + 2 where null", "sum(salary) + 4 where not true")
+        );
+
+        var eval = as(right.child(), Eval.class);
+        assertThat(eval.fields().size(), is(4));
+
+        var alias1 = as(eval.fields().get(0), Alias.class);
+        assertTrue(alias1.child().foldable());
+        assertThat(alias1.child().fold(FoldContext.small()), nullValue());
+
+        var alias2 = as(eval.fields().get(1), Alias.class);
+        assertFalse(alias2.child().foldable());
+
+        var alias3 = as(eval.fields().get(2), Alias.class);
+        assertTrue(alias3.child().foldable());
+        assertThat(alias3.child().fold(FoldContext.small()), nullValue());
+
+        var alias4 = as(eval.fields().get(3), Alias.class);
+        assertTrue(alias4.child().foldable());
+        assertThat(alias4.child().fold(FoldContext.small()), nullValue());
+
+        var aggregate = as(eval.child(), Aggregate.class);
+        assertThat(Expressions.names(aggregate.aggregates()), contains("$$SUM$sum(salary)_+_3$1"));
+
+        var source = as(aggregate.child(), StubRelation.class);
+        assertThat(Expressions.names(source.output()), contains("salary"));
+    }
+
+    /**
+     * EsqlProject[[emp_no{f}#6, salary{f}#11, count(salary) where not true{r}#5]]
+     * \_Eval[[0[LONG] AS count(salary) where not true#5]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, ge..]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalNotTrue() {
+        var plan = plan("""
+            from test
+            | keep emp_no, salary
+            | inlinestats count(salary) where not true
+            """);
+
+        var project = as(plan, EsqlProject.class);
+        assertThat(Expressions.names(project.projections()), contains("emp_no", "salary", "count(salary) where not true"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(alias.name(), is("count(salary) where not true"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), is(0L));
+
+        var limit = as(eval.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+    }
+
+    /**
+     * Limit[1000[INTEGER],true]
+     * \_InlineJoin[LEFT,[],[],[]]
+     *   |_EsqlProject[[emp_no{f}#8, salary{f}#13, gender{f}#10]]
+     *   | \_Limit[1000[INTEGER],false]
+     *   |   \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..]
+     *   \_Aggregate[[],[COUNT(salary{f}#13,true[BOOLEAN]) AS m1#7]]
+     *     \_StubRelation[[emp_no{f}#8, salary{f}#13, gender{f}#10]]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalNotFalse() {
+        var plan = plan("""
+            from test
+            | keep emp_no, salary, gender
+            | inlinestats m1 = count(salary) where not false
+            """);
+        var limit = as(plan, Limit.class);
+        var ij = as(limit.child(), InlineJoin.class);
+
+        var left = as(ij.left(), EsqlProject.class);
+        assertThat(Expressions.names(left.projections()), contains("emp_no", "salary", "gender"));
+        var leftLimit = as(left.child(), Limit.class);
+        as(leftLimit.child(), EsRelation.class);
+
+        var right = as(ij.right(), Aggregate.class);
+        assertThat(Expressions.names(right.aggregates()), contains("m1"));
+        assertEquals(1, right.aggregates().size());
+        assertTrue(right.aggregates().get(0).children().get(0) instanceof Count);
+        assertEquals("true", ((Count) right.aggregates().get(0).children().get(0)).filter().toString());
+
+        var source = as(right.child(), StubRelation.class);
+        assertThat(Expressions.names(source.output()), contains("emp_no", "salary", "gender"));
+    }
+
+    /**
+     * EsqlProject[[salary{f}#10, count(salary) where false{r}#4]]
+     * \_Eval[[0[LONG] AS count(salary) where false#4]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalCount() {
+        var plan = plan("""
+            from test
+            | keep salary
+            | inlinestats count(salary) where false
+            """);
+        var project = as(plan, EsqlProject.class);
+        assertThat(Expressions.names(project.projections()), contains("salary", "count(salary) where false"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(alias.name(), is("count(salary) where false"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), is(0L));
+
+        var limit = as(eval.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+    }
+
+    /**
+     * EsqlProject[[salary{f}#10, count_distinct(salary   2)   3 where false{r}#4]]
+     * \_Eval[[3[LONG] AS count_distinct(salary   2)   3 where false#4]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalCountDistinctInExpression() {
+        var plan = plan("""
+            from test
+            | keep salary
+            | inlinestats count_distinct(salary + 2) + 3 where false
+            """);
+        var project = as(plan, EsqlProject.class);
+        assertThat(Expressions.names(project.projections()), contains("salary", "count_distinct(salary + 2) + 3 where false"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(1));
+        var alias = as(eval.fields().getFirst(), Alias.class);
+        assertThat(alias.name(), is("count_distinct(salary + 2) + 3 where false"));
+        assertTrue(alias.child().foldable());
+        assertThat(alias.child().fold(FoldContext.small()), is(3L));
+
+        var limit = as(eval.child(), Limit.class);
+        as(limit.child(), EsRelation.class);
+    }
+
+    /*
+     * Limit[1000[INTEGER],true]
+     * \_InlineJoin[LEFT,[emp_no{f}#17],[emp_no{f}#17],[emp_no{r}#17]]
+     *   |_EsqlProject[[emp_no{f}#17, salary{f}#22]]
+     *   | \_Limit[1000[INTEGER],false]
+     *   |   \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..]
+     *   \_Project[[max{r}#6, max_a{r}#9, min{r}#12, min_a{r}#15, emp_no{f}#17]]
+     *     \_Eval[[null[INTEGER] AS max_a#9, null[INTEGER] AS min_a#15]]
+     *       \_Aggregate[[emp_no{f}#17],[MAX(salary{f}#22,true[BOOLEAN]) AS max#6, MIN(salary{f}#22,true[BOOLEAN]) AS min#12, emp_no{f}#17]]
+     *         \_StubRelation[[emp_no{f}#17, salary{f}#22]]
+     */
+    public void testReplaceInlinestatsFilteredAggWithEvalSameAggWithAndWithoutFilter() {
+        var plan = plan("""
+            from test
+            | keep emp_no, salary
+            | inlinestats max = max(salary), max_a = max(salary) where null,
+                    min = min(salary),
+                    min_a = min(salary) where to_string(null) == "abc"
+              by emp_no
+            """);
+        var limit = as(plan, Limit.class);
+        var ij = as(limit.child(), InlineJoin.class);
+
+        var left = as(ij.left(), EsqlProject.class);
+        assertThat(Expressions.names(left.projections()), contains("emp_no", "salary"));
+        var leftLimit = as(left.child(), Limit.class);
+        as(leftLimit.child(), EsRelation.class);
+
+        var right = as(ij.right(), Project.class);
+        assertThat(Expressions.names(right.projections()), contains("max", "max_a", "min", "min_a", "emp_no"));
+
+        var eval = as(right.child(), Eval.class);
+        assertThat(eval.fields().size(), is(2));
+
+        var aliasMaxA = as(eval.fields().get(0), Alias.class);
+        assertThat(Expressions.name(aliasMaxA), containsString("max_a"));
+        assertTrue(aliasMaxA.child().foldable());
+        assertThat(aliasMaxA.child().fold(FoldContext.small()), nullValue());
+        assertThat(aliasMaxA.child().dataType(), is(INTEGER));
+
+        var aliasMinA = as(eval.fields().get(1), Alias.class);
+        assertThat(Expressions.name(aliasMinA), containsString("min_a"));
+        assertTrue(aliasMinA.child().foldable());
+        assertThat(aliasMinA.child().fold(FoldContext.small()), nullValue());
+        assertThat(aliasMinA.child().dataType(), is(INTEGER));
+
+        var aggregate = as(eval.child(), Aggregate.class);
+        assertThat(Expressions.names(aggregate.aggregates()), contains("max", "min", "emp_no"));
+
+        var source = as(aggregate.child(), StubRelation.class);
+        assertThat(Expressions.names(source.output()), contains("emp_no", "salary"));
+    }
+
+    /*
+     * EsqlProject[[emp_no{f}#9, count{r}#5, cc{r}#8]]
+     * \_Eval[[0[LONG] AS count#5, 0[LONG] AS cc#8]]
+     *   \_TopN[[Order[emp_no{f}#9,ASC,LAST]],3[INTEGER]]
+     *     \_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..]
+     */
+    public void testReplaceTwoConsecutiveInlinestats_WithFalseFilters() {
+        var plan = plan("""
+            from test
+                | keep emp_no
+                | sort emp_no
+                | limit 3
+                | inlinestats count = count(*) where false
+                | inlinestats cc = count_distinct(emp_no) where false
+            """);
+
+        var project = as(plan, EsqlProject.class);
+        assertThat(Expressions.names(project.projections()), contains("emp_no", "count", "cc"));
+
+        var eval = as(project.child(), Eval.class);
+        assertThat(eval.fields().size(), is(2));
+
+        var aliasCount = as(eval.fields().get(0), Alias.class);
+        assertThat(Expressions.name(aliasCount), startsWith("count"));
+        assertTrue(aliasCount.child().foldable());
+        assertThat(aliasCount.child().fold(FoldContext.small()), is(0L));
+
+        var aliasCc = as(eval.fields().get(1), Alias.class);
+        assertThat(Expressions.name(aliasCc), startsWith("cc"));
+        assertTrue(aliasCc.child().foldable());
+        assertThat(aliasCc.child().fold(FoldContext.small()), is(0L));
+
+        var topN = as(eval.child(), TopN.class);
+        as(topN.child(), EsRelation.class);
+    }
+}

+ 18 - 18
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/FieldNameUtilsTests.java

@@ -34,7 +34,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testBasicFromCommandWithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("from test | inlinestats max(salary) by gender", ALL_FIELDS);
     }
 
@@ -43,7 +43,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testBasicFromCommandWithMetadata_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("from test metadata _index, _id, _version | inlinestats max(salary)", ALL_FIELDS);
     }
 
@@ -320,7 +320,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testLimitZero_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             FROM employees
             | INLINESTATS COUNT(*), MAX(salary) BY gender
@@ -335,7 +335,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testDocsDropHeight_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             FROM employees
             | DROP height
@@ -351,7 +351,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testDocsDropHeightWithWildcard_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             FROM employees
             | INLINESTATS MAX(salary) BY gender
@@ -518,7 +518,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testSortWithLimitOne_DropHeight_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("from employees | inlinestats avg(salary) by languages | sort languages | limit 1 | drop height*", ALL_FIELDS);
     }
 
@@ -818,7 +818,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testFilterById_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("FROM apps metadata _id | INLINESTATS max(rate) | WHERE _id == \"4\"", ALL_FIELDS);
     }
 
@@ -1289,7 +1289,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testProjectDropPattern_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | inlinestats max(foo) by bar
@@ -1372,7 +1372,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testCountAllAndOtherStatGrouped_WithInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | inlinestats c = count(*), min = min(emp_no) by languages
@@ -1411,7 +1411,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testCountAllWithEval_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | rename languages as l
@@ -1424,7 +1424,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testKeepAfterEval_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | rename languages as l
@@ -1437,7 +1437,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testKeepBeforeEval_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | rename languages as l
@@ -1450,7 +1450,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testStatsBeforeEval_AndInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | rename languages as l
@@ -1462,7 +1462,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testStatsBeforeInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | stats min = min(salary) by languages
@@ -1471,7 +1471,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testKeepBeforeInlinestats() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertFieldNames("""
             from test
             | keep languages, salary
@@ -2845,7 +2845,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testForkBeforeInlineStatsIgnore() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertTrue("FORK required", EsqlCapabilities.Cap.FORK_V9.isEnabled());
         assertFieldNames("""
             FROM employees
@@ -2858,7 +2858,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testForkBranchWithInlineStatsIgnore() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertTrue("FORK required", EsqlCapabilities.Cap.FORK_V9.isEnabled());
         assertFieldNames("""
             FROM employees
@@ -2872,7 +2872,7 @@ public class FieldNameUtilsTests extends ESTestCase {
     }
 
     public void testForkAfterInlineStatsIgnore() {
-        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V9.isEnabled());
+        assumeTrue("INLINESTATS required", EsqlCapabilities.Cap.INLINESTATS_V10.isEnabled());
         assertTrue("FORK required", EsqlCapabilities.Cap.FORK_V9.isEnabled());
         assertFieldNames("""
             FROM employees