This is an automated email from the ASF dual-hosted git repository.
vldpyatkov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new c1bfd5eef01 IGNITE-29038 Fix recursive CTE subqueries and multiple
consumers (#13559)
c1bfd5eef01 is described below
commit c1bfd5eef015238126fd6ae052cd75710628cf09
Author: Vladislav Pyatkov <[email protected]>
AuthorDate: Tue Sep 8 21:19:07 2026 +0300
IGNITE-29038 Fix recursive CTE subqueries and multiple consumers (#13559)
---
.../query/calcite/exec/ExecutionContext.java | 10 --
.../query/calcite/exec/LogicalRelImplementor.java | 8 +-
.../calcite/exec/rel/RecursiveTableScanNode.java | 49 +++++++
.../query/calcite/exec/rel/RepeatUnionNode.java | 36 +++++-
.../query/calcite/prepare/IgniteSqlValidator.java | 8 ++
.../query/calcite/prepare/PlanningContext.java | 22 ----
.../calcite/prepare/RecursiveCteValidator.java | 141 +++++++++++++++++++++
.../calcite/rel/IgniteRecursiveTableScan.java | 22 +---
.../query/calcite/rel/IgniteRepeatUnion.java | 27 +---
.../query/calcite/rule/RecursiveCteUtils.java | 11 --
.../rule/RecursiveTableScanConverterRule.java | 3 +-
.../calcite/rule/RepeatUnionConverterRule.java | 2 -
.../integration/RecursiveCteIntegrationTest.java | 104 +++++++++++++++
13 files changed, 347 insertions(+), 96 deletions(-)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
index 1d129bd3bb0..41a477ef083 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -49,7 +48,6 @@ import
org.apache.ignite.internal.processors.cache.transactions.TransactionChang
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.ExpressionFactory;
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.ExpressionFactoryImpl;
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.ReflectiveCallNotNullImplementor;
-import
org.apache.ignite.internal.processors.query.calcite.exec.rel.RecursiveCteState;
import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.ExecutionNodeMemoryTracker;
import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.IoTracker;
import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.MemoryTracker;
@@ -141,9 +139,6 @@ public class ExecutionContext<Row> extends
AbstractQueryContext implements DataC
/** Map associates UDF name to instance of class that contains this UDF. */
private final Map<String, Object> udfInstances = new ConcurrentHashMap<>();
- /** Query-local recursive CTE states, keyed by transient table identifier.
*/
- private final Map<String, RecursiveCteState<Row>> recursiveCteStates = new
HashMap<>();
-
/** Session context provider injected into UDF targets. */
private final SessionContextProvider sesCtxProv = new
SessionContextProviderImpl();
@@ -477,11 +472,6 @@ public class ExecutionContext<Row> extends
AbstractQueryContext implements DataC
return ExecutionNodeMemoryTracker.create(qryMemoryTracker,
rowOverhead);
}
- /** Returns the state shared by the repeat union and scans of one
recursive CTE. */
- RecursiveCteState<Row> recursiveCteState(String stateId) {
- return recursiveCteStates.computeIfAbsent(stateId, key -> new
RecursiveCteState<>(this));
- }
-
/** */
public IoTracker ioTracker() {
return ioTracker;
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index 569fe0b38fd..e2777240471 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -74,7 +74,7 @@ import
org.apache.ignite.internal.processors.query.calcite.exec.rel.NestedLoopJo
import org.apache.ignite.internal.processors.query.calcite.exec.rel.Node;
import org.apache.ignite.internal.processors.query.calcite.exec.rel.Outbox;
import
org.apache.ignite.internal.processors.query.calcite.exec.rel.ProjectNode;
-import
org.apache.ignite.internal.processors.query.calcite.exec.rel.RecursiveCteState;
+import
org.apache.ignite.internal.processors.query.calcite.exec.rel.RecursiveTableScanNode;
import
org.apache.ignite.internal.processors.query.calcite.exec.rel.RepeatUnionNode;
import org.apache.ignite.internal.processors.query.calcite.exec.rel.ScanNode;
import
org.apache.ignite.internal.processors.query.calcite.exec.rel.ScanStorageNode;
@@ -621,7 +621,7 @@ public class LogicalRelImplementor<Row> implements
IgniteRelVisitor<Node<Row>> {
/** {@inheritDoc} */
@Override public Node<Row> visit(IgniteRecursiveTableScan rel) {
- return new ScanNode<>(ctx, rel.getRowType(),
ctx.recursiveCteState(rel.stateId()).current());
+ return new RecursiveTableScanNode<>(ctx, rel.getRowType());
}
/** {@inheritDoc} */
@@ -646,10 +646,8 @@ public class LogicalRelImplementor<Row> implements
IgniteRelVisitor<Node<Row>> {
/** {@inheritDoc} */
@Override public Node<Row> visit(IgniteRepeatUnion rel) {
- RecursiveCteState<Row> state = ctx.recursiveCteState(rel.stateId());
- RepeatUnionNode<Row> node = new RepeatUnionNode<>(ctx,
rel.getRowType(), state, rel.iterationLimit());
+ RepeatUnionNode<Row> node = new RepeatUnionNode<>(ctx,
rel.getRowType(), rel.iterationLimit());
- state.clear();
node.register(F.asList(visit(rel.getLeft()), visit(rel.getRight())));
return node;
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveTableScanNode.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveTableScanNode.java
new file mode 100644
index 00000000000..0bbc221d9de
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveTableScanNode.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.exec.rel;
+
+import java.util.Collections;
+import java.util.Iterator;
+import org.apache.calcite.rel.type.RelDataType;
+import
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+
+import static java.util.Objects.requireNonNull;
+
+/** Scan of the current delta owned by the enclosing recursive union. */
+public class RecursiveTableScanNode<Row> extends ScanNode<Row> {
+ /** Owning recursive union, bound when its sources are registered. */
+ private RepeatUnionNode<Row> repeatUnion;
+
+ /** */
+ public RecursiveTableScanNode(ExecutionContext<Row> ctx, RelDataType
rowType) {
+ super(ctx, rowType, Collections.emptyList());
+ }
+
+ /** Binds this scan to the recursive union that owns its current delta. */
+ void bind(RepeatUnionNode<Row> repeatUnion) {
+ assert this.repeatUnion == null;
+
+ this.repeatUnion = requireNonNull(repeatUnion);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Iterator<Row> sourceIterator() {
+ return requireNonNull(repeatUnion, "Recursive table scan is not bound
to a repeat union")
+ .current().iterator();
+ }
+}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
index 33622e6ce57..9586b91a362 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.processors.query.calcite.exec.rel;
+import java.util.List;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
@@ -55,15 +56,23 @@ public class RepeatUnionNode<Row> extends AbstractNode<Row>
implements Downstrea
public RepeatUnionNode(
ExecutionContext<Row> ctx,
RelDataType rowType,
- RecursiveCteState<Row> state,
int iterationLimit
) {
super(ctx, rowType);
- this.state = state;
+ state = new RecursiveCteState<>(ctx);
this.iterationLimit = iterationLimit;
}
+ /** {@inheritDoc} */
+ @Override public void register(List<Node<Row>> sources) {
+ assert sources.size() == 2;
+
+ bindRecursiveScans(sources.get(RECURSIVE_SOURCE));
+
+ super.register(sources);
+ }
+
/** {@inheritDoc} */
@Override public void request(int rowsCnt) throws Exception {
assert !F.isEmpty(sources()) && sources().size() == 2;
@@ -134,6 +143,11 @@ public class RepeatUnionNode<Row> extends
AbstractNode<Row> implements Downstrea
return this;
}
+ /** Current delta visible to recursive scans owned by this union. */
+ Iterable<Row> current() {
+ return state.current();
+ }
+
/** {@inheritDoc} */
@Override protected void rewindInternal() {
curSrc = SEED_SOURCE;
@@ -155,6 +169,24 @@ public class RepeatUnionNode<Row> extends
AbstractNode<Row> implements Downstrea
return sources().get(curSrc);
}
+ /** Binds recursive scans in this union's recursive term without crossing
nested recursive unions. */
+ private void bindRecursiveScans(Node<Row> node) {
+ if (node instanceof RecursiveTableScanNode) {
+ ((RecursiveTableScanNode<Row>)node).bind(this);
+
+ return;
+ }
+
+ // Nested recursive unions bind their own scans when registering their
sources.
+ if (node instanceof RepeatUnionNode)
+ return;
+
+ if (!F.isEmpty(node.sources())) {
+ for (Node<Row> src : node.sources())
+ bindRecursiveScans(src);
+ }
+ }
+
/** Starts collecting and requests rows from the active input. */
private void requestSource() throws Exception {
if (!writing) {
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
index d7af46290f4..5f31f4d64a5 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
@@ -52,6 +52,7 @@ import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlUpdate;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.SqlWindow;
+import org.apache.calcite.sql.SqlWithItem;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.sql.fun.SqlCase;
import org.apache.calcite.sql.parser.SqlParserPos;
@@ -155,6 +156,13 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
super.validateInsert(insert);
}
+ /** {@inheritDoc} */
+ @Override public void validateWithItem(SqlWithItem withItem) {
+ super.validateWithItem(withItem);
+
+ RecursiveCteValidator.validate(this, withItem);
+ }
+
/**
* Validates insert target columns to ensure they do not contain the
version column.
*
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java
index ffb42003e83..31558c6af82 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java
@@ -17,18 +17,14 @@
package org.apache.ignite.internal.processors.query.calcite.prepare;
-import java.util.IdentityHashMap;
-import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import org.apache.calcite.plan.Context;
import org.apache.calcite.plan.Contexts;
import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.prepare.CalciteCatalogReader;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.schema.SchemaPlus;
-import org.apache.calcite.schema.TransientTable;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.RuleSet;
@@ -62,12 +58,6 @@ public final class PlanningContext implements Context {
/** */
private IgnitePlanner planner;
- /** Query-local identifiers of recursive transient tables. */
- private final Map<TransientTable, String> recursiveCteStateIds = new
IdentityHashMap<>();
-
- /** Next query-local recursive CTE identifier. */
- private int nextRecursiveCteStateId;
-
/** */
private final long startTs;
@@ -189,18 +179,6 @@ public final class PlanningContext implements Context {
return planner().cluster();
}
- /** Returns an identifier unique for the given recursive CTE within this
planning context. */
- public synchronized String recursiveCteStateId(RelOptTable table) {
- TransientTable transientTable = table.unwrap(TransientTable.class);
-
- assert transientTable != null;
-
- return recursiveCteStateIds.computeIfAbsent(
- transientTable,
- key -> String.join(".", table.getQualifiedName()) + '#' +
nextRecursiveCteStateId++
- );
- }
-
/** {@inheritDoc} */
@Override public <C> @Nullable C unwrap(Class<C> aCls) {
if (aCls == getClass())
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/RecursiveCteValidator.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/RecursiveCteValidator.java
new file mode 100644
index 00000000000..d809e051d51
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/RecursiveCteValidator.java
@@ -0,0 +1,141 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.prepare;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.SqlWithItem;
+import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.calcite.sql.validate.SqlValidatorNamespace;
+import org.apache.calcite.sql.validate.SqlWithItemTableRef;
+import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+
+/** Validates restrictions specific to recursive common table expressions. */
+final class RecursiveCteValidator {
+ /** */
+ private RecursiveCteValidator() {
+ // No-op.
+ }
+
+ /** Validates that a recursive CTE does not reference itself from a nested
query. */
+ static void validate(IgniteSqlValidator validator, SqlWithItem withItem) {
+ if (withItem.recursive == null || !withItem.recursive.booleanValue())
+ return;
+
+ Set<SqlNode> topLevelQueries = Collections.newSetFromMap(new
IdentityHashMap<>());
+
+ collectTopLevelQueries(withItem.query, topLevelQueries);
+
+ RecursiveCteReferenceFinder finder =
+ new RecursiveCteReferenceFinder(validator, withItem,
topLevelQueries);
+
+ withItem.query.accept(finder);
+
+ if (finder.nestedSelfReferenceFound) {
+ throw new IgniteSQLException(
+ "Unsupported recursive CTE: self-references inside subqueries
are not supported",
+ IgniteQueryErrorCode.UNSUPPORTED_OPERATION
+ );
+ }
+ }
+
+ /** Collects query nodes that constitute the CTE's top-level query
expression. */
+ private static void collectTopLevelQueries(SqlNode node, Set<SqlNode>
queries) {
+ if (node == null || !node.isA(SqlKind.QUERY))
+ return;
+
+ queries.add(node);
+
+ if (node.isA(SqlKind.SET_QUERY)) {
+ for (SqlNode operand : ((SqlCall)node).getOperandList())
+ collectTopLevelQueries(operand, queries);
+ }
+ else if (node instanceof SqlWith)
+ collectTopLevelQueries(((SqlWith)node).body, queries);
+ else if (node.getKind() == SqlKind.ORDER_BY)
+ collectTopLevelQueries(((SqlCall)node).operand(0), queries);
+ }
+
+ /** Finds references to the recursive CTE located inside nested queries. */
+ private static class RecursiveCteReferenceFinder extends
SqlBasicVisitor<Void> {
+ /** SQL validator. */
+ private final IgniteSqlValidator validator;
+
+ /** Recursive CTE being validated. */
+ private final SqlWithItem withItem;
+
+ /** Nodes that belong to the CTE's top-level query expression. */
+ private final Set<SqlNode> topLevelQueries;
+
+ /** Whether the visitor is inside a nested query. */
+ private boolean insideSubquery;
+
+ /** Whether an unsupported reference was found. */
+ private boolean nestedSelfReferenceFound;
+
+ /** */
+ private RecursiveCteReferenceFinder(
+ IgniteSqlValidator validator,
+ SqlWithItem withItem,
+ Set<SqlNode> topLevelQueries
+ ) {
+ this.validator = validator;
+ this.withItem = withItem;
+ this.topLevelQueries = topLevelQueries;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Void visit(SqlCall call) {
+ if (nestedSelfReferenceFound)
+ return null;
+
+ boolean wasInsideSubquery = insideSubquery;
+
+ insideSubquery |= call.isA(SqlKind.QUERY) &&
!topLevelQueries.contains(call);
+
+ super.visit(call);
+
+ insideSubquery = wasInsideSubquery;
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Void visit(SqlIdentifier id) {
+ if (!insideSubquery)
+ return null;
+
+ SqlValidatorNamespace namespace = validator.getNamespace(id);
+ SqlNode resolvedNode = namespace == null ? null :
namespace.resolve().getNode();
+
+ if (resolvedNode instanceof SqlWithItemTableRef
+ && ((SqlWithItemTableRef)resolvedNode).getWithItem() ==
withItem) {
+ nestedSelfReferenceFound = true;
+ }
+
+ return null;
+ }
+ }
+}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRecursiveTableScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRecursiveTableScan.java
index d51e1bd53d1..fa12efb5ce1 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRecursiveTableScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRecursiveTableScan.java
@@ -26,24 +26,17 @@ import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.type.RelDataType;
-import static java.util.Objects.requireNonNull;
-
/** Scan of the current delta of a query-local recursive CTE. */
public class IgniteRecursiveTableScan extends AbstractRelNode implements
IgniteRel {
- /** Query-local recursive state identifier. */
- private final String stateId;
-
/** */
public IgniteRecursiveTableScan(
RelOptCluster cluster,
RelTraitSet traits,
- RelDataType rowType,
- String stateId
+ RelDataType rowType
) {
super(cluster, traits);
this.rowType = rowType;
- this.stateId = stateId;
}
/** Constructor used for deserialization. */
@@ -51,21 +44,15 @@ public class IgniteRecursiveTableScan extends
AbstractRelNode implements IgniteR
this(
input.getCluster(),
input.getTraitSet().replace(IgniteConvention.INSTANCE),
- input.getRowType("rowType"),
- requireNonNull(input.getString("stateId"), "stateId")
+ input.getRowType("rowType")
);
}
- /** Query-local recursive state identifier. */
- public String stateId() {
- return stateId;
- }
-
/** {@inheritDoc} */
@Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
assert inputs.isEmpty();
- return new IgniteRecursiveTableScan(getCluster(), traitSet, rowType,
stateId);
+ return new IgniteRecursiveTableScan(getCluster(), traitSet, rowType);
}
/** {@inheritDoc} */
@@ -77,13 +64,12 @@ public class IgniteRecursiveTableScan extends
AbstractRelNode implements IgniteR
@Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
assert inputs.isEmpty();
- return new IgniteRecursiveTableScan(cluster, getTraitSet(), rowType,
stateId);
+ return new IgniteRecursiveTableScan(cluster, getTraitSet(), rowType);
}
/** {@inheritDoc} */
@Override public RelWriter explainTerms(RelWriter pw) {
return super.explainTerms(pw)
- .item("stateId", stateId)
.item("rowType", rowType);
}
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
index 0a7db70d139..a5689f2dbe6 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
@@ -22,28 +22,19 @@ import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.core.RepeatUnion;
-import static java.util.Objects.requireNonNull;
-
/** Coordinator-side iterative UNION ALL for a recursive CTE. */
public class IgniteRepeatUnion extends RepeatUnion implements IgniteRel {
- /** Query-local recursive state identifier. */
- private final String stateId;
-
/** */
public IgniteRepeatUnion(
RelOptCluster cluster,
RelTraitSet traits,
RelNode seed,
RelNode iterative,
- String stateId,
int iterationLimit
) {
super(cluster, traits, seed, iterative, true, iterationLimit, null);
-
- this.stateId = stateId;
}
/** Constructor used for deserialization. */
@@ -53,16 +44,10 @@ public class IgniteRepeatUnion extends RepeatUnion
implements IgniteRel {
input.getTraitSet().replace(IgniteConvention.INSTANCE),
input.getInputs().get(0),
input.getInputs().get(1),
- requireNonNull(input.getString("stateId"), "stateId"),
iterationLimit(input)
);
}
- /** Query-local recursive state identifier. */
- public String stateId() {
- return stateId;
- }
-
/** Maximum number of recursive iterations, or a negative value for no
limit. */
public int iterationLimit() {
return iterationLimit;
@@ -72,7 +57,7 @@ public class IgniteRepeatUnion extends RepeatUnion implements
IgniteRel {
@Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
assert inputs.size() == 2;
- return new IgniteRepeatUnion(getCluster(), traitSet, inputs.get(0),
inputs.get(1), stateId, iterationLimit);
+ return new IgniteRepeatUnion(getCluster(), traitSet, inputs.get(0),
inputs.get(1), iterationLimit);
}
/** {@inheritDoc} */
@@ -84,16 +69,10 @@ public class IgniteRepeatUnion extends RepeatUnion
implements IgniteRel {
@Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
assert inputs.size() == 2;
- return new IgniteRepeatUnion(cluster, getTraitSet(), inputs.get(0),
inputs.get(1), stateId, iterationLimit);
- }
-
- /** {@inheritDoc} */
- @Override public RelWriter explainTerms(RelWriter pw) {
- return super.explainTerms(pw)
- .item("stateId", stateId);
+ return new IgniteRepeatUnion(cluster, getTraitSet(), inputs.get(0),
inputs.get(1), iterationLimit);
}
- /** Reads the optional iteration limit emitted by {@link
RepeatUnion#explainTerms(RelWriter)}. */
+ /** Reads the optional iteration limit from a serialized plan. */
private static int iterationLimit(RelInput input) {
Number iterationLimit = (Number)input.get("iterationLimit");
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveCteUtils.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveCteUtils.java
index 395d85b38d1..ee7b04da2f4 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveCteUtils.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveCteUtils.java
@@ -19,7 +19,6 @@ package
org.apache.ignite.internal.processors.query.calcite.rule;
import java.util.ArrayList;
import java.util.List;
-import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
@@ -36,7 +35,6 @@ import org.apache.calcite.rex.RexSubQuery;
import org.apache.calcite.schema.TransientTable;
import org.apache.calcite.sql.validate.SqlUserDefinedFunction;
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteScalarFunction;
-import
org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
import
org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait;
import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
@@ -55,15 +53,6 @@ final class RecursiveCteUtils {
return table != null && table.unwrap(TransientTable.class) != null;
}
- /** Stable identifier preserved in the serialized physical plan. */
- static String stateId(RelOptPlanner planner, RelOptTable table) {
- PlanningContext ctx =
planner.getContext().unwrap(PlanningContext.class);
-
- assert ctx != null;
-
- return ctx.recursiveCteStateId(table);
- }
-
/** Counts scans of the recursive transient table. */
static int referenceCount(RelNode rel, RelOptTable table) {
rel = original(rel);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveTableScanConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveTableScanConverterRule.java
index 63c0e01401f..003efe72349 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveTableScanConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveTableScanConverterRule.java
@@ -56,8 +56,7 @@ public class RecursiveTableScanConverterRule extends
AbstractIgniteConverterRule
return new IgniteRecursiveTableScan(
rel.getCluster(),
traits,
- rel.getRowType(),
- RecursiveCteUtils.stateId(planner, rel.getTable())
+ rel.getRowType()
);
}
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
index d53542ea5ac..ef37d92cbb0 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
@@ -57,7 +57,6 @@ public class RepeatUnionConverterRule extends
AbstractIgniteConverterRule<Logica
if (!rel.all)
throw unsupported("only UNION ALL is supported");
- String stateId = RecursiveCteUtils.stateId(planner, table);
int iterationLimit =
planner.getContext().unwrap(PlanningContext.class).recursiveCteIterationLimit();
RelNode seed = unwrapSpool(rel.getSeedRel(), "seed");
@@ -72,7 +71,6 @@ public class RepeatUnionConverterRule extends
AbstractIgniteConverterRule<Logica
traits,
convert(seed, traits),
convert(iterative, traits),
- stateId,
iterationLimit
);
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
index 11a333e30fa..be0a8aa3039 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
@@ -115,6 +115,88 @@ public class RecursiveCteIntegrationTest extends
AbstractBasicIntegrationTest {
.check();
}
+ /** */
+ @Test
+ public void testRecursiveCteWithMultipleJoinConsumers() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT l.n, r.n " +
+ "FROM numbers l " +
+ "JOIN numbers r ON l.n = r.n")
+ .returns(1, 1)
+ .returns(2, 2)
+ .returns(3, 3)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testRecursiveCteWithMultipleUnionConsumers() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT n FROM numbers " +
+ "UNION ALL " +
+ "SELECT n FROM numbers")
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testSelfReferenceInScalarSubqueryIsRejected() {
+ assertThrows(
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT (SELECT n + 1 FROM numbers) FROM (VALUES (0))" +
+ ") " +
+ "SELECT n FROM numbers FETCH FIRST 3 ROWS ONLY",
+ IgniteSQLException.class,
+ "self-references inside subqueries are not supported"
+ );
+ }
+
+ /** */
+ @Test
+ public void testSelfReferenceInDerivedTableIsRejected() {
+ assertThrows(
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM (SELECT n FROM numbers) WHERE n < 3" +
+ ") " +
+ "SELECT n FROM numbers",
+ IgniteSQLException.class,
+ "self-references inside subqueries are not supported"
+ );
+ }
+
+ /** */
+ @Test
+ public void testRecursiveCteCanBeReadFromScalarSubquery() {
+ assertQuery(
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT (SELECT MAX(n) FROM numbers)"
+ )
+ .returns(3)
+ .check();
+ }
+
/** */
@Test
public void testRecursiveCteWithMultipleRecursiveBranches() {
@@ -178,6 +260,28 @@ public class RecursiveCteIntegrationTest extends
AbstractBasicIntegrationTest {
.check();
}
+ /** */
+ @Test
+ public void testNestedRecursiveCteStatesAreIsolated() {
+ assertQuery("WITH RECURSIVE first_numbers(n) AS (" +
+ "SELECT 10 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM first_numbers WHERE n < 12" +
+ "), second_numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT second_numbers.n + 1 " +
+ "FROM second_numbers " +
+ "JOIN first_numbers ON second_numbers.n + 9 = first_numbers.n
" +
+ "WHERE second_numbers.n < 3" +
+ ") " +
+ "SELECT n FROM second_numbers")
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .check();
+ }
+
/** */
@Test
public void
testIndependentNonDeterministicSubtreeIsEvaluatedForEveryIteration() {