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 395384e40bd branch-4.2: [fix](fe) Deduplicate recursive CTE fragment
reset requests #67738 (#70038)
395384e40bd is described below
commit 395384e40bd8d18108702af4d031ea68440bbf20
Author: morrySnow <[email protected]>
AuthorDate: Tue Nov 3 17:20:53 2026 +0800
branch-4.2: [fix](fe) Deduplicate recursive CTE fragment reset requests
#67738 (#70038)
picked from master #67738
Co-authored-by: Mryange <[email protected]>
---
.../doris/nereids/rules/rewrite/CTEInline.java | 23 +++++
.../doris/qe/runtime/ThriftPlansBuilder.java | 6 ++
.../doris/nereids/rules/rewrite/CTEInlineTest.java | 39 ++++++++
.../data/recursive_cte/shared_cte_reset_test.out | 8 ++
.../recursive_cte/shared_cte_reset_test.groovy | 108 +++++++++++++++++++++
5 files changed, 184 insertions(+)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
index 9983c1062da..5ebb4ad0ab6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
@@ -57,6 +57,9 @@ public class CTEInline extends
DefaultPlanRewriter<LogicalCTEProducer<?>> implem
@Override
public Plan rewriteRoot(Plan plan, JobContext jobContext) {
mustInlineCTEs =
jobContext.getCascadesContext().getStatementContext().getMustInlineCTEs();
+ if (!mustInlineCTEs.isEmpty()) {
+ collectRecursiveCteDependencies(plan);
+ }
Plan root = plan.accept(this, null);
// collect cte id to consumer
@@ -68,6 +71,26 @@ public class CTEInline extends
DefaultPlanRewriter<LogicalCTEProducer<?>> implem
return root;
}
+ private void collectRecursiveCteDependencies(Plan plan) {
+ // Resolve the transitive dependencies before making any
materialization decisions.
+ // Otherwise an outer producer can remain shared by independent
recursive controllers
+ // even when its consumers are inside CTEs that must be inlined.
+ List<LogicalCTEProducer<?>> producers = plan.collectToList(p -> p
instanceof LogicalCTEProducer);
+ boolean changed;
+ do {
+ changed = false;
+ for (LogicalCTEProducer<?> producer : producers) {
+ if (mustInlineCTEs.contains(producer.getCteId())) {
+ List<LogicalCTEConsumer> consumers = producer.child()
+ .collectToList(p -> p instanceof
LogicalCTEConsumer);
+ for (LogicalCTEConsumer consumer : consumers) {
+ changed |= mustInlineCTEs.add(consumer.getCteId());
+ }
+ }
+ }
+ } while (changed);
+ }
+
@Override
public Plan visitLogicalCTEAnchor(LogicalCTEAnchor<? extends Plan, ?
extends Plan> cteAnchor,
LogicalCTEProducer<?> producer) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
index 5b3d8b24f6d..01401934586 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
@@ -820,6 +820,7 @@ public class ThriftPlansBuilder {
List<TRecCTETarget> targets = new ArrayList<>();
// reset infos for all instances of child fragments (used to
reset state)
List<TRecCTEResetInfo> fragmentsToReset = new ArrayList<>();
+ Set<String> resetFragmentKeys = new HashSet<>();
// The recursive side is under the right child; collect all
fragments
List<PlanFragment> childFragments = new ArrayList<>();
recursiveCteNode.getChild(1).getChild(0).getFragment().collectAll(PlanFragment.class::isInstance,
@@ -843,6 +844,11 @@ public class ThriftPlansBuilder {
String.format("can't find TNetworkAddress for
fragment %d", childFragmentId));
}
for (TNetworkAddress address : tNetworkAddresses) {
+ String resetFragmentKey = childFragmentId.asInt() + "@"
+ + address.getHostname() + ":" +
address.getPort();
+ if (!resetFragmentKeys.add(resetFragmentKey)) {
+ continue;
+ }
TRecCTEResetInfo tRecCTEResetInfo = new
TRecCTEResetInfo();
tRecCTEResetInfo.setFragmentId(childFragmentId.asInt());
tRecCTEResetInfo.setAddr(address);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
index 1acebe8e3f8..232da8467d3 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
@@ -34,6 +34,7 @@ import org.apache.doris.utframe.TestWithFeService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -113,6 +114,44 @@ public class CTEInlineTest extends TestWithFeService
implements MemoPatternMatch
);
}
+ @Test
+ public void inlineTransitiveRecursiveDependencies() {
+ boolean oldEnableCteMaterialize =
connectContext.getSessionVariable().enableCTEMaterialize;
+ int oldCteInlineMode =
connectContext.getSessionVariable().cteInlineMode;
+ int oldInlineCteReferencedThreshold =
connectContext.getSessionVariable().inlineCTEReferencedThreshold;
+ connectContext.getSessionVariable().enableCTEMaterialize = true;
+ connectContext.getSessionVariable().cteInlineMode = 0;
+ connectContext.getSessionVariable().inlineCTEReferencedThreshold = 1;
+ try {
+ for (String input : new String[] {"base", "middle"}) {
+ String sql = "with recursive "
+ + "base as (select 1 as src, 2 as dst union all select
2, 3), "
+ + "middle as (select * from base union all select *
from base), "
+ + "edges as (select * from " + input + " union all
select * from " + input + "), "
+ + "ordinary as (select id from cte_inline_tbl), "
+ + "r1(n) as (select 1 union all "
+ + "select e.dst from r1 r join edges e on r.n =
e.src), "
+ + "r2(n) as (select 2 union all "
+ + "select e.dst from r2 r join edges e on r.n = e.src)
"
+ + "select r1.n, r2.n from r1 join r2 on r1.n = r2.n "
+ + "join ordinary a on a.id = r1.n join ordinary b on
b.id = r2.n";
+ LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+ NereidsPlanner planner = new NereidsPlanner(new
StatementContext(connectContext,
+ new OriginStatement(sql, 0)));
+ planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+ ExplainCommand.ExplainLevel.REWRITTEN_PLAN);
+ List<LogicalCTEConsumer> consumers = planner.getRewrittenPlan()
+ .collectToList(p -> p instanceof LogicalCTEConsumer);
+ Assertions.assertEquals(2, consumers.size());
+ Assertions.assertTrue(consumers.stream().allMatch(c ->
c.getName().equals("ordinary")));
+ }
+ } finally {
+ connectContext.getSessionVariable().enableCTEMaterialize =
oldEnableCteMaterialize;
+ connectContext.getSessionVariable().cteInlineMode =
oldCteInlineMode;
+ connectContext.getSessionVariable().inlineCTEReferencedThreshold =
oldInlineCteReferencedThreshold;
+ }
+ }
+
@Test
public void refreshCteConsumersAfterNormalizeEliminatesEmptyBranch() {
int oldCteInlineMode =
connectContext.getSessionVariable().cteInlineMode;
diff --git a/regression-test/data/recursive_cte/shared_cte_reset_test.out
b/regression-test/data/recursive_cte/shared_cte_reset_test.out
new file mode 100644
index 00000000000..b62c44c996c
--- /dev/null
+++ b/regression-test/data/recursive_cte/shared_cte_reset_test.out
@@ -0,0 +1,8 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !shared_cte_reset --
+1 MP:a
+2 MP:a
+3 MP:b
+a MP:a
+b MP:b
+
diff --git a/regression-test/suites/recursive_cte/shared_cte_reset_test.groovy
b/regression-test/suites/recursive_cte/shared_cte_reset_test.groovy
new file mode 100644
index 00000000000..05980941140
--- /dev/null
+++ b/regression-test/suites/recursive_cte/shared_cte_reset_test.groovy
@@ -0,0 +1,108 @@
+// 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("shared_cte_reset_test", "rec_cte") {
+ qt_shared_cte_reset """
+ WITH RECURSIVE
+ org_mp_bridge AS (
+ SELECT '1' AS organization_id, 'a' AS marketplace_account_id
+ UNION ALL SELECT '2', 'a'
+ UNION ALL SELECT '3', 'b'
+ ),
+ edges AS (
+ SELECT CONCAT('ORG:', organization_id) AS src,
+ CONCAT('MP:', marketplace_account_id) AS dst
+ FROM org_mp_bridge
+ UNION
+ SELECT CONCAT('MP:', marketplace_account_id),
+ CONCAT('ORG:', organization_id)
+ FROM org_mp_bridge
+ ),
+ nodes AS (
+ SELECT src AS node FROM edges
+ UNION
+ SELECT dst FROM edges
+ ),
+ reach (start_node, node) AS (
+ SELECT node, node FROM nodes
+ UNION
+ SELECT r.start_node, e.dst
+ FROM reach r
+ JOIN edges e ON r.node = e.src
+ ),
+ node_group AS (
+ SELECT node, MIN(start_node) AS reconciliation_group_id
+ FROM reach
+ GROUP BY node
+ ),
+ org_group AS (
+ SELECT DISTINCT b.organization_id, g.reconciliation_group_id
+ FROM org_mp_bridge b
+ JOIN node_group g ON g.node = CONCAT('ORG:', b.organization_id)
+ ),
+ mp_group AS (
+ SELECT DISTINCT b.marketplace_account_id, g.reconciliation_group_id
+ FROM org_mp_bridge b
+ JOIN node_group g ON g.node = CONCAT('MP:',
b.marketplace_account_id)
+ )
+ SELECT organization_id, reconciliation_group_id FROM org_group
+ UNION ALL
+ SELECT marketplace_account_id, reconciliation_group_id FROM mp_group
+ ORDER BY organization_id, reconciliation_group_id
+ """
+
+ // Each recursive controller must own its transitive ordinary CTE
dependencies.
+ // Keep materialization enabled so this also checks that the planner
enforces inlining.
+ sql "set enable_cte_materialize = true"
+ sql "set inline_cte_referenced_threshold = 1"
+ for (def input : ["base", "middle"]) {
+ def query = """
+ WITH RECURSIVE
+ base AS (
+ SELECT 1 AS src, 2 AS dst
+ UNION ALL SELECT 2, 3
+ UNION ALL SELECT 3, 4
+ ),
+ middle AS (
+ SELECT * FROM base
+ UNION ALL SELECT * FROM base
+ ),
+ edges AS (
+ SELECT * FROM ${input}
+ UNION ALL SELECT * FROM ${input}
+ ),
+ r1(n) AS (
+ SELECT 1
+ UNION ALL
+ SELECT e.dst FROM r1 r JOIN edges e ON r.n = e.src
+ ),
+ r2(n) AS (
+ SELECT 2
+ UNION ALL
+ SELECT e.dst FROM r2 r JOIN edges e ON r.n = e.src
+ )
+ SELECT r1.n, r2.n FROM r1 JOIN r2 ON r1.n = r2.n
+ ORDER BY r1.n, r2.n
+ """
+ explain {
+ sql query
+ notContains "MultiCastDataSinks"
+ }
+ // Exercise multiple rounds and final close, which previously raised
NOT_FOUND.
+ sql query
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]