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 122cca6f5ab branch-4.2: [fix](cascades) Guard aggregate semi-join 
transposition against unsafe expressions #67831 (#70022)
122cca6f5ab is described below

commit 122cca6f5abd0d8788ca77cd3171a49e032e2049
Author: morrySnow <[email protected]>
AuthorDate: Thu Nov 5 10:14:25 2026 +0800

    branch-4.2: [fix](cascades) Guard aggregate semi-join transposition against 
unsafe expressions #67831 (#70022)
    
    picked from master #67831
    
    Co-authored-by: feiniaofeiafei <[email protected]>
---
 .../exploration/TransposeAggSemiJoinProject.java   |   2 +-
 .../rules/rewrite/TransposeSemiJoinAgg.java        |  17 +++
 .../rules/rewrite/TransposeSemiJoinAggProject.java |   2 +-
 .../TransposeSemiJoinAggExpressionTest.java        | 149 +++++++++++++++++++++
 .../transposeSemiJoinAggExpression.out             |  29 ++++
 .../transposeSemiJoinAggExpression.groovy          |  90 +++++++++++++
 6 files changed, 287 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/TransposeAggSemiJoinProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/TransposeAggSemiJoinProject.java
index 4f63f13e991..459715d4a92 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/TransposeAggSemiJoinProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/TransposeAggSemiJoinProject.java
@@ -40,7 +40,7 @@ public class TransposeAggSemiJoinProject extends 
OneExplorationRuleFactory {
                 .then(agg -> {
                     LogicalProject<LogicalJoin<GroupPlan, GroupPlan>> project 
= agg.child();
                     LogicalJoin<GroupPlan, GroupPlan> join = project.child();
-                    if (!TransposeSemiJoinAgg.canTranspose(agg, join)) {
+                    if (!TransposeSemiJoinAgg.canTranspose(agg, join, 
project)) {
                         return null;
                     }
                     Plan newJoin = 
join.withChildren(agg.withChildren(project.withChildren(join.left())), 
join.right());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAgg.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAgg.java
index 4006d614ca8..1347deabac8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAgg.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAgg.java
@@ -20,9 +20,11 @@ package org.apache.doris.nereids.rules.rewrite;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.expressions.Slot;
+import 
org.apache.doris.nereids.trees.expressions.functions.NoneMovableFunction;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
 import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.qe.ConnectContext;
 
 import java.util.Set;
@@ -51,6 +53,10 @@ public class TransposeSemiJoinAgg extends 
OneRewriteRuleFactory {
      */
     public static boolean canTranspose(LogicalAggregate<? extends Plan> 
aggregate,
             LogicalJoin<? extends Plan, ? extends Plan> join) {
+        // Transposition changes the rows or the number of times these 
expressions are evaluated.
+        if (containsUnsafeExpression(join) || 
containsUnsafeExpression(aggregate)) {
+            return false;
+        }
         Set<Slot> canPushDownSlots = 
PushDownFilterThroughAggregation.getCanPushDownSlots(aggregate);
         // avoid push down scalar agg.
         if (canPushDownSlots.isEmpty()) {
@@ -59,4 +65,15 @@ public class TransposeSemiJoinAgg extends 
OneRewriteRuleFactory {
         Set<Slot> leftConditionSlot = join.getLeftConditionSlot();
         return canPushDownSlots.containsAll(leftConditionSlot);
     }
+
+    /** Check the intermediate project as well as the aggregate and join. */
+    public static boolean canTranspose(LogicalAggregate<? extends Plan> 
aggregate,
+            LogicalJoin<? extends Plan, ? extends Plan> join, LogicalProject<? 
extends Plan> project) {
+        return !containsUnsafeExpression(project) && canTranspose(aggregate, 
join);
+    }
+
+    private static boolean containsUnsafeExpression(Plan plan) {
+        return plan.getExpressions().stream().anyMatch(expression -> 
expression.containsVolatileExpression()
+                || expression.containsType(NoneMovableFunction.class));
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggProject.java
index 17ca8f71395..c0112c59c81 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggProject.java
@@ -38,7 +38,7 @@ public class TransposeSemiJoinAggProject extends 
OneRewriteRuleFactory {
                 .then(join -> {
                     LogicalProject<LogicalAggregate<Plan>> project = 
join.left();
                     LogicalAggregate<Plan> aggregate = project.child();
-                    if (!TransposeSemiJoinAgg.canTranspose(aggregate, join)) {
+                    if (!TransposeSemiJoinAgg.canTranspose(aggregate, join, 
project)) {
                         return null;
                     }
                     Plan newPlan = 
aggregate.withChildren(join.withChildren(aggregate.child(), join.right()));
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggExpressionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggExpressionTest.java
new file mode 100644
index 00000000000..9a8477e07ff
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/TransposeSemiJoinAggExpressionTest.java
@@ -0,0 +1,149 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+import org.apache.doris.nereids.rules.exploration.TransposeAggSemiJoin;
+import org.apache.doris.nereids.rules.exploration.TransposeAggSemiJoinProject;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Random;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.LogicalPlanBuilder;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+class TransposeSemiJoinAggExpressionTest {
+    enum Location {
+        HASH_CONDITION, OTHER_CONDITION, PROJECT, GROUP_KEY, AGG_ARGUMENT
+    }
+
+    enum ExpressionKind {
+        SAFE, VOLATILE, NON_MOVABLE
+    }
+
+    static Stream<Arguments> cases() {
+        List<Arguments> cases = new ArrayList<>();
+        for (JoinType joinType : new JoinType[] {JoinType.LEFT_SEMI_JOIN, 
JoinType.LEFT_ANTI_JOIN}) {
+            for (boolean eager : new boolean[] {true, false}) {
+                for (boolean withProject : new boolean[] {true, false}) {
+                    for (Location location : Location.values()) {
+                        if (location == Location.PROJECT && !withProject) {
+                            continue;
+                        }
+                        for (ExpressionKind kind : ExpressionKind.values()) {
+                            cases.add(Arguments.of(joinType, eager, 
withProject, location, kind));
+                        }
+                    }
+                }
+            }
+        }
+        return cases.stream();
+    }
+
+    @ParameterizedTest(name = "{0}, eager={1}, project={2}, {3}, {4}")
+    @MethodSource("cases")
+    void expressionMovement(JoinType joinType, boolean eager, boolean 
withProject,
+            Location location, ExpressionKind kind) {
+        LogicalOlapScan left = PlanConstructor.newLogicalOlapScan(0, "t1", 0);
+        LogicalOlapScan right = PlanConstructor.newLogicalOlapScan(1, "t2", 0);
+        Slot key = left.getOutput().get(0);
+        Slot value = left.getOutput().get(1);
+        Expression expression;
+        switch (kind) {
+            case VOLATILE:
+                expression = new Cast(new Random(), IntegerType.INSTANCE);
+                break;
+            case NON_MOVABLE:
+                expression = new Cast(new AssertTrue(new GreaterThan(key, new 
IntegerLiteral(0)),
+                        new VarcharLiteral("positive key")), 
IntegerType.INSTANCE);
+                break;
+            default:
+                expression = new Add(key, new IntegerLiteral(1));
+        }
+        Expression hashLeft = location == Location.HASH_CONDITION ? new 
Add(key, expression) : key;
+        List<Expression> hash = ImmutableList.of(new EqualTo(hashLeft, 
right.getOutput().get(0)));
+        List<Expression> other = location == Location.OTHER_CONDITION
+                ? ImmutableList.of(new GreaterThan(expression, new 
IntegerLiteral(0))) : ImmutableList.of();
+        List<Expression> groupKeys = location == Location.GROUP_KEY
+                ? ImmutableList.of(key, expression) : ImmutableList.of(key);
+        Alias projected = new Alias(expression, "checked");
+        Expression argument = location == Location.AGG_ARGUMENT ? expression : 
value;
+        LogicalPlanBuilder builder = new LogicalPlanBuilder(left);
+        if (eager) {
+            builder = builder.join(right, joinType, hash, other);
+            if (withProject) {
+                List<NamedExpression> projects = new 
ArrayList<>(left.getOutput());
+                if (location == Location.PROJECT && kind != 
ExpressionKind.SAFE) {
+                    projects.add(projected);
+                    argument = projected.toSlot();
+                }
+                builder = builder.projectExprs(projects);
+            }
+        }
+        builder = builder.agg(groupKeys, ImmutableList.of(key, new Alias(new 
Sum(argument), "sum")));
+        if (!eager) {
+            if (withProject) {
+                List<NamedExpression> projects = new 
ArrayList<>(builder.build().getOutput());
+                if (location == Location.PROJECT && kind != 
ExpressionKind.SAFE) {
+                    projects.add(projected);
+                }
+                builder = builder.projectExprs(projects);
+            }
+            builder = builder.join(right, joinType, hash, other);
+        }
+        LogicalPlan plan = builder.build();
+        PlanChecker checker = 
PlanChecker.from(MemoTestUtils.createConnectContext(), plan);
+        if (eager) {
+            checker.applyExploration(withProject ? 
TransposeAggSemiJoinProject.INSTANCE.build()
+                    : TransposeAggSemiJoin.INSTANCE.build());
+            Assertions.assertEquals(kind == ExpressionKind.SAFE ? 2 : 1, 
checker.getAllPlan().size());
+        } else {
+            checker.applyTopDown(withProject ? new 
TransposeSemiJoinAggProject() : new TransposeSemiJoinAgg());
+            Class<?> expectedRoot = kind == ExpressionKind.SAFE
+                    ? (withProject ? LogicalProject.class : 
LogicalAggregate.class) : LogicalJoin.class;
+            Assertions.assertEquals(expectedRoot, 
checker.getPlan().getClass());
+        }
+    }
+}
diff --git 
a/regression-test/data/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.out
 
b/regression-test/data/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.out
new file mode 100644
index 00000000000..fd103e508f6
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.out
@@ -0,0 +1,29 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !non_movable_project --
+1      64
+
+-- !non_movable_argument --
+1      64
+
+-- !volatile_semi_plan --
+PhysicalResultSink
+--hashJoin[LEFT_SEMI_JOIN broadcast] hashCondition=((d.k = s.k)) 
otherCondition=((random() < 0.5))
+----hashAgg[GLOBAL]
+------PhysicalProject
+--------PhysicalOlapScan[transpose_semi_agg_expression_t]
+----PhysicalOlapScan[transpose_semi_agg_expression_s(s)]
+
+-- !volatile_anti_plan --
+PhysicalResultSink
+--hashJoin[LEFT_ANTI_JOIN broadcast] hashCondition=((d.k = s.k)) 
otherCondition=((random() < 0.5))
+----hashAgg[GLOBAL]
+------PhysicalProject
+--------PhysicalOlapScan[transpose_semi_agg_expression_t]
+----PhysicalOlapScan[transpose_semi_agg_expression_s(s)]
+
+-- !volatile_semi_condition --
+0
+
+-- !volatile_anti_condition --
+0
+
diff --git 
a/regression-test/suites/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.groovy
 
b/regression-test/suites/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.groovy
new file mode 100644
index 00000000000..e1ccbf8cd61
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/transposeJoin/transposeSemiJoinAggExpression.groovy
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("transposeSemiJoinAggExpression") {
+    sql "set runtime_filter_mode=OFF"
+    sql "set disable_join_reorder=false"
+    sql "set enable_dphyp_optimizer=false"
+
+    sql "drop table if exists transpose_semi_agg_expression_t"
+    sql """
+        create table transpose_semi_agg_expression_t (k int not null, v int 
not null)
+        duplicate key(k) distributed by hash(k) buckets 1
+        properties("replication_num"="1")
+    """
+    sql "drop table if exists transpose_semi_agg_expression_s"
+    sql """
+        create table transpose_semi_agg_expression_s (k int not null)
+        duplicate key(k) distributed by hash(k) buckets 1
+        properties("replication_num"="1")
+    """
+    sql "insert into transpose_semi_agg_expression_t select 1, 1 from 
numbers(\"number\"=\"64\")"
+    sql "insert into transpose_semi_agg_expression_t values (2, -1)"
+    sql "insert into transpose_semi_agg_expression_s values (1)"
+    sql "analyze table transpose_semi_agg_expression_t with sync"
+    sql "analyze table transpose_semi_agg_expression_s with sync"
+
+    // The unmatched negative row must be removed before evaluating 
assert_true.
+    order_qt_non_movable_project """
+        select k, sum(checked) from (
+            select t.k, cast(assert_true(t.v > 0, 'positive rows only') as 
int) checked
+            from transpose_semi_agg_expression_t t
+            left semi join transpose_semi_agg_expression_s s on t.k = s.k
+        ) q group by k
+    """
+    order_qt_non_movable_argument """
+        select t.k, sum(cast(assert_true(t.v > 0, 'positive rows only') as 
int))
+        from transpose_semi_agg_expression_t t
+        left semi join transpose_semi_agg_expression_s s on t.k = s.k
+        group by t.k
+    """
+
+    // Inspect the original query directly: the join must stay above 
aggregation.
+    qt_volatile_semi_plan """
+        explain shape plan
+        select d.k, d.c from (
+            select k, count(*) c from transpose_semi_agg_expression_t group by 
k
+        ) d left semi join transpose_semi_agg_expression_s s
+        on d.k = s.k and random() < 0.5
+    """
+    qt_volatile_anti_plan """
+        explain shape plan
+        select d.k, d.c from (
+            select k, count(*) c from transpose_semi_agg_expression_t group by 
k
+        ) d left anti join transpose_semi_agg_expression_s s
+        on d.k = s.k and random() < 0.5
+    """
+
+    // A surviving group must retain all 64 rows, regardless of the random 
predicate.
+    // Count only invalid results so the expected output is deterministic.
+    order_qt_volatile_semi_condition """
+        select count(*) from (
+            select d.k, d.c from (
+                select k, count(*) c from transpose_semi_agg_expression_t 
group by k
+            ) d left semi join transpose_semi_agg_expression_s s
+            on d.k = s.k and random() < 0.5
+        ) q where k = 1 and c <> 64
+    """
+    order_qt_volatile_anti_condition """
+        select count(*) from (
+            select d.k, d.c from (
+                select k, count(*) c from transpose_semi_agg_expression_t 
group by k
+            ) d left anti join transpose_semi_agg_expression_s s
+            on d.k = s.k and random() < 0.5
+        ) q where k = 1 and c <> 64
+    """
+}


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

Reply via email to