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]

Reply via email to