This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new da298089d07 [fix](aggregate) Ignore lambda-local slots in aggregate
validation (#67742)
da298089d07 is described below
commit da298089d0755677f70b6fec12ccefbdc632fe14
Author: morrySnow <[email protected]>
AuthorDate: Thu Sep 10 12:18:59 2026 +0800
[fix](aggregate) Ignore lambda-local slots in aggregate validation (#67742)
## Problem
Valid HAVING predicates were rejected when a lambda function consumed an
aggregate result. Both map and array forms failed because the analyzer
treated
the lambda's local parameters as ungrouped table columns.
## Root cause
Lambda parameters are represented by `ArrayItemSlot`, which extends
`SlotReference`. `FillUpMissingSlots.Resolver` recursively visits HAVING
expressions and applied normal GROUP BY validation to every
`SlotReference`,
including these lambda-local slots. They are bound by their
`ArrayItemReference` and are not inputs from the aggregate child; the
general
expression input-slot collector already excludes them for the same
reason.
## Reproduction
```sql
SELECT id, COUNT(*) AS n
FROM (SELECT 1 id UNION ALL SELECT 1 id) t
GROUP BY id
HAVING map_exists((k, v) -> v > 1, map(1, COUNT(*)));
SELECT id, COUNT(*) AS n
FROM (SELECT 1 id UNION ALL SELECT 1 id) t
GROUP BY id
HAVING array_match_any(array_map(x -> x > 1, array(COUNT(*))));
```
The map query reported an internal map-entry parameter as ungrouped, and
the
array query reported `x` as ungrouped. Both should return `(1, 2)`.
## Fix
Skip `ArrayItemSlot` at the missing-slot resolver entry point. The
lambda
binder owns these local slots, so no aggregate output or GROUP BY
validation
is needed for them. Ordinary `SlotReference` handling is unchanged, and
a
real ungrouped input column inside the surrounding expression is still
rejected.
## Tests
- Added analyzer coverage for both map and array lambda parameters in
HAVING.
- Added a negative analyzer case proving an ordinary ungrouped input
remains
rejected.
- Added execution-level regression coverage for both valid queries and
the
invalid-column boundary.
- Focused FE tests passed: 13 tests, 0 failures.
- Regression suite passed: 1 suite, 0 failed suites.
- Sandbox verification returned `(1, 2)` for both valid queries and
preserved
the expected GROUP BY error for `ungrouped_col`.
---
.../nereids/rules/analysis/FillUpMissingSlots.java | 6 +++++
.../rules/analysis/FillUpMissingSlotsTest.java | 21 +++++++++++++++
.../test_having_with_aggregate_function.out | 6 +++++
.../test_having_with_aggregate_function.groovy | 30 ++++++++++++++++++++++
4 files changed, 63 insertions(+)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlots.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlots.java
index a9536031d23..30440b83ffe 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlots.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlots.java
@@ -24,6 +24,7 @@ import org.apache.doris.nereids.properties.OrderKey;
import org.apache.doris.nereids.rules.Rule;
import org.apache.doris.nereids.rules.RuleType;
import org.apache.doris.nereids.trees.expressions.Alias;
+import
org.apache.doris.nereids.trees.expressions.ArrayItemReference.ArrayItemSlot;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
@@ -207,6 +208,11 @@ public class FillUpMissingSlots implements
AnalysisRuleFactory {
}
public void resolve(Expression expression, ResolvePlanType planType) {
+ // ArrayItemSlot represents a lambda-local variable, not an input
from the aggregate's child.
+ // It is bound by its ArrayItemReference and should not
participate in GROUP BY validation.
+ if (expression instanceof ArrayItemSlot) {
+ return;
+ }
Pair<Optional<Expression>, Boolean> result = lookUp(expression);
Optional<Expression> found = result.first;
boolean isFoundInOutputExpressions = result.second;
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlotsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlotsTest.java
index 922c6ba700c..d3fab2e4856 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlotsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/FillUpMissingSlotsTest.java
@@ -355,6 +355,27 @@ public class FillUpMissingSlotsTest extends
AnalyzeCheckTestBase implements Memo
).when(FieldChecker.check("projects",
Lists.newArrayList(a1.toSlot(), sumA2.toSlot()))));
}
+ @Test
+ void testHavingLambdaLocalSlots() {
+ String mapSql = "SELECT a1, COUNT(*) AS n FROM t1 GROUP BY a1 "
+ + "HAVING map_exists((k, v) -> v > 1, map(1, COUNT(*)))";
+
Assertions.assertNotNull(PlanChecker.from(connectContext).analyze(mapSql).getPlan());
+
+ String arraySql = "SELECT a1, COUNT(*) AS n FROM t1 GROUP BY a1 "
+ + "HAVING array_match_any(array_map(x -> x > 1,
array(COUNT(*))))";
+
Assertions.assertNotNull(PlanChecker.from(connectContext).analyze(arraySql).getPlan());
+
+ ExceptionChecker.expectThrowsWithMsg(
+ AnalysisException.class,
+ "HAVING expression 'a2' must appear in the GROUP BY clause"
+ + " or be used in an aggregate function.",
+ () -> PlanChecker.from(connectContext).analyze(
+ "SELECT a1, COUNT(*) AS n FROM t1 GROUP BY a1 "
+ + "HAVING array_match_any(array_map(x -> x >
1, "
+ + "array(COUNT(*) + a2)))"
+ ));
+ }
+
@Test
void testInvalidHaving() {
ExceptionChecker.expectThrowsWithMsg(
diff --git
a/regression-test/data/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.out
b/regression-test/data/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.out
index b9581decc50..fcf06b6b3bf 100644
---
a/regression-test/data/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.out
+++
b/regression-test/data/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.out
@@ -4,3 +4,9 @@
-- !having_project_2 --
+-- !having_map_lambda_local_slots --
+1 2
+
+-- !having_array_lambda_local_slots --
+1 2
+
diff --git
a/regression-test/suites/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.groovy
b/regression-test/suites/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.groovy
index 8fc24380561..04a2ec51039 100644
---
a/regression-test/suites/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.groovy
+++
b/regression-test/suites/nereids_rules_p0/fill_up_missing_slots/test_having_with_aggregate_function.groovy
@@ -34,6 +34,36 @@ suite("test_having_project") {
SELECT 1 AS c1 FROM t HAVING count(1) > 0
"""
+ qt_having_map_lambda_local_slots """
+ SELECT id, COUNT(*) AS n
+ FROM (SELECT 1 id UNION ALL SELECT 1 id) input
+ GROUP BY id
+ HAVING map_exists((k, v) -> v > 1, map(1, COUNT(*)))
+ ORDER BY id
+ """
+
+ qt_having_array_lambda_local_slots """
+ SELECT id, COUNT(*) AS n
+ FROM (SELECT 1 id UNION ALL SELECT 1 id) input
+ GROUP BY id
+ HAVING array_match_any(array_map(x -> x > 1, array(COUNT(*))))
+ ORDER BY id
+ """
+
+ test {
+ sql """
+ SELECT id, COUNT(*) AS n
+ FROM (
+ SELECT 1 id, 1 AS ungrouped_col
+ UNION ALL
+ SELECT 1 id, 2 AS ungrouped_col
+ ) input
+ GROUP BY id
+ HAVING array_match_any(array_map(x -> x > 1, array(COUNT(*) +
ungrouped_col)))
+ """
+ exception "HAVING expression 'ungrouped_col' must appear in the GROUP
BY clause or be used in an aggregate function"
+ }
+
test {
sql "SELECT 1 AS c1 FROM t HAVING count(1) > 0 OR c1 IS NOT NULL"
exception "HAVING expression 'c1' must appear in the GROUP BY clause
or be used in an aggregate function"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]