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 6f2a343d282 IGNITE-27822 Support recursive query for Calcite engine
(#13479)
6f2a343d282 is described below
commit 6f2a343d282441dc7360960a6542d4459a297358
Author: Vladislav Pyatkov <[email protected]>
AuthorDate: Fri Sep 4 12:50:53 2026 +0300
IGNITE-27822 Support recursive query for Calcite engine (#13479)
---
.../calcite/DistributedCalciteConfiguration.java | 33 +++
.../query/calcite/exec/ExecutionContext.java | 10 +
.../query/calcite/exec/LogicalRelImplementor.java | 20 ++
.../query/calcite/exec/rel/RecursiveCteState.java | 94 +++++++++
.../query/calcite/exec/rel/RepeatUnionNode.java | 183 ++++++++++++++++
.../calcite/metadata/IgniteMdFragmentMapping.java | 12 ++
.../processors/query/calcite/prepare/Cloner.java | 12 ++
.../query/calcite/prepare/IgniteRelShuttle.java | 12 ++
.../query/calcite/prepare/PlannerPhase.java | 4 +
.../query/calcite/prepare/PlanningContext.java | 41 ++++
.../query/calcite/prepare/PrepareServiceImpl.java | 2 +
.../query/calcite/rel/IgniteHashIndexSpool.java | 19 ++
.../query/calcite/rel/IgniteIndexScan.java | 16 ++
.../calcite/rel/IgniteRecursiveTableScan.java | 89 ++++++++
.../query/calcite/rel/IgniteRelVisitor.java | 10 +
.../query/calcite/rel/IgniteRepeatUnion.java | 102 +++++++++
.../query/calcite/rel/IgniteSortedIndexSpool.java | 34 +++
.../query/calcite/rel/IgniteTableScan.java | 11 +
.../rel/ProjectableFilterableTableScan.java | 30 +++
.../rel/logical/IgniteLogicalIndexScan.java | 10 +
.../rel/logical/IgniteLogicalTableScan.java | 11 +
.../query/calcite/rule/RecursiveCteUtils.java | 218 ++++++++++++++++++++
.../rule/RecursiveTableScanConverterRule.java | 63 ++++++
.../calcite/rule/RepeatUnionConverterRule.java | 102 +++++++++
.../CalciteQueryProcessorPropertiesTest.java | 52 +++++
.../integration/MemoryQuotasIntegrationTest.java | 18 ++
.../integration/RecursiveCteIntegrationTest.java | 208 +++++++++++++++++--
.../calcite/planner/RecursiveCtePlannerTest.java | 229 +++++++++++++++++++++
.../apache/ignite/testsuites/PlannerTestSuite.java | 2 +
.../sql/hierarchy/test_recursive_hierarchy.test | 64 ++++++
.../sql/hierarchy/test_recursive_sequence.test | 62 ++++++
.../sql/hierarchy/test_recursive_subquery.test | 54 +++++
32 files changed, 1809 insertions(+), 18 deletions(-)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/DistributedCalciteConfiguration.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/DistributedCalciteConfiguration.java
index 26c3a7b14c9..3aee850cd26 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/DistributedCalciteConfiguration.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/DistributedCalciteConfiguration.java
@@ -42,18 +42,28 @@ public class DistributedCalciteConfiguration extends
DistributedSqlConfiguration
/** Plan cache size property name. */
public static final String PLAN_CACHE_SIZE_PROPERTY_NAME =
"sql.calcite.planCacheSize";
+ /** Recursive CTE iteration limit property name. */
+ public static final String RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME =
+ "sql.calcite.recursiveCteIterationLimit";
+
/** Default value of the disabled rules. */
public static final String[] DFLT_DISABLED_RULES = new String[0];
/** Default value of plan cache size. */
public static final int DFLT_PLAN_CACHE_SIZE = 1024;
+ /** Default recursive CTE iteration limit. */
+ public static final int DFLT_RECURSIVE_CTE_ITERATION_LIMIT = 100;
+
/** Globally disabled rules. */
private volatile DistributedChangeableProperty<String[]> disabledRules;
/** Plan cache size. */
private volatile DistributedChangeableProperty<Integer> planCacheSize;
+ /** Recursive CTE iteration limit. */
+ private volatile DistributedChangeableProperty<Integer>
recursiveCteIterationLimit;
+
/** */
private QueryPlanCache qryPlanCache;
@@ -91,6 +101,13 @@ public class DistributedCalciteConfiguration extends
DistributedSqlConfiguration
return getProperty(planCacheSize, DFLT_PLAN_CACHE_SIZE);
}
+ /**
+ * @return Maximum number of recursive CTE iterations, or a negative value
for no limit.
+ */
+ public int recursiveCteIterationLimit() {
+ return getProperty(recursiveCteIterationLimit,
DFLT_RECURSIVE_CTE_ITERATION_LIMIT);
+ }
+
/** */
private <T extends Serializable> T
getProperty(DistributedChangeableProperty<T> prop, T dflt) {
T res = prop == null ? dflt : prop.get();
@@ -140,6 +157,21 @@ public class DistributedCalciteConfiguration extends
DistributedSqlConfiguration
);
planCacheSize.addListener(planCacheCleaner);
+
+ registerProperty(
+ dispatcher,
+ RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME,
+ prop -> recursiveCteIterationLimit = prop,
+ () -> new SimpleDistributedProperty<>(
+ RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME,
+ Integer::parseInt,
+ "Maximum number of recursive CTE iterations before query
execution fails. " +
+ "A negative value disables the limit. NOTE: cleans the
planning cache on change."
+ ),
+ log
+ );
+
+ recursiveCteIterationLimit.addListener(planCacheCleaner);
}
/** {@inheritDoc} */
@@ -148,5 +180,6 @@ public class DistributedCalciteConfiguration extends
DistributedSqlConfiguration
setDefaultValue(disabledRules, DFLT_DISABLED_RULES, log);
setDefaultValue(planCacheSize, DFLT_PLAN_CACHE_SIZE, log);
+ setDefaultValue(recursiveCteIterationLimit,
DFLT_RECURSIVE_CTE_ITERATION_LIMIT, log);
}
}
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 41a477ef083..1d129bd3bb0 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,6 +22,7 @@ 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;
@@ -48,6 +49,7 @@ 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;
@@ -139,6 +141,9 @@ 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();
@@ -472,6 +477,11 @@ 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 ed04f327eb3..569fe0b38fd 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,6 +74,8 @@ 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.RepeatUnionNode;
import org.apache.ignite.internal.processors.query.calcite.exec.rel.ScanNode;
import
org.apache.ignite.internal.processors.query.calcite.exec.rel.ScanStorageNode;
import
org.apache.ignite.internal.processors.query.calcite.exec.rel.ScanTableRowNode;
@@ -101,8 +103,10 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRepeatUnion;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
@@ -615,6 +619,11 @@ 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());
+ }
+
/** {@inheritDoc} */
@Override public Node<Row> visit(IgniteValues rel) {
List<RexLiteral> vals = Commons.flat(Commons.cast(rel.getTuples()));
@@ -635,6 +644,17 @@ public class LogicalRelImplementor<Row> implements
IgniteRelVisitor<Node<Row>> {
return node;
}
+ /** {@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());
+
+ state.clear();
+ node.register(F.asList(visit(rel.getLeft()), visit(rel.getRight())));
+
+ return node;
+ }
+
/** {@inheritDoc} */
@Override public Node<Row> visit(IgniteLimit rel) {
long offset = validateAndGetOffset(rel.offset(),
LimitNode.OFFSET_DEFAULT);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveCteState.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveCteState.java
new file mode 100644
index 00000000000..8a865164de8
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RecursiveCteState.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.RowTracker;
+import org.apache.ignite.internal.util.GridUnsafe;
+
+/** Query-local current and next deltas of a recursive CTE. */
+public class RecursiveCteState<Row> {
+ /** Rows visible to the recursive table scan. */
+ private List<Row> cur = Collections.emptyList();
+
+ /** Rows produced by the active seed or recursive term. */
+ private List<Row> next;
+
+ /** Memory tracker for rows in the current delta. */
+ private RowTracker<Row> curMemoryTracker;
+
+ /** Memory tracker for rows in the next delta. */
+ private RowTracker<Row> nextMemoryTracker;
+
+ /** */
+ public RecursiveCteState(ExecutionContext<Row> ctx) {
+ curMemoryTracker =
ctx.createNodeMemoryTracker(GridUnsafe.OBJ_REF_SIZE);
+ nextMemoryTracker =
ctx.createNodeMemoryTracker(GridUnsafe.OBJ_REF_SIZE);
+ }
+
+ /** Starts collecting the next delta. */
+ public void beginWrite() {
+ assert next == null;
+
+ next = new ArrayList<>();
+ }
+
+ /** Adds a row to the next delta. */
+ public void add(Row row) {
+ assert next != null;
+
+ next.add(row);
+ nextMemoryTracker.onRowAdded(row);
+ }
+
+ /** Makes the collected delta visible to recursive scans. */
+ public void commit() {
+ assert next != null;
+
+ curMemoryTracker.reset();
+ cur = next;
+ next = null;
+
+ RowTracker<Row> tracker = curMemoryTracker;
+
+ curMemoryTracker = nextMemoryTracker;
+ nextMemoryTracker = tracker;
+ }
+
+ /** Current delta. */
+ public Iterable<Row> current() {
+ return () -> cur.iterator();
+ }
+
+ /** Returns whether the current delta is empty. */
+ public boolean isEmpty() {
+ return cur.isEmpty();
+ }
+
+ /** Clears all query-local rows. */
+ public void clear() {
+ cur = Collections.emptyList();
+ next = null;
+
+ curMemoryTracker.reset();
+ nextMemoryTracker.reset();
+ }
+}
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
new file mode 100644
index 00000000000..33622e6ce57
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
@@ -0,0 +1,183 @@
+/*
+ * 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 org.apache.calcite.rel.type.RelDataType;
+import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.util.typedef.F;
+
+import static
org.apache.ignite.internal.processors.query.calcite.DistributedCalciteConfiguration.RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME;
+
+/** Coordinator-side executor for recursive UNION ALL. */
+public class RepeatUnionNode<Row> extends AbstractNode<Row> implements
Downstream<Row> {
+ /** Index of the seed input. */
+ private static final int SEED_SOURCE = 0;
+
+ /** Index of the recursive-term input. */
+ private static final int RECURSIVE_SOURCE = 1;
+
+ /** Query-local recursive state. */
+ private final RecursiveCteState<Row> state;
+
+ /** Maximum number of recursive iterations, or a negative value for no
limit. */
+ private final int iterationLimit;
+
+ /** Index of the active source. */
+ private int curSrc = SEED_SOURCE;
+
+ /** Number of rows still requested by downstream. */
+ private int waiting;
+
+ /** Number of completed recursive iterations. */
+ private int iteration;
+
+ /** Whether the active input is being collected into the next delta. */
+ private boolean writing;
+
+ /** */
+ public RepeatUnionNode(
+ ExecutionContext<Row> ctx,
+ RelDataType rowType,
+ RecursiveCteState<Row> state,
+ int iterationLimit
+ ) {
+ super(ctx, rowType);
+
+ this.state = state;
+ this.iterationLimit = iterationLimit;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void request(int rowsCnt) throws Exception {
+ assert !F.isEmpty(sources()) && sources().size() == 2;
+ assert rowsCnt > 0 && waiting == 0;
+
+ checkState();
+
+ waiting = rowsCnt;
+ requestSource();
+ }
+
+ /** {@inheritDoc} */
+ @Override public void push(Row row) throws Exception {
+ assert downstream() != null;
+ assert waiting > 0;
+ assert writing;
+
+ checkState();
+
+ waiting--;
+ state.add(row);
+
+ downstream().push(row);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void end() throws Exception {
+ assert downstream() != null;
+ assert waiting > 0;
+ assert writing;
+
+ checkState();
+
+ state.commit();
+ writing = false;
+
+ if (state.isEmpty()) {
+ finish();
+
+ return;
+ }
+
+ if (curSrc == SEED_SOURCE) {
+ if (iterationLimit == 0) {
+ throw iterationLimitExceeded();
+ }
+
+ curSrc = RECURSIVE_SOURCE;
+ requestSource();
+
+ return;
+ }
+
+ iteration++;
+
+ if (iterationLimit >= 0 && iteration == iterationLimit) {
+ throw iterationLimitExceeded();
+ }
+
+ source().rewind();
+ requestSource();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Downstream<Row> requestDownstream(int idx) {
+ assert idx >= 0 && idx < 2;
+
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void rewindInternal() {
+ curSrc = SEED_SOURCE;
+ waiting = 0;
+ iteration = 0;
+ writing = false;
+ state.clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void closeInternal() {
+ state.clear();
+
+ super.closeInternal();
+ }
+
+ /** */
+ private Node<Row> source() {
+ return sources().get(curSrc);
+ }
+
+ /** Starts collecting and requests rows from the active input. */
+ private void requestSource() throws Exception {
+ if (!writing) {
+ state.beginWrite();
+ writing = true;
+ }
+
+ source().request(waiting);
+ }
+
+ /** */
+ private void finish() throws Exception {
+ waiting = -1;
+ state.clear();
+ downstream().end();
+ }
+
+ /** */
+ private IgniteSQLException iterationLimitExceeded() {
+ return new IgniteSQLException(
+ "Recursive CTE iteration limit exceeded [limit=" + iterationLimit +
+ ", property=" + RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME +
']',
+ IgniteQueryErrorCode.QUERY_CANCELED
+ );
+ }
+}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
index 51b0ca886ad..99ad37affff 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
@@ -37,6 +37,7 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableFunctionScan;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteTrimExchange;
@@ -216,6 +217,17 @@ public class IgniteMdFragmentMapping implements
MetadataHandler<FragmentMappingM
rel.getTable().unwrap(IgniteTable.class).colocationGroup(ctx));
}
+ /**
+ * See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode,
RelMetadataQuery, MappingQueryContext)}
+ */
+ public FragmentMapping fragmentMapping(
+ IgniteRecursiveTableScan rel,
+ RelMetadataQuery mq,
+ MappingQueryContext ctx
+ ) {
+ return FragmentMapping.create(ctx.localNodeId());
+ }
+
/**
* See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode,
RelMetadataQuery, MappingQueryContext)}
*/
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
index a60ae8a9cb2..dc6b0e3c8bc 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
@@ -33,8 +33,10 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRepeatUnion;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
@@ -179,6 +181,11 @@ public class Cloner implements IgniteRelVisitor<IgniteRel>
{
return rel.clone(cluster, F.asList());
}
+ /** {@inheritDoc} */
+ @Override public IgniteRel visit(IgniteRecursiveTableScan rel) {
+ return rel.clone(cluster, F.asList());
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel visit(IgniteValues rel) {
return rel.clone(cluster, F.asList());
@@ -189,6 +196,11 @@ public class Cloner implements IgniteRelVisitor<IgniteRel>
{
return rel.clone(cluster, Commons.transform(rel.getInputs(), rel0 ->
visit((IgniteRel)rel0)));
}
+ /** {@inheritDoc} */
+ @Override public IgniteRel visit(IgniteRepeatUnion rel) {
+ return rel.clone(cluster, F.asList(visit((IgniteRel)rel.getLeft()),
visit((IgniteRel)rel.getRight())));
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel visit(IgniteSort rel) {
return rel.clone(cluster, F.asList(visit((IgniteRel)rel.getInput())));
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
index c922ccba7c8..e5b317a5eaf 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
@@ -32,8 +32,10 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRepeatUnion;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
@@ -142,6 +144,11 @@ public class IgniteRelShuttle implements
IgniteRelVisitor<IgniteRel> {
return processNode(rel);
}
+ /** {@inheritDoc} */
+ @Override public IgniteRel visit(IgniteRepeatUnion rel) {
+ return processNode(rel);
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel visit(IgniteSort rel) {
return processNode(rel);
@@ -172,6 +179,11 @@ public class IgniteRelShuttle implements
IgniteRelVisitor<IgniteRel> {
return processNode(rel);
}
+ /** {@inheritDoc} */
+ @Override public IgniteRel visit(IgniteRecursiveTableScan rel) {
+ return processNode(rel);
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel visit(IgniteReceiver rel) {
return processNode(rel);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
index 71753b173f2..eac30d2d05e 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
@@ -62,6 +62,8 @@ import
org.apache.ignite.internal.processors.query.calcite.rule.LogicalScanConve
import
org.apache.ignite.internal.processors.query.calcite.rule.MergeJoinConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.NestedLoopJoinConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.ProjectConverterRule;
+import
org.apache.ignite.internal.processors.query.calcite.rule.RecursiveTableScanConverterRule;
+import
org.apache.ignite.internal.processors.query.calcite.rule.RepeatUnionConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.SetOpConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.SortAggregateConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.SortConverterRule;
@@ -311,6 +313,8 @@ public enum PlannerPhase {
//CoreRules.WINDOW_REDUCE_EXPRESSIONS,
ValuesConverterRule.INSTANCE,
+ RepeatUnionConverterRule.INSTANCE,
+ RecursiveTableScanConverterRule.INSTANCE,
LogicalScanConverterRule.INDEX_SCAN,
LogicalScanConverterRule.TABLE_SCAN,
IndexCountRule.INSTANCE,
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 b522f1a0db3..ffb42003e83 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,14 +17,18 @@
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;
@@ -34,6 +38,8 @@ import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import static
org.apache.ignite.internal.processors.query.calcite.DistributedCalciteConfiguration.DFLT_RECURSIVE_CTE_ITERATION_LIMIT;
+
/**
* Planning context.
*/
@@ -56,12 +62,21 @@ 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;
/** */
private final long plannerTimeout;
+ /** Maximum number of recursive CTE iterations, or a negative value for no
limit. */
+ private int recursiveCteIterationLimit =
DFLT_RECURSIVE_CTE_ITERATION_LIMIT;
+
/**
* Private constructor, used by a builder.
*/
@@ -115,6 +130,20 @@ public final class PlanningContext implements Context {
return plannerTimeout;
}
+ /**
+ * @return Maximum number of recursive CTE iterations, or a negative value
for no limit.
+ */
+ public int recursiveCteIterationLimit() {
+ return recursiveCteIterationLimit;
+ }
+
+ /**
+ * @param recursiveCteIterationLimit Maximum number of recursive CTE
iterations, or a negative value for no limit.
+ */
+ void recursiveCteIterationLimit(int recursiveCteIterationLimit) {
+ this.recursiveCteIterationLimit = recursiveCteIterationLimit;
+ }
+
/**
* @return Schema.
*/
@@ -160,6 +189,18 @@ 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/PrepareServiceImpl.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
index 52c1bc819b9..a2f2ffdff6a 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
@@ -103,6 +103,8 @@ public class PrepareServiceImpl extends AbstractService
implements PrepareServic
assert distrCfg != null;
+
ctx.recursiveCteIterationLimit(distrCfg.recursiveCteIterationLimit());
+
String[] disbledRules = distrCfg.disabledRules();
if (!F.isEmpty(disbledRules))
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
index e8fb9c25f24..07d7b7b6456 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
@@ -29,6 +29,7 @@ import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.core.Spool;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.util.ImmutableBitSet;
import
org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
import
org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCostFactory;
@@ -92,6 +93,24 @@ public class IgniteHashIndexSpool extends Spool implements
IgniteRel {
return visitor.visit(this);
}
+ /** {@inheritDoc} */
+ @Override public RelNode accept(RexShuttle shuttle) {
+ List<RexNode> newSearchRow = shuttle.apply(searchRow);
+ RexNode newCondition = shuttle.apply(cond);
+
+ if (newSearchRow == searchRow && newCondition == cond)
+ return this;
+
+ return new IgniteHashIndexSpool(
+ getCluster(),
+ getTraitSet(),
+ getInput(),
+ newSearchRow,
+ newCondition,
+ allowNulls
+ );
+ }
+
/** */
@Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
return new IgniteHashIndexSpool(cluster, getTraitSet(), inputs.get(0),
searchRow, cond, allowNulls);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexScan.java
index a7397ffcedb..0d202e154b2 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexScan.java
@@ -28,6 +28,8 @@ import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.util.ImmutableBitSet;
import
org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
import org.jetbrains.annotations.Nullable;
import static
org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils.changeTraits;
@@ -134,6 +136,20 @@ public class IgniteIndexScan extends AbstractIndexScan
implements SourceAwareIgn
return visitor.visit(this);
}
+ /** {@inheritDoc} */
+ @Override protected IgniteIndexScan copy(
+ RelTraitSet traitSet,
+ @Nullable RelDataType rowType,
+ @Nullable List<RexNode> projects,
+ @Nullable RexNode condition
+ ) {
+ IgniteIndex idx =
getTable().unwrap(IgniteTable.class).getIndex(idxName);
+ List<SearchBounds> newSearchBounds = idx.toSearchBounds(getCluster(),
condition, requiredColumns);
+
+ return new IgniteIndexScan(sourceId, getCluster(), traitSet,
getTable(), idxName, rowType, projects, condition,
+ newSearchBounds, requiredColumns, collation);
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel clone(long sourceId) {
return new IgniteIndexScan(sourceId, getCluster(), getTraitSet(),
getTable(),
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
new file mode 100644
index 00000000000..d51e1bd53d1
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRecursiveTableScan.java
@@ -0,0 +1,89 @@
+/*
+ * 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.rel;
+
+import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelInput;
+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
+ ) {
+ super(cluster, traits);
+
+ this.rowType = rowType;
+ this.stateId = stateId;
+ }
+
+ /** Constructor used for deserialization. */
+ public IgniteRecursiveTableScan(RelInput input) {
+ this(
+ input.getCluster(),
+ input.getTraitSet().replace(IgniteConvention.INSTANCE),
+ input.getRowType("rowType"),
+ requireNonNull(input.getString("stateId"), "stateId")
+ );
+ }
+
+ /** 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);
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T> T accept(IgniteRelVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
+ assert inputs.isEmpty();
+
+ return new IgniteRecursiveTableScan(cluster, getTraitSet(), rowType,
stateId);
+ }
+
+ /** {@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/IgniteRelVisitor.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
index 456b7b46d26..e82b63001a3 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
@@ -89,6 +89,11 @@ public interface IgniteRelVisitor<T> {
*/
T visit(IgniteTableScan rel);
+ /**
+ * See {@link IgniteRelVisitor#visit(IgniteRel)}
+ */
+ T visit(IgniteRecursiveTableScan rel);
+
/**
* See {@link IgniteRelVisitor#visit(IgniteRel)}
*/
@@ -144,6 +149,11 @@ public interface IgniteRelVisitor<T> {
*/
T visit(IgniteUnionAll rel);
+ /**
+ * See {@link IgniteRelVisitor#visit(IgniteRel)}
+ */
+ T visit(IgniteRepeatUnion rel);
+
/**
* See {@link IgniteRelVisitor#visit(IgniteRel)}
*/
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
new file mode 100644
index 00000000000..0a7db70d139
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
@@ -0,0 +1,102 @@
+/*
+ * 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.rel;
+
+import java.util.List;
+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. */
+ public IgniteRepeatUnion(RelInput input) {
+ this(
+ input.getCluster(),
+ 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;
+ }
+
+ /** {@inheritDoc} */
+ @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);
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T> T accept(IgniteRelVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
+ /** {@inheritDoc} */
+ @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);
+ }
+
+ /** Reads the optional iteration limit emitted by {@link
RepeatUnion#explainTerms(RelWriter)}. */
+ private static int iterationLimit(RelInput input) {
+ Number iterationLimit = (Number)input.get("iterationLimit");
+
+ return iterationLimit == null ? -1 : iterationLimit.intValue();
+ }
+}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
index 33d45ea51a7..654ba228663 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
@@ -30,10 +30,12 @@ import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.core.Spool;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
import
org.apache.ignite.internal.processors.query.calcite.externalize.RelInputEx;
import
org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
import
org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCostFactory;
import
org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
/**
* Relational operator that returns the sorted contents of a table
@@ -87,6 +89,38 @@ public class IgniteSortedIndexSpool extends Spool implements
IgniteRel {
return visitor.visit(this);
}
+ /** {@inheritDoc} */
+ @Override public RelNode accept(RexShuttle shuttle) {
+ boolean[] boundsChanged = {false};
+
+ List<SearchBounds> newSearchBounds = searchBounds == null ? null :
Commons.transform(searchBounds, bounds -> {
+ if (bounds == null)
+ return null;
+
+ return bounds.transform(node -> {
+ RexNode newNode = shuttle.apply(node);
+
+ boundsChanged[0] |= newNode != node;
+
+ return newNode;
+ });
+ });
+
+ RexNode newCondition = shuttle.apply(condition);
+
+ if (!boundsChanged[0] && newCondition == condition)
+ return this;
+
+ return new IgniteSortedIndexSpool(
+ getCluster(),
+ getTraitSet(),
+ getInput(),
+ collation,
+ newCondition,
+ newSearchBounds
+ );
+ }
+
/** */
@Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
return new IgniteSortedIndexSpool(cluster, getTraitSet(),
inputs.get(0), collation, condition, searchBounds);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
index c35b415f197..5ca8fdc00a2 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
@@ -129,6 +129,17 @@ public class IgniteTableScan extends
ProjectableFilterableTableScan implements S
return visitor.visit(this);
}
+ /** {@inheritDoc} */
+ @Override protected IgniteTableScan copy(
+ RelTraitSet traitSet,
+ @Nullable RelDataType rowType,
+ @Nullable List<RexNode> projects,
+ @Nullable RexNode condition
+ ) {
+ return new IgniteTableScan(sourceId, getCluster(), traitSet,
getTable(), rowType, projects, condition,
+ requiredColumns);
+ }
+
/** {@inheritDoc} */
@Override public IgniteRel clone(long sourceId) {
return new IgniteTableScan(sourceId, getCluster(), getTraitSet(),
getTable(), rowType, projects, condition,
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
index bef05a9b5e3..ab648abb1dd 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
@@ -121,6 +121,36 @@ public abstract class ProjectableFilterableTableScan
extends TableScan {
return this;
}
+ /** {@inheritDoc} */
+ @Override public RelNode accept(RexShuttle shuttle) {
+ List<RexNode> newProjects = projects == null ? null :
shuttle.apply(projects);
+ RexNode newCondition = shuttle.apply(condition);
+
+ if (newProjects == projects && newCondition == condition)
+ return this;
+
+ RelDataType newRowType = rowType;
+
+ if (newProjects != projects) {
+ newRowType = RexUtil.createStructType(
+ getCluster().getTypeFactory(),
+ newProjects,
+ getRowType().getFieldNames(),
+ null
+ );
+ }
+
+ return copy(getTraitSet(), newRowType, newProjects, newCondition);
+ }
+
+ /** Creates a copy with transformed projects and condition. */
+ protected abstract ProjectableFilterableTableScan copy(
+ RelTraitSet traitSet,
+ @Nullable RelDataType rowType,
+ @Nullable List<RexNode> projects,
+ @Nullable RexNode condition
+ );
+
/** {@inheritDoc} */
@Override public RelWriter explainTerms(RelWriter pw) {
return explainTerms0(super.explainTerms(pw));
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
index c0cfa088e6f..80c6f474614 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
@@ -85,4 +85,14 @@ public class IgniteLogicalIndexScan extends
AbstractIndexScan {
) {
super(cluster, traits, tbl, idxName, rowType, proj, cond,
searchBounds, requiredCols);
}
+
+ /** {@inheritDoc} */
+ @Override protected IgniteLogicalIndexScan copy(
+ RelTraitSet traitSet,
+ @Nullable RelDataType rowType,
+ @Nullable List<RexNode> projects,
+ @Nullable RexNode condition
+ ) {
+ return create(getCluster(), traitSet, getTable(), idxName, rowType,
projects, condition, requiredColumns);
+ }
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java
index 1cbd7544bf5..016d909853c 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java
@@ -76,4 +76,15 @@ public class IgniteLogicalTableScan extends
ProjectableFilterableTableScan {
return new IgniteLogicalTableScan(getCluster(), getTraitSet(),
getTable(), hints,
rowType, projects(), condition(), requiredColumns());
}
+
+ /** {@inheritDoc} */
+ @Override protected IgniteLogicalTableScan copy(
+ RelTraitSet traitSet,
+ @Nullable RelDataType rowType,
+ @Nullable List<RexNode> projects,
+ @Nullable RexNode condition
+ ) {
+ return new IgniteLogicalTableScan(getCluster(), traitSet, getTable(),
getHints(), rowType, projects, condition,
+ requiredColumns);
+ }
}
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
new file mode 100644
index 00000000000..395d85b38d1
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveCteUtils.java
@@ -0,0 +1,218 @@
+/*
+ * 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.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;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+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;
+
+import static
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions.single;
+
+/** Utilities shared by recursive CTE converter rules. */
+final class RecursiveCteUtils {
+ /** */
+ private RecursiveCteUtils() {
+ // No-op.
+ }
+
+ /** Returns whether the table is Calcite's query-local transient table. */
+ static boolean isTransient(RelOptTable table) {
+ 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);
+
+ int cnt = isRecursiveScan(rel, table) ? 1 : 0;
+
+ for (RelNode input : rel.getInputs())
+ cnt += referenceCount(input, table);
+
+ return cnt;
+ }
+
+ /** Converts maximal inputs that do not depend on the current delta to
coordinator-local rewindable execution. */
+ static RelNode convertStaticInputs(RelNode rel, RelOptTable table,
RelTraitSet traits) {
+ rel = original(rel);
+
+ if (isRecursiveScan(rel, table))
+ return rel;
+
+ List<RelNode> inputs = rel.getInputs();
+
+ if (inputs.isEmpty())
+ return rel;
+
+ List<RelNode> newInputs = new ArrayList<>(inputs.size());
+
+ for (RelNode input : inputs) {
+ if (referenceCount(input, table) == 0 && isInvariant(input))
+ newInputs.add(convertStaticInput(input, traits));
+ else
+ newInputs.add(convertStaticInputs(input, table, traits));
+ }
+
+ return rel.copy(rel.getTraitSet(), newInputs);
+ }
+
+ /** Converts an iteration-independent input to coordinator-local
rewindable execution. */
+ private static RelNode convertStaticInput(RelNode input, RelTraitSet
traits) {
+ IgniteDistribution inputDistribution =
+
(IgniteDistribution)input.getCluster().getMetadataQuery().distribution(original(input));
+
+ if (inputDistribution.satisfies(single()))
+ return RelOptRule.convert(input,
traits.replace(RewindabilityTrait.REWINDABLE));
+
+ RelNode convertedInput = RelOptRule.convert(input, traits);
+
+ return TraitUtils.convertRewindability(
+ input.getCluster().getPlanner(),
+ RewindabilityTrait.REWINDABLE,
+ convertedInput
+ );
+ }
+
+ /** Returns whether the subtree produces the same result on every
recursive iteration. */
+ private static boolean isInvariant(RelNode rel) {
+ rel = original(rel);
+
+ if (!RelOptUtil.getVariablesUsed(rel).isEmpty())
+ return false;
+
+ DeterminismChecker checker = new DeterminismChecker();
+
+ rel.accept(checker);
+
+ if (!checker.deterministic)
+ return false;
+
+ if (rel instanceof Aggregate) {
+ for (AggregateCall call : ((Aggregate)rel).getAggCallList()) {
+ if (!call.getAggregation().isDeterministic())
+ return false;
+ }
+ }
+
+ for (RelNode input : rel.getInputs()) {
+ if (!isInvariant(input))
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the logical expression represented by a Volcano subset.
Converter rule inputs can be subsets whose own
+ * input list is empty.
+ */
+ static RelNode original(RelNode rel) {
+ while (rel instanceof RelSubset) {
+ RelNode original = ((RelSubset)rel).getOriginal();
+
+ if (original == null)
+ return rel;
+
+ rel = original;
+ }
+
+ return rel;
+ }
+
+ /** Returns whether both optimizer tables represent the same transient
table instance. */
+ static boolean sameTransientTable(RelOptTable first, RelOptTable second) {
+ TransientTable firstTable = first == null ? null :
first.unwrap(TransientTable.class);
+ TransientTable secondTable = second == null ? null :
second.unwrap(TransientTable.class);
+
+ return firstTable != null && firstTable == secondTable;
+ }
+
+ /** */
+ private static boolean isRecursiveScan(RelNode rel, RelOptTable table) {
+ return rel instanceof TableScan
+ && sameTransientTable(((TableScan)rel).getTable(), table);
+ }
+
+ /** Finds non-deterministic expressions in one relational node. */
+ private static class DeterminismChecker extends RexShuttle {
+ /** Whether all visited expressions are deterministic. */
+ private boolean deterministic = true;
+
+ /** {@inheritDoc} */
+ @Override public RexNode visitCall(RexCall call) {
+ if (!call.getOperator().isDeterministic() ||
isNonDeterministicUdf(call)) {
+ deterministic = false;
+
+ return call;
+ }
+
+ return super.visitCall(call);
+ }
+
+ /** {@inheritDoc} */
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ if (!isInvariant(subQuery.rel)) {
+ deterministic = false;
+
+ return subQuery;
+ }
+
+ return super.visitSubQuery(subQuery);
+ }
+
+ /** Returns whether the call targets an Ignite UDF declared as
non-deterministic. */
+ private static boolean isNonDeterministicUdf(RexCall call) {
+ if (!(call.getOperator() instanceof SqlUserDefinedFunction))
+ return false;
+
+ Object function =
((SqlUserDefinedFunction)call.getOperator()).getFunction();
+
+ return function instanceof IgniteScalarFunction
+ && !((IgniteScalarFunction)function).isDeterministic();
+ }
+ }
+}
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
new file mode 100644
index 00000000000..63c0e01401f
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RecursiveTableScanConverterRule.java
@@ -0,0 +1,63 @@
+/*
+ * 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.rule;
+
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.PhysicalNode;
+import org.apache.calcite.rel.logical.LogicalTableScan;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
+import
org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait;
+import org.jetbrains.annotations.Nullable;
+
+import static
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions.single;
+
+/** Converts a scan of the recursive transient table to a query-local delta
scan. */
+public class RecursiveTableScanConverterRule extends
AbstractIgniteConverterRule<LogicalTableScan> {
+ /** Instance. */
+ public static final RelOptRule INSTANCE = new
RecursiveTableScanConverterRule();
+
+ /** */
+ private RecursiveTableScanConverterRule() {
+ super(LogicalTableScan.class, "RecursiveTableScanConverterRule");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected @Nullable PhysicalNode convert(
+ RelOptPlanner planner,
+ RelMetadataQuery mq,
+ LogicalTableScan rel
+ ) {
+ if (!RecursiveCteUtils.isTransient(rel.getTable()))
+ return null;
+
+ RelTraitSet traits =
rel.getCluster().traitSetOf(IgniteConvention.INSTANCE)
+ .replace(single())
+ .replace(RewindabilityTrait.REWINDABLE);
+
+ return new IgniteRecursiveTableScan(
+ rel.getCluster(),
+ traits,
+ rel.getRowType(),
+ RecursiveCteUtils.stateId(planner, rel.getTable())
+ );
+ }
+}
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
new file mode 100644
index 00000000000..d53542ea5ac
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
@@ -0,0 +1,102 @@
+/*
+ * 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.rule;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.PhysicalNode;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Spool;
+import org.apache.calcite.rel.logical.LogicalRepeatUnion;
+import org.apache.calcite.rel.logical.LogicalTableSpool;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import
org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRepeatUnion;
+
+import static
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions.single;
+
+/** Converts Calcite's logical recursive union to coordinator-side execution.
*/
+public class RepeatUnionConverterRule extends
AbstractIgniteConverterRule<LogicalRepeatUnion> {
+ /** Instance. */
+ public static final RelOptRule INSTANCE = new RepeatUnionConverterRule();
+
+ /** */
+ private RepeatUnionConverterRule() {
+ super(LogicalRepeatUnion.class, "RecursiveCteConverterRule");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected PhysicalNode convert(RelOptPlanner planner,
RelMetadataQuery mq, LogicalRepeatUnion rel) {
+ RelOptTable table = rel.getTransientTable();
+
+ if (table == null || !RecursiveCteUtils.isTransient(table))
+ throw unsupported("a transient table is required");
+
+ // TODO: IGNITE-29012 Support recursive CTE with UNION DISTINCT.
+ 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");
+ RelNode iterative = unwrapSpool(rel.getIterativeRel(), "recursive
term");
+
+ RelOptCluster cluster = rel.getCluster();
+ RelTraitSet traits =
cluster.traitSetOf(IgniteConvention.INSTANCE).replace(single());
+ iterative = RecursiveCteUtils.convertStaticInputs(iterative, table,
traits);
+
+ return new IgniteRepeatUnion(
+ cluster,
+ traits,
+ convert(seed, traits),
+ convert(iterative, traits),
+ stateId,
+ iterationLimit
+ );
+ }
+
+ /** */
+ private static RelNode unwrapSpool(RelNode rel, String term) {
+ rel = RecursiveCteUtils.original(rel);
+
+ if (!(rel instanceof LogicalTableSpool))
+ throw unsupported("the " + term + " must use a transient table
spool");
+
+ LogicalTableSpool spool = (LogicalTableSpool)rel;
+
+ if (spool.readType != Spool.Type.LAZY || spool.writeType !=
Spool.Type.LAZY)
+ throw unsupported("only lazy transient table spools are
supported");
+
+ return spool.getInput();
+ }
+
+ /** */
+ private static IgniteSQLException unsupported(String detail) {
+ return new IgniteSQLException(
+ "Unsupported recursive CTE: " + detail,
+ IgniteQueryErrorCode.UNSUPPORTED_OPERATION
+ );
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorPropertiesTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorPropertiesTest.java
index 71b16cc4536..bd0f3646d74 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorPropertiesTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorPropertiesTest.java
@@ -38,8 +38,11 @@ import org.junit.Test;
import static
org.apache.ignite.internal.processors.query.QueryParserMetricsHolder.QUERY_PARSER_METRIC_GROUP_NAME;
import static
org.apache.ignite.internal.processors.query.calcite.DistributedCalciteConfiguration.DFLT_PLAN_CACHE_SIZE;
+import static
org.apache.ignite.internal.processors.query.calcite.DistributedCalciteConfiguration.DFLT_RECURSIVE_CTE_ITERATION_LIMIT;
+import static
org.apache.ignite.internal.processors.query.calcite.DistributedCalciteConfiguration.RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME;
import static
org.apache.ignite.internal.processors.query.calcite.QueryChecker.containsIndexScan;
import static
org.apache.ignite.internal.processors.query.calcite.QueryChecker.containsSubPlan;
+import static
org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause;
import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
import static org.hamcrest.CoreMatchers.not;
@@ -54,6 +57,12 @@ public class CalciteQueryProcessorPropertiesTest extends
AbstractBasicIntegratio
@Override protected void afterTest() throws Exception {
changeDistributedProperty(DistributedCalciteConfiguration.DISABLED_RULES_PROPERTY_NAME,
" ",
pVal -> F.compareArrays(pVal, new String[0]) == 0);
+
+ changeDistributedProperty(
+ RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME,
+ String.valueOf(DFLT_RECURSIVE_CTE_ITERATION_LIMIT),
+ pVal -> pVal.equals(DFLT_RECURSIVE_CTE_ITERATION_LIMIT)
+ );
}
/** */
@@ -212,6 +221,49 @@ public class CalciteQueryProcessorPropertiesTest extends
AbstractBasicIntegratio
checkPlanCacheSize(grid(1), 100);
}
+ /** */
+ @Test
+ public void testRecursiveCteIterationLimit() throws Exception {
+ for (Ignite ig : G.allGrids()) {
+ DistributedChangeableProperty<Integer> prop =
+ distributedProperty(ig,
RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME);
+
+ assertNotNull(prop);
+ assertEquals(DFLT_RECURSIVE_CTE_ITERATION_LIMIT,
prop.get().intValue());
+ }
+
+ String qry = "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 5" +
+ ") " +
+ "SELECT n FROM numbers";
+
+ // Populate the plan cache with the default iteration limit.
+ assertQuery(qry)
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .returns(4)
+ .returns(5)
+ .check();
+
+ changeDistributedProperty(
+ RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME,
+ "2",
+ pVal -> pVal.equals(2)
+ );
+
+ // The property listener must invalidate the cached plan and apply the
new limit to the same query.
+ assertThrowsAnyCause(
+ log,
+ () -> sql(qry),
+ IgniteSQLException.class,
+ "Recursive CTE iteration limit exceeded [limit=2, property=" +
+ RECURSIVE_CTE_ITERATION_LIMIT_PROPERTY_NAME + ']'
+ );
+ }
+
/** */
private void checkPlanCacheSize(IgniteEx grid, int expectedCacheSize) {
MetricRegistryImpl mreg =
grid.context().metric().registry(QUERY_PARSER_METRIC_GROUP_NAME);
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java
index 2d905e457d7..ca271177986 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java
@@ -243,6 +243,24 @@ public class MemoryQuotasIntegrationTest extends
AbstractBasicIntegrationTest {
IgniteSQLException.class, "Query quota exceeded");
}
+ /** */
+ @Test
+ public void testRecursiveDeltaIsAccountedForMemoryQuota() {
+ assertThrows(
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 " +
+ "FROM numbers " +
+ "CROSS JOIN (VALUES (1), (2)) AS fanout(x) " +
+ "WHERE n < 19" +
+ ") " +
+ "SELECT COUNT(*) FROM numbers",
+ IgniteSQLException.class,
+ "Query quota exceeded"
+ );
+ }
+
/** */
@Test
public void testRightMeterializedJoins() {
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 ff77e4f1e0f..11a333e30fa 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
@@ -17,44 +17,216 @@
package org.apache.ignite.internal.processors.query.calcite.integration;
-import org.apache.calcite.plan.RelOptPlanner;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.cache.query.annotations.QuerySqlFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
-import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
import org.junit.Test;
/**
* Integration tests for recursive common table expressions.
*/
public class RecursiveCteIntegrationTest extends AbstractBasicIntegrationTest {
+ /** Number of invocations of a non-deterministic function. */
+ private static final AtomicInteger nonDeterministicCallCnt = new
AtomicInteger();
+
+ /** */
+ @Test
+ public void testEmployeeHierarchy() {
+ createEmployeeTable();
+
+ assertQuery("WITH RECURSIVE employee_hierarchy (id, manager_id, name,
depth) AS (" +
+ "SELECT id, manager_id, name, 0 FROM employee WHERE manager_id IS
NULL " +
+ "UNION ALL " +
+ "SELECT e.id, e.manager_id, e.name, h.depth + 1 " +
+ "FROM employee e " +
+ "JOIN employee_hierarchy h ON e.manager_id = h.id" +
+ ") " +
+ "SELECT id, manager_id, name, depth FROM employee_hierarchy ORDER
BY depth, id")
+ .returns(1, null, "CEO", 0)
+ .returns(2, 1, "Manager", 1)
+ .returns(4, 1, "Accountant", 1)
+ .returns(3, 2, "Developer", 2)
+ .check();
+ }
+
/** */
@Test
- public void testHierarchicalQueryIsNotSupported() {
+ public void testRecursionStopsWhenDeltaIsEmpty() {
sql("CREATE TABLE employee (id INT PRIMARY KEY, manager_id INT, name
VARCHAR)");
- sql("INSERT INTO employee VALUES " +
- "(1, NULL, 'CEO'), " +
- "(2, 1, 'Manager'), " +
- "(3, 2, 'Developer'), " +
- "(4, 1, 'Accountant')");
+ sql("INSERT INTO employee VALUES (1, NULL, 'CEO')");
- String qry = "WITH RECURSIVE employee_hierarchy (id, manager_id, name,
depth) AS (" +
+ assertQuery("WITH RECURSIVE employee_hierarchy (id, manager_id, name,
depth) AS (" +
"SELECT id, manager_id, name, 0 FROM employee WHERE manager_id IS
NULL " +
"UNION ALL " +
"SELECT e.id, e.manager_id, e.name, h.depth + 1 " +
"FROM employee e " +
"JOIN employee_hierarchy h ON e.manager_id = h.id" +
") " +
- "SELECT id, manager_id, name, depth FROM employee_hierarchy ORDER
BY depth, id";
+ "SELECT id, manager_id, name, depth FROM employee_hierarchy")
+ .returns(1, null, "CEO", 0)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testRecursiveTermIsNotExecutedWhenSeedIsEmpty() {
+ sql("CREATE TABLE empty_seed (n INT PRIMARY KEY)");
+
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT n FROM empty_seed " +
+ "UNION ALL " +
+ "SELECT v.n FROM numbers RIGHT JOIN (VALUES (42)) v(n) ON TRUE" +
+ ") " +
+ "SELECT n FROM numbers FETCH FIRST 1 ROW ONLY")
+ .resultSize(0)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testRecursiveTermWithoutSelfReferenceAfterOptimization() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE FALSE" +
+ ") " +
+ "SELECT n FROM numbers")
+ .returns(1)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testRecursiveTermWithMultipleSelfReferences() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT left_numbers.n + 1 " +
+ "FROM numbers left_numbers " +
+ "JOIN numbers right_numbers ON left_numbers.n =
right_numbers.n " +
+ "WHERE left_numbers.n < 3" +
+ ") " +
+ "SELECT n FROM numbers")
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .check();
+ }
- Throwable err = GridTestUtils.assertThrows(
- log,
- () -> sql(qry),
+ /** */
+ @Test
+ public void testRecursiveCteWithMultipleRecursiveBranches() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "(" +
+ "SELECT n + 1 FROM numbers WHERE n < 3 " +
+ "UNION ALL " +
+ "SELECT n + 10 FROM numbers WHERE n < 3" +
+ ")" +
+ ") " +
+ "SELECT n FROM numbers")
+ .returns(1)
+ .returns(2)
+ .returns(11)
+ .returns(3)
+ .returns(12)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testRecursiveCteWithDistinctUnionIsRejected() {
+ assertThrows(
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT n FROM numbers",
IgniteSQLException.class,
- "Failed to plan query"
+ "only UNION ALL is supported"
);
+ }
+
+ /** */
+ @Test
+ public void testStateIsIsolatedBetweenSameNamedRecursiveCtes() {
+ assertQuery("SELECT /*+ MERGE_JOIN */ l.n, r.n " +
+ "FROM (" +
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT n, n + 9 AS join_key FROM numbers" +
+ ") l " +
+ "JOIN (" +
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 10 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 12" +
+ ") " +
+ "SELECT n, n - 9 AS join_key FROM numbers" +
+ ") r ON l.join_key = r.n")
+ .matches(QueryChecker.containsSubPlan("IgniteMergeJoin"))
+ .returns(1, 10)
+ .returns(2, 11)
+ .returns(3, 12)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void
testIndependentNonDeterministicSubtreeIsEvaluatedForEveryIteration() {
+ registerRecursiveFunctions();
+
+ nonDeterministicCallCnt.set(0);
+
+ String qry = "WITH RECURSIVE numbers(n, marker) AS (" +
+ "SELECT 1, 0 " +
+ "UNION ALL " +
+ "SELECT n + 1, v.marker " +
+ "FROM numbers " +
+ "CROSS JOIN (SELECT nextRecursiveValue() AS marker) v " +
+ "WHERE n < 4" +
+ ") " +
+ "SELECT n, marker FROM numbers ORDER BY n";
+
+ assertQuery(qry)
+ .returns(1, 0)
+ .returns(2, 1)
+ .returns(3, 2)
+ .returns(4, 3)
+ .check();
+ }
+
+ /** SQL functions used by recursive CTE tests. */
+ public static class RecursiveFunctions {
+ /** Returns a different value on every invocation. */
+ @QuerySqlFunction(deterministic = false)
+ public static int nextRecursiveValue() {
+ return nonDeterministicCallCnt.incrementAndGet();
+ }
+ }
+
+ /** */
+ private void createEmployeeTable() {
+ sql("CREATE TABLE employee (id INT PRIMARY KEY, manager_id INT, name
VARCHAR)");
+
+ sql("INSERT INTO employee VALUES " +
+ "(1, NULL, 'CEO'), " +
+ "(2, 1, 'Manager'), " +
+ "(3, 2, 'Developer'), " +
+ "(4, 1, 'Accountant')");
+ }
- assertEquals(1, err.getSuppressed().length);
- assertTrue(err.getSuppressed()[0] instanceof
RelOptPlanner.CannotPlanException);
- assertTrue(err.getSuppressed()[0].getMessage().contains(
- "There are not enough rules to produce a node with desired
properties"));
+ /** Registers SQL functions used by recursive CTE tests. */
+ private void registerRecursiveFunctions() {
+ client.getOrCreateCache(new CacheConfiguration<Integer,
Integer>("recursive_functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(RecursiveFunctions.class));
}
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/RecursiveCtePlannerTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/RecursiveCtePlannerTest.java
new file mode 100644
index 00000000000..658c1431bda
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/RecursiveCtePlannerTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.planner;
+
+import org.apache.calcite.rel.core.Exchange;
+import org.apache.calcite.rel.core.Spool;
+import org.apache.calcite.sql.type.SqlTypeName;
+import
org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteScalarFunction;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRecursiveTableScan;
+import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRepeatUnion;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteUnionAll;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
+import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.junit.Test;
+
+/** Planner tests for recursive common table expressions. */
+public class RecursiveCtePlannerTest extends AbstractPlannerTest {
+ /** Employee hierarchy query used for distribution and index planning
checks. */
+ private static final String EMPLOYEE_HIERARCHY_QUERY =
+ "WITH RECURSIVE employee_hierarchy (id, manager_id, depth) AS (" +
+ "SELECT id, manager_id, 0 FROM employee WHERE manager_id IS NULL "
+
+ "UNION ALL " +
+ "SELECT e.id, e.manager_id, h.depth + 1 " +
+ "FROM employee e " +
+ "JOIN employee_hierarchy h ON e.manager_id = h.id" +
+ ") " +
+ "SELECT id, manager_id, depth FROM employee_hierarchy";
+
+ /** Employee hierarchy query that requests indexed correlated lookups in
the recursive term. */
+ private static final String INDEXED_EMPLOYEE_HIERARCHY_QUERY =
+ EMPLOYEE_HIERARCHY_QUERY
+ .replace("SELECT e.id", "SELECT /*+ CNL_JOIN */ e.id")
+ .replace("FROM employee e", "FROM employee /*+
FORCE_INDEX(EMPLOYEE_MANAGER_IDX) */ e");
+
+ /** Checks the physical operators used to maintain the recursive delta. */
+ @Test
+ public void testRecursiveDeltaPlan() throws Exception {
+ IgniteSchema schema = new IgniteSchema(DEFAULT_SCHEMA);
+
+ String sql =
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT n FROM numbers";
+
+ assertPlan(sql, schema, isInstanceOf(IgniteRepeatUnion.class)
+ .and(hasDistribution(IgniteDistributions.single()))
+ .and(input(0, isInstanceOf(IgniteValues.class)))
+ .and(input(1,
hasChildThat(isInstanceOf(IgniteRecursiveTableScan.class))))
+ );
+ }
+
+ /** A replicated source can be read on the coordinator without an
exchange. */
+ @Test
+ public void testRecursiveCteWithReplicatedTable() throws Exception {
+ IgniteSchema schema = hierarchySchema(IgniteDistributions.broadcast(),
false);
+
+ assertPlan(EMPLOYEE_HIERARCHY_QUERY, schema,
isInstanceOf(IgniteRepeatUnion.class)
+ .and(hasDistribution(IgniteDistributions.single()))
+ .and(hasChildThat(isInstanceOf(Exchange.class)).negate())
+ );
+ }
+
+ /** A partitioned source has to be transferred to the coordinator-side
recursive plan. */
+ @Test
+ public void testRecursiveCteWithPartitionedTable() throws Exception {
+ IgniteDistribution distribution = IgniteDistributions.affinity(0,
"EMPLOYEE", "hash");
+ IgniteSchema schema = hierarchySchema(distribution, false);
+
+ assertPlan(EMPLOYEE_HIERARCHY_QUERY, schema,
isInstanceOf(IgniteRepeatUnion.class)
+ .and(hasDistribution(IgniteDistributions.single()))
+ .and(input(1, hasChildThat(isInstanceOf(Spool.class)
+ .and(hasChildThat(isInstanceOf(Exchange.class))))))
+ );
+ }
+
+ /** A replicated indexed input can be rewound without materialization. */
+ @Test
+ public void testRecursiveCteWithReplicatedIndexedTable() throws Exception {
+ IgniteSchema schema = hierarchySchema(IgniteDistributions.broadcast(),
true);
+
+ assertPlan(INDEXED_EMPLOYEE_HIERARCHY_QUERY, schema,
isInstanceOf(IgniteRepeatUnion.class)
+ .and(hasDistribution(IgniteDistributions.single()))
+ .and(input(1, hasChildThat(isInstanceOf(IgniteIndexScan.class))))
+ .and(input(1, hasChildThat(isInstanceOf(Spool.class)).negate()))
+ );
+ }
+
+ /** A non-deterministic projection in a table scan must be evaluated on
every iteration. */
+ @Test
+ public void testNonDeterministicTableScanIsNotMaterialized() throws
Exception {
+ IgniteSchema schema = recursiveMarkersSchema(false);
+
+ String sql = "WITH RECURSIVE numbers(n, marker) AS (" +
+ "SELECT 1, 0 " +
+ "UNION ALL " +
+ "SELECT n + 1, v.marker " +
+ "FROM numbers " +
+ "CROSS JOIN (" +
+ "SELECT nextRecursiveValue() AS marker FROM recursive_markers"
+
+ ") v " +
+ "WHERE n < 4" +
+ ") " +
+ "SELECT n, marker FROM numbers";
+
+ assertPlan(sql, schema, isInstanceOf(IgniteRepeatUnion.class)
+ .and(input(1, hasChildThat(isInstanceOf(IgniteTableScan.class)
+ .and(scan -> scan.projects() != null)
+ .and(scan ->
scan.projects().toString().contains("NEXTRECURSIVEVALUE")))))
+ .and(input(1, hasChildThat(isInstanceOf(Spool.class)).negate()))
+ );
+ }
+
+ /** A non-deterministic condition in an index scan must be evaluated on
every iteration. */
+ @Test
+ public void testNonDeterministicIndexScanIsNotMaterialized() throws
Exception {
+ IgniteSchema schema = recursiveMarkersSchema(true);
+
+ String sql = "WITH RECURSIVE numbers(n, marker) AS (" +
+ "SELECT 1, 0 " +
+ "UNION ALL " +
+ "SELECT n + 1, " +
+ "(SELECT marker FROM recursive_markers /*+ FORCE_INDEX */ " +
+ "WHERE id = numbers.n AND nextRecursiveValue() > 0) " +
+ "FROM numbers " +
+ "WHERE n < 4" +
+ ") " +
+ "SELECT n, marker FROM numbers";
+
+ assertPlan(sql, schema, isInstanceOf(IgniteRepeatUnion.class)
+ .and(input(1, hasChildThat(isInstanceOf(IgniteIndexScan.class)
+ .and(scan -> scan.condition() != null)
+ .and(scan ->
scan.condition().toString().contains("NEXTRECURSIVEVALUE")))))
+ .and(input(1, hasChildThat(isInstanceOf(Spool.class)).negate()))
+ );
+ }
+
+ /** Calcite places multiple non-recursive branches into the seed input of
RepeatUnion. */
+ @Test
+ public void testRecursiveCteWithMultipleSeedBranches() throws Exception {
+ IgniteSchema schema = createSchema(
+ createTable("T", IgniteDistributions.single(), "ID",
SqlTypeName.INTEGER)
+ );
+
+ String sql =
+ "WITH RECURSIVE numbers(n) AS (" +
+ "SELECT ID FROM T WHERE ID = 1 " +
+ "UNION ALL " +
+ "SELECT ID FROM T WHERE ID = 2 " +
+ "UNION ALL " +
+ "SELECT n + 1 FROM numbers WHERE n < 3" +
+ ") " +
+ "SELECT n FROM numbers";
+
+ assertPlan(sql, schema, isInstanceOf(IgniteRepeatUnion.class)
+ .and(hasDistribution(IgniteDistributions.single()))
+ .and(input(0, isInstanceOf(IgniteUnionAll.class)
+ .and(input(0, isTableScan("T")))
+ .and(input(1, isTableScan("T")))))
+ );
+ }
+
+ /** Creates an employee table with the requested distribution and optional
manager index. */
+ private static IgniteSchema hierarchySchema(IgniteDistribution
distribution, boolean withManagerIdx) {
+ TestTable table = createTable(
+ "EMPLOYEE",
+ distribution,
+ "ID", Integer.class,
+ "MANAGER_ID", Integer.class
+ );
+
+ if (withManagerIdx)
+ table.addIndex("EMPLOYEE_MANAGER_IDX", 1);
+
+ return createSchema(table);
+ }
+
+ /** Creates a replicated table and registers a non-deterministic function
used by scan tests. */
+ private static IgniteSchema recursiveMarkersSchema(boolean withIndex)
throws NoSuchMethodException {
+ TestTable table = createTable(
+ "RECURSIVE_MARKERS",
+ IgniteDistributions.broadcast(),
+ "ID", SqlTypeName.INTEGER,
+ "MARKER", SqlTypeName.INTEGER
+ );
+
+ if (withIndex)
+ table.addIndex("RECURSIVE_MARKERS_ID_IDX", 0);
+
+ IgniteSchema schema = createSchema(table);
+
+ schema.addFunction(
+ "NEXTRECURSIVEVALUE",
+ IgniteScalarFunction.create(
+ RecursiveCtePlannerTest.class.getMethod("nextRecursiveValue"),
+ false
+ )
+ );
+
+ return schema;
+ }
+
+ /** Function used only to build a non-deterministic expression in planner
tests. */
+ public static int nextRecursiveValue() {
+ return 1;
+ }
+
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
index be2d0283c15..a04f5c1becc 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
@@ -39,6 +39,7 @@ import
org.apache.ignite.internal.processors.query.calcite.planner.PlanExecution
import
org.apache.ignite.internal.processors.query.calcite.planner.PlanSplitterTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.PlannerTimeoutTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.ProjectFilterScanMergePlannerTest;
+import
org.apache.ignite.internal.processors.query.calcite.planner.RecursiveCtePlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.RexSimplificationPlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.SerializationPlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.SetOpPlannerTest;
@@ -91,6 +92,7 @@ import org.junit.runners.Suite;
InlineIndexScanPlannerTest.class,
UserDefinedViewsPlannerTest.class,
RexSimplificationPlannerTest.class,
+ RecursiveCtePlannerTest.class,
SerializationPlannerTest.class,
UncollectPlannerTest.class,
WindowPlannerTest.class,
diff --git
a/modules/calcite/src/test/sql/hierarchy/test_recursive_hierarchy.test
b/modules/calcite/src/test/sql/hierarchy/test_recursive_hierarchy.test
new file mode 100644
index 00000000000..986e28ec98d
--- /dev/null
+++ b/modules/calcite/src/test/sql/hierarchy/test_recursive_hierarchy.test
@@ -0,0 +1,64 @@
+# name: test/sql/hierarchy/test_recursive_hierarchy.test
+# description: Test recursive traversal of parent-child rows
+# group: [hierarchy]
+
+statement ok
+CREATE TABLE nodes (id INTEGER PRIMARY KEY, parent_id INTEGER);
+
+statement ok
+INSERT INTO nodes VALUES (1, NULL), (2, 1), (3, 1), (4, 2), (5, 2), (6, 3),
(10, NULL), (11, 10);
+
+# Traverse all trees. The seed has two roots and the recursive term joins a
base table.
+query III
+WITH RECURSIVE tree(id, parent_id, depth) AS (
+ SELECT id, parent_id, 0
+ FROM nodes
+ WHERE parent_id IS NULL
+ UNION ALL
+ SELECT n.id, n.parent_id, t.depth + 1
+ FROM nodes n
+ JOIN tree t ON n.parent_id = t.id
+)
+SELECT id, parent_id, depth FROM tree ORDER BY id;
+----
+1 NULL 0
+2 1 1
+3 1 1
+4 2 2
+5 2 2
+6 3 2
+10 NULL 0
+11 10 1
+
+# Start from an ordinary row rather than a root and stop when no next rows
exist.
+query III
+WITH RECURSIVE tree(id, parent_id, depth) AS (
+ SELECT id, parent_id, 0
+ FROM nodes
+ WHERE id = 2
+ UNION ALL
+ SELECT n.id, n.parent_id, t.depth + 1
+ FROM nodes n
+ JOIN tree t ON n.parent_id = t.id
+)
+SELECT id, parent_id, depth FROM tree ORDER BY id;
+----
+2 1 0
+4 2 1
+5 2 1
+
+# An empty seed produces an empty result and does not start an iteration.
+query I
+WITH RECURSIVE tree(id) AS (
+ SELECT id
+ FROM nodes
+ WHERE id < 0
+ UNION ALL
+ SELECT n.id
+ FROM nodes n
+ JOIN tree t ON n.parent_id = t.id
+)
+SELECT id FROM tree;
+----
+
+# End of test.
diff --git
a/modules/calcite/src/test/sql/hierarchy/test_recursive_sequence.test
b/modules/calcite/src/test/sql/hierarchy/test_recursive_sequence.test
new file mode 100644
index 00000000000..6d8e15c91a6
--- /dev/null
+++ b/modules/calcite/src/test/sql/hierarchy/test_recursive_sequence.test
@@ -0,0 +1,62 @@
+# name: test/sql/hierarchy/test_recursive_sequence.test
+# description: Test recursive CTEs used to generate integer sequences
+# group: [hierarchy]
+
+# Fixed recursion bound.
+query I
+WITH RECURSIVE numbers(n) AS (
+ SELECT 1
+ UNION ALL
+ SELECT n + 1
+ FROM numbers
+ WHERE n < 5
+)
+SELECT n FROM numbers ORDER BY n;
+----
+1
+2
+3
+4
+5
+
+statement ok
+CREATE TABLE ranges (id INTEGER PRIMARY KEY, last_value INTEGER);
+
+statement ok
+INSERT INTO ranges VALUES (1, 3), (2, 2);
+
+# The seed contains several rows and every row has its own recursion bound.
+query II
+WITH RECURSIVE numbers(id, n, last_value) AS (
+ SELECT id, 1, last_value
+ FROM ranges
+ UNION ALL
+ SELECT id, n + 1, last_value
+ FROM numbers
+ WHERE n < last_value
+)
+SELECT id, n FROM numbers ORDER BY id, n;
+----
+1 1
+1 2
+1 3
+2 1
+2 2
+
+# A predicate outside the CTE filters rows after the recursion is complete.
+query I
+WITH RECURSIVE numbers(n) AS (
+ SELECT 1
+ UNION ALL
+ SELECT n + 1
+ FROM numbers
+ WHERE n < 6
+)
+SELECT n
+FROM numbers
+WHERE n >= 4
+ORDER BY n;
+----
+4
+5
+6
diff --git
a/modules/calcite/src/test/sql/hierarchy/test_recursive_subquery.test
b/modules/calcite/src/test/sql/hierarchy/test_recursive_subquery.test
new file mode 100644
index 00000000000..092eb389cc2
--- /dev/null
+++ b/modules/calcite/src/test/sql/hierarchy/test_recursive_subquery.test
@@ -0,0 +1,54 @@
+# name: test/sql/hierarchy/test_recursive_subquery.test
+# description: Test recursive CTEs with source CTEs and subqueries
+# group: [hierarchy]
+
+statement ok
+CREATE TABLE groups (id INTEGER PRIMARY KEY, parent_id INTEGER);
+
+statement ok
+INSERT INTO groups VALUES (1, NULL), (2, 1), (3, 1), (4, 2), (5, NULL);
+
+statement ok
+CREATE TABLE items (id INTEGER PRIMARY KEY, group_id INTEGER);
+
+statement ok
+INSERT INTO items VALUES (100, 1), (200, 2), (300, 3), (400, 4), (500, 5);
+
+# A non-recursive CTE supplies rows to the seed of a recursive CTE.
+query II
+WITH RECURSIVE roots(id) AS (
+ SELECT id FROM groups WHERE id = 2
+), descendants(id, depth) AS (
+ SELECT id, 0 FROM roots
+ UNION ALL
+ SELECT g.id, d.depth + 1
+ FROM groups g
+ JOIN descendants d ON g.parent_id = d.id
+)
+SELECT id, depth FROM descendants ORDER BY id;
+----
+2 0
+4 1
+
+# The complete recursive query is used as an EXISTS subquery and correlated
with the outer row.
+query I
+SELECT i.id
+FROM items i
+WHERE EXISTS (
+ WITH RECURSIVE descendants(id) AS (
+ SELECT id FROM groups WHERE id = 1
+ UNION ALL
+ SELECT g.id
+ FROM groups g
+ JOIN descendants d ON g.parent_id = d.id
+ )
+ SELECT 1
+ FROM descendants d
+ WHERE d.id = i.group_id
+)
+ORDER BY i.id;
+----
+100
+200
+300
+400