This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 7cd9e8b1360 branch-4.2: [fix](aggregate) Guard aggregate not-null 
inference by null semantics #67668 (#70071)
7cd9e8b1360 is described below

commit 7cd9e8b1360a502adf7427eb282343aaba22ee1e
Author: morrySnow <[email protected]>
AuthorDate: Thu Nov 5 09:40:30 2026 +0800

    branch-4.2: [fix](aggregate) Guard aggregate not-null inference by null 
semantics #67668 (#70071)
    
    picked from master #67668
---
 .../nereids/rules/rewrite/InferAggNotNull.java     | 23 ++------
 .../apache/doris/nereids/util/ExpressionUtils.java | 60 +++++++++++++++------
 .../nereids/rules/rewrite/InferAggNotNullTest.java | 24 ++++++++-
 .../eliminate_aggregate_casewhen.out               |  3 +-
 .../infer_agg_not_null/infer_agg_not_null.out      | 12 +++++
 .../infer_agg_not_null/infer_agg_not_null.groovy   | 63 ++++++++++++++++++++++
 6 files changed, 150 insertions(+), 35 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
index e27a7b8cfad..f5fc26cb436 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
@@ -23,11 +23,7 @@ import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Not;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
-import org.apache.doris.nereids.trees.expressions.functions.agg.Avg;
-import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
-import org.apache.doris.nereids.trees.expressions.functions.agg.Max;
-import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
-import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
+import 
org.apache.doris.nereids.trees.expressions.functions.agg.NullIgnoringAggregateFunction;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.algebra.Filter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
@@ -40,9 +36,7 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
-/**
- * InferNotNull from Agg count(distinct);
- */
+/** Infer not-null predicates from null-ignoring global aggregate arguments. */
 public class InferAggNotNull extends OneRewriteRuleFactory {
     @Override
     public Rule build() {
@@ -103,20 +97,13 @@ public class InferAggNotNull extends OneRewriteRuleFactory 
{
 
     private Set<Expression> inferFunctionNotNulls(
             AggregateFunction aggregateFunction, CascadesContext 
cascadesContext) {
-        return 
ExpressionUtils.inferNotNull(ImmutableSet.copyOf(aggregateFunction.children()), 
cascadesContext);
+        return ExpressionUtils.inferNotNullForNullIgnoringAggregate(
+                ImmutableSet.copyOf(aggregateFunction.children()), 
cascadesContext);
     }
 
     private boolean canInferFunctionNotNull(AggregateFunction 
aggregateFunction) {
-        return isSupportedAggregateFunction(aggregateFunction)
+        return aggregateFunction instanceof NullIgnoringAggregateFunction
                 && !aggregateFunction.children().isEmpty();
     }
 
-    private boolean isSupportedAggregateFunction(AggregateFunction 
aggregateFunction) {
-        return aggregateFunction instanceof Count
-                || aggregateFunction instanceof Avg
-                || aggregateFunction instanceof Sum
-                || aggregateFunction instanceof Max
-                || aggregateFunction instanceof Min;
-    }
-
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
index 11167e70a95..07c5e16495c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
@@ -1002,15 +1002,7 @@ public class ExpressionUtils {
      * infer notNulls slot from predicate
      */
     public static Set<Slot> inferNotNullSlots(Set<Expression> predicates, 
CascadesContext cascadesContext) {
-        Set<Slot> targetSlots = new HashSet<>();
-        for (Expression predicate : predicates) {
-            for (Slot slot : predicate.getInputSlots()) {
-                if (!(slot instanceof MarkJoinSlotReference)) {
-                    targetSlots.add(slot);
-                }
-            }
-        }
-        return inferNotNullSlots(predicates, targetSlots, cascadesContext);
+        return inferNotNullSlots(predicates, 
collectNotNullInferenceTargetSlots(predicates), cascadesContext);
     }
 
     /**
@@ -1018,6 +1010,11 @@ public class ExpressionUtils {
      */
     public static Set<Slot> inferNotNullSlots(Set<Expression> predicates, 
Set<Slot> targetSlots,
             CascadesContext cascadesContext) {
+        return inferNotNullSlots(predicates, targetSlots, cascadesContext, 
ExpressionUtils::isFalseOrNull);
+    }
+
+    private static Set<Slot> inferNotNullSlots(Set<Expression> predicates, 
Set<Slot> targetSlots,
+            CascadesContext cascadesContext, Predicate<Expression> 
nullInputResultPredicate) {
         ImmutableSet.Builder<Slot> notNullSlots = 
ImmutableSet.builderWithExpectedSize(targetSlots.size());
         Set<Slot> inputSlots = new HashSet<>();
         for (Expression predicate : predicates) {
@@ -1036,7 +1033,7 @@ public class ExpressionUtils {
             }
             inputSlots = mergedInputSlots.get();
             for (Slot slot : candidateSlots) {
-                if (isNullRejecting(predicate, slot, cascadesContext)) {
+                if (matchesWhenSlotIsNull(predicate, slot, cascadesContext, 
nullInputResultPredicate)) {
                     notNullSlots.add(slot);
                 }
             }
@@ -1044,14 +1041,27 @@ public class ExpressionUtils {
         return notNullSlots.build();
     }
 
-    private static boolean isNullRejecting(Expression predicate, Slot slot, 
CascadesContext cascadesContext) {
+    private static Set<Slot> 
collectNotNullInferenceTargetSlots(Set<Expression> expressions) {
+        Set<Slot> targetSlots = new HashSet<>();
+        for (Expression expression : expressions) {
+            for (Slot slot : expression.getInputSlots()) {
+                if (!(slot instanceof MarkJoinSlotReference)) {
+                    targetSlots.add(slot);
+                }
+            }
+        }
+        return targetSlots;
+    }
+
+    private static boolean matchesWhenSlotIsNull(Expression expression, Slot 
slot, CascadesContext cascadesContext,
+            Predicate<Expression> nullInputResultPredicate) {
         Map<Expression, Expression> replaceMap = new HashMap<>();
         Literal nullLiteral = new NullLiteral(slot.getDataType());
         replaceMap.put(slot, nullLiteral);
         Expression evalExpr = FoldConstantRule.evaluate(
-                ExpressionUtils.replace(predicate, replaceMap),
+                ExpressionUtils.replace(expression, replaceMap),
                 new ExpressionRewriteContext(cascadesContext));
-        return evalExpr.isNullLiteral() || 
BooleanLiteral.FALSE.equals(evalExpr);
+        return nullInputResultPredicate.apply(evalExpr);
     }
 
     private static Optional<Set<Slot>> mergeInputSlotsWithinLimit(Set<Slot> 
inputSlots, Set<Slot> predicateInputSlots) {
@@ -1070,8 +1080,28 @@ public class ExpressionUtils {
      * infer notNulls slot from predicate
      */
     public static Set<Expression> inferNotNull(Set<Expression> predicates, 
CascadesContext cascadesContext) {
-        ImmutableSet.Builder<Expression> newPredicates = 
ImmutableSet.builderWithExpectedSize(predicates.size());
-        for (Slot slot : inferNotNullSlots(predicates, cascadesContext)) {
+        return buildNotNullPredicates(inferNotNullSlots(predicates, 
cascadesContext));
+    }
+
+    /**
+     * Infer not-null predicates for an aggregate that ignores rows with SQL 
NULL arguments.
+     *
+     * <p>The caller must first establish the aggregate's null-input contract. 
Even for a
+     * null-ignoring aggregate, a row can only be discarded when its argument 
evaluates to SQL
+     * NULL. FALSE is null-rejecting as a filter predicate, but it is a valid 
aggregate argument
+     * and must be kept.
+     */
+    public static Set<Expression> inferNotNullForNullIgnoringAggregate(
+            Set<Expression> arguments, CascadesContext cascadesContext) {
+        Set<Slot> targetSlots = collectNotNullInferenceTargetSlots(arguments);
+        Set<Slot> notNullSlots = inferNotNullSlots(
+                arguments, targetSlots, cascadesContext, 
Expression::isNullLiteral);
+        return buildNotNullPredicates(notNullSlots);
+    }
+
+    private static Set<Expression> buildNotNullPredicates(Set<Slot> 
notNullSlots) {
+        ImmutableSet.Builder<Expression> newPredicates = 
ImmutableSet.builderWithExpectedSize(notNullSlots.size());
+        for (Slot slot : notNullSlots) {
             newPredicates.add(new Not(new IsNull(slot), false));
         }
         return newPredicates.build();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
index c127f7e40ad..f0e709dc1b7 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
@@ -37,6 +37,8 @@ import 
org.apache.doris.nereids.trees.expressions.functions.agg.CountByEnum;
 import org.apache.doris.nereids.trees.expressions.functions.agg.GroupConcat;
 import org.apache.doris.nereids.trees.expressions.functions.agg.MapAgg;
 import org.apache.doris.nereids.trees.expressions.functions.agg.MapAggV2;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Max;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.NullIgnoringAggregateFunction;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Sum0;
@@ -79,13 +81,33 @@ class InferAggNotNullTest implements 
MemoPatternMatchSupported {
                 );
     }
 
+    @Test
+    void testNotInferWhenAggregateArgumentReturnsFalseForNullInput() {
+        Expression isNotNull = new Not(new IsNull(scan1.getOutput().get(1)));
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .aggGroupUsingIndex(ImmutableList.of(),
+                        ImmutableList.of(new Alias(new Count(false, 
isNotNull), "cnt")))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new InferAggNotNull())
+                .matches(
+                        logicalAggregate(
+                                logicalOlapScan()
+                        )
+                );
+    }
+
     @Test
     void testInferMultipleAggregateSameInput() {
         LogicalPlan plan = new LogicalPlanBuilder(scan1)
                 .aggGroupUsingIndex(ImmutableList.of(),
                         ImmutableList.of(
+                                new Alias(new Count(false, 
scan1.getOutput().get(1)), "count_k"),
                                 new Alias(new Avg(scan1.getOutput().get(1)), 
"avg_k"),
-                                new Alias(new Sum(scan1.getOutput().get(1)), 
"sum_k")))
+                                new Alias(new Sum(scan1.getOutput().get(1)), 
"sum_k"),
+                                new Alias(new Max(scan1.getOutput().get(1)), 
"max_k"),
+                                new Alias(new Min(scan1.getOutput().get(1)), 
"min_k")))
                 .build();
 
         PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
diff --git 
a/regression-test/data/nereids_rules_p0/eliminate_aggregate_casewhen/eliminate_aggregate_casewhen.out
 
b/regression-test/data/nereids_rules_p0/eliminate_aggregate_casewhen/eliminate_aggregate_casewhen.out
index 4de89d48061..df19fb31688 100644
--- 
a/regression-test/data/nereids_rules_p0/eliminate_aggregate_casewhen/eliminate_aggregate_casewhen.out
+++ 
b/regression-test/data/nereids_rules_p0/eliminate_aggregate_casewhen/eliminate_aggregate_casewhen.out
@@ -200,7 +200,8 @@ PhysicalResultSink
 --------hashAgg[GLOBAL]
 ----------hashAgg[LOCAL]
 ------------PhysicalProject
---------------PhysicalOlapScan[t4]
+--------------filter((t4.c4 = 102))
+----------------PhysicalOlapScan[t4]
 
 -- !bool_or_eliminate --
 PhysicalResultSink
diff --git 
a/regression-test/data/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.out
 
b/regression-test/data/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.out
index feb9fa5c982..273ae0d5cac 100644
--- 
a/regression-test/data/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.out
+++ 
b/regression-test/data/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.out
@@ -8,3 +8,15 @@
 -- !null_sensitive_aggregates_keep_rows --
 3      3
 
+-- !count_boolean_argument --
+3
+
+-- !count_nullable_slot --
+1
+
+-- !count_coalesce_argument --
+3
+
+-- !mixed_count_array_agg --
+1      3
+
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.groovy
 
b/regression-test/suites/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.groovy
index 056287766cd..2537a261aea 100644
--- 
a/regression-test/suites/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/infer_agg_not_null/infer_agg_not_null.groovy
@@ -73,4 +73,67 @@ suite("infer_agg_not_null") {
         """
         contains "LEFT_OUTER_JOIN"
     }
+    order_qt_count_boolean_argument """
+        SELECT COUNT(r.v IS NOT NULL)
+        FROM infer_agg_not_null_left l
+        LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+    """
+
+    order_qt_count_nullable_slot """
+        SELECT COUNT(r.v)
+        FROM infer_agg_not_null_left l
+        LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+    """
+
+    order_qt_count_coalesce_argument """
+        SELECT COUNT(COALESCE(r.v, 0))
+        FROM infer_agg_not_null_left l
+        LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+    """
+
+    order_qt_mixed_count_array_agg """
+        SELECT COUNT(r.v), ARRAY_SIZE(ARRAY_AGG(r.v))
+        FROM infer_agg_not_null_left l
+        LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+    """
+
+    explain {
+        sql """
+            SHAPE PLAN
+            SELECT COUNT(r.v IS NOT NULL)
+            FROM infer_agg_not_null_left l
+            LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+        """
+        contains "LEFT_OUTER_JOIN"
+    }
+
+    explain {
+        sql """
+            SHAPE PLAN
+            SELECT COUNT(r.v)
+            FROM infer_agg_not_null_left l
+            LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+        """
+        contains "INNER_JOIN"
+    }
+
+    explain {
+        sql """
+            SHAPE PLAN
+            SELECT COUNT(COALESCE(r.v, 0))
+            FROM infer_agg_not_null_left l
+            LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+        """
+        contains "LEFT_OUTER_JOIN"
+    }
+
+    explain {
+        sql """
+            SHAPE PLAN
+            SELECT COUNT(r.v), ARRAY_SIZE(ARRAY_AGG(r.v))
+            FROM infer_agg_not_null_left l
+            LEFT JOIN infer_agg_not_null_right r ON l.k = r.k
+        """
+        contains "LEFT_OUTER_JOIN"
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to