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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 685d735e978 branch-4.1: [fix](fe) Prevent unsafe CTE runtime filter 
pushdown #65247 (#66730)
685d735e978 is described below

commit 685d735e9789665940eac3829f863d0cb9f30e61
Author: yujun <[email protected]>
AuthorDate: Fri Aug 14 11:31:39 2026 +0800

    branch-4.1: [fix](fe) Prevent unsafe CTE runtime filter pushdown #65247 
(#66730)
    
    cherry-pick: #65247
---
 .../processor/post/RuntimeFilterGenerator.java     | 45 ++++++++++++++++++
 .../nereids/postprocess/RuntimeFilterTest.java     | 49 ++++++++++++++++++++
 .../runtime_filter/cte-runtime-filter.groovy       | 54 +++++++++++++++++++++-
 3 files changed, 147 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterGenerator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterGenerator.java
index bbc926bbd11..478f4fab801 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterGenerator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterGenerator.java
@@ -60,11 +60,14 @@ import org.apache.doris.statistics.ColumnStatistic;
 import org.apache.doris.thrift.TMinMaxRuntimeFilterType;
 import org.apache.doris.thrift.TRuntimeFilterType;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -88,6 +91,8 @@ public class RuntimeFilterGenerator extends PlanPostProcessor 
{
             JoinType.NULL_AWARE_LEFT_ANTI_JOIN
     );
 
+    private static final Logger LOG = 
LogManager.getLogger(RuntimeFilterGenerator.class);
+
     private static final Set<Class<? extends PhysicalPlan>> SPJ_PLAN = 
ImmutableSet.of(
             PhysicalRelation.class,
             PhysicalProject.class,
@@ -159,6 +164,9 @@ public class RuntimeFilterGenerator extends 
PlanPostProcessor {
                         if (rfsToPushDown.isEmpty()) {
                             break;
                         }
+                        if 
(!canPushDownRuntimeFiltersIntoCTEProducer(rfsToPushDown, cteId)) {
+                            continue;
+                        }
 
                         // the most right deep buildNode from rfsToPushDown is 
used as buildNode for pushDown rf
                         // since the srcExpr are the same, all buildNodes of 
rfToPushDown are in the same tree path
@@ -509,6 +517,43 @@ public class RuntimeFilterGenerator extends 
PlanPostProcessor {
         return expression instanceof Slot ? ((Slot) expression) : null;
     }
 
+    /**
+     * Check whether runtime filters on CTE consumers can be pushed into their 
shared CTE producer.
+     */
+    @VisibleForTesting
+    public static boolean canPushDownRuntimeFiltersIntoCTEProducer(
+            List<RuntimeFilter> rfsToPushDown, CTEId cteId) {
+        if (rfsToPushDown.isEmpty()) {
+            LOG.warn("Skip pushing runtime filters into CTE producer because 
no runtime filters exist for cteId: {}",
+                    cteId);
+            return false;
+        }
+        Set<Expression> producerTargetExpressions = rfsToPushDown.stream()
+                .map(rf -> getProducerTargetExpression(rf, cteId))
+                .collect(Collectors.toSet());
+        return producerTargetExpressions.size() == 1;
+    }
+
+    private static Expression getProducerTargetExpression(RuntimeFilter rf, 
CTEId cteId) {
+        List<PhysicalRelation> targetScans = rf.getTargetScans();
+        List<Expression> targetExpressions = rf.getTargetExpressions();
+        Preconditions.checkArgument(targetScans.size() == 
targetExpressions.size());
+        for (int i = 0; i < targetScans.size(); i++) {
+            PhysicalRelation rel = targetScans.get(i);
+            if (rel instanceof PhysicalCTEConsumer
+                    && ((PhysicalCTEConsumer) rel).getCteId().equals(cteId)) {
+                PhysicalCTEConsumer consumer = (PhysicalCTEConsumer) rel;
+                Expression targetExpression = targetExpressions.get(i);
+                Map<Expression, Expression> replaceMap = Maps.newHashMap();
+                for (Slot slot : targetExpression.getInputSlots()) {
+                    replaceMap.put(slot, consumer.getProducerSlot(slot));
+                }
+                return ExpressionUtils.replace(targetExpression, replaceMap);
+            }
+        }
+        throw new IllegalStateException("runtime filter does not target cteId: 
" + cteId);
+    }
+
     private boolean doPushDownIntoCTEProducerInternal(RuntimeFilter rf, 
Expression targetExpression,
                                                    RuntimeFilterContext ctx, 
PhysicalCTEProducer cteProducer) {
         PhysicalPlan inputPlanNode = (PhysicalPlan) cteProducer.child(0);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
index 59538f98e22..4122ceaabbe 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
@@ -28,32 +28,46 @@ import org.apache.doris.nereids.hint.DistributeHint;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.processor.post.PlanPostProcessors;
 import org.apache.doris.nereids.processor.post.RuntimeFilterContext;
+import org.apache.doris.nereids.processor.post.RuntimeFilterGenerator;
 import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.Add;
 import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.CTEId;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.Subtract;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
 import org.apache.doris.nereids.trees.plans.DistributeType;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin;
 import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.RuntimeFilter;
+import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.planner.RuntimeFilterId;
 import org.apache.doris.qe.OriginStatement;
+import org.apache.doris.thrift.TMinMaxRuntimeFilterType;
+import org.apache.doris.thrift.TRuntimeFilterType;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -449,6 +463,41 @@ public class RuntimeFilterTest extends SSBTestBase {
                 .getAppliedRuntimeFilters().size());
     }
 
+    @Test
+    public void 
testPushSharedCteRuntimeFilterOnlyForSameProducerTargetExpression() {
+        CTEId cteId = new CTEId(1);
+        SlotReference src = new SlotReference("src", IntegerType.INSTANCE);
+        SlotReference producerPk = new SlotReference("pk", 
IntegerType.INSTANCE);
+        SlotReference consumerPk1 = new SlotReference("pk", 
IntegerType.INSTANCE);
+        SlotReference consumerPk2 = new SlotReference("pk", 
IntegerType.INSTANCE);
+
+        List<RuntimeFilter> sameTargetFilters = ImmutableList.of(
+                newCteConsumerRuntimeFilter(src, consumerPk1, consumerPk1, 
producerPk, cteId),
+                newCteConsumerRuntimeFilter(src, consumerPk2, consumerPk2, 
producerPk, cteId));
+        
Assertions.assertTrue(RuntimeFilterGenerator.canPushDownRuntimeFiltersIntoCTEProducer(
+                sameTargetFilters, cteId));
+
+        List<RuntimeFilter> differentTargetFilters = ImmutableList.of(
+                newCteConsumerRuntimeFilter(src, consumerPk1,
+                        new Add(consumerPk1, new IntegerLiteral(6)), 
producerPk, cteId),
+                newCteConsumerRuntimeFilter(src, consumerPk2,
+                        new Subtract(consumerPk2, new IntegerLiteral(1)), 
producerPk, cteId));
+        
Assertions.assertFalse(RuntimeFilterGenerator.canPushDownRuntimeFiltersIntoCTEProducer(
+                differentTargetFilters, cteId));
+    }
+
+    private RuntimeFilter newCteConsumerRuntimeFilter(Expression src, Slot 
targetSlot,
+            Expression targetExpression, Slot producerSlot, CTEId cteId) {
+        PhysicalCTEConsumer consumer = Mockito.mock(PhysicalCTEConsumer.class);
+        Mockito.when(consumer.getCteId()).thenReturn(cteId);
+        
Mockito.when(consumer.getProducerSlot(targetSlot)).thenReturn(producerSlot);
+        AbstractPhysicalJoin builder = 
Mockito.mock(AbstractPhysicalJoin.class);
+        return new 
RuntimeFilter(RuntimeFilterId.createGenerator().getNextId(), src,
+                ImmutableList.of(targetSlot), 
ImmutableList.of(targetExpression),
+                TRuntimeFilterType.IN_OR_BLOOM, 0, builder, -1L, true,
+                TMinMaxRuntimeFilterType.MIN_MAX, consumer);
+    }
+
     @Test
     public void testRuntimeFilterBlockByRecCte() {
         String sql = new StringBuilder().append("with recursive xx as 
(\n").append("  select\n")
diff --git 
a/regression-test/suites/nereids_p0/runtime_filter/cte-runtime-filter.groovy 
b/regression-test/suites/nereids_p0/runtime_filter/cte-runtime-filter.groovy
index 6e9393e05d2..d7a6cb2ca93 100644
--- a/regression-test/suites/nereids_p0/runtime_filter/cte-runtime-filter.groovy
+++ b/regression-test/suites/nereids_p0/runtime_filter/cte-runtime-filter.groovy
@@ -68,4 +68,56 @@ suite('cte-runtime-filter') {
         from cte a
         join cte_runtime_filter_table b on a.user_id=b.user_id ;
         '''
-}
\ No newline at end of file
+
+    sql '''
+    drop table if exists cte_runtime_filter_shared_probe;
+    create table cte_runtime_filter_shared_probe (
+        pk int not null
+    ) ENGINE=OLAP
+    DUPLICATE KEY(pk)
+    DISTRIBUTED BY HASH(pk) BUCKETS 1
+    PROPERTIES (
+        "replication_allocation" = "tag.location.default: 1"
+    );
+
+    insert into cte_runtime_filter_shared_probe values (4), (11);
+
+    drop table if exists cte_runtime_filter_shared_build;
+    create table cte_runtime_filter_shared_build (
+        pk bigint not null
+    ) ENGINE=OLAP
+    DUPLICATE KEY(pk)
+    DISTRIBUTED BY HASH(pk) BUCKETS 1
+    PROPERTIES (
+        "replication_allocation" = "tag.location.default: 1"
+    );
+
+    insert into cte_runtime_filter_shared_build values (10);
+
+    set enable_nereids_planner=true;
+    set enable_fallback_to_original_planner=false;
+    set inline_cte_referenced_threshold=0;
+    set disable_join_reorder=true;
+    set enable_runtime_filter_prune=false;
+    set runtime_filter_mode=global;
+    set runtime_filter_wait_infinitely=true;
+    set runtime_filter_type=2;
+    '''
+
+    def sharedCteRuntimeFilterSql = '''
+        with probe as (
+            select pk from cte_runtime_filter_shared_probe
+        )
+        select count(*)
+        from probe p1
+        cross join probe p2
+        join cte_runtime_filter_shared_build b
+            on cast(p1.pk as bigint) + 6 = b.pk
+            and cast(p2.pk as bigint) - 1 = b.pk
+    '''
+    assertEquals([[1L]], sql(sharedCteRuntimeFilterSql))
+
+    sql "set runtime_filter_type=''"
+    assertEquals([[1L]], sql(sharedCteRuntimeFilterSql))
+    sql "set runtime_filter_wait_infinitely=false"
+}


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

Reply via email to