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 1df6a4efe7e IGNITE-29012 SQL Calcite: Support recursive CTE with UNION
DISTINCT (#13563)
1df6a4efe7e is described below
commit 1df6a4efe7ea19a75070195c3ec254d65576f89b
Author: Vladislav Pyatkov <[email protected]>
AuthorDate: Wed Sep 9 22:11:53 2026 +0300
IGNITE-29012 SQL Calcite: Support recursive CTE with UNION DISTINCT (#13563)
---
.../query/calcite/exec/LogicalRelImplementor.java | 2 +-
.../query/calcite/exec/rel/RecursiveCteState.java | 56 +++++++++++++-----
.../query/calcite/exec/rel/RepeatUnionNode.java | 69 +++++++++++-----------
.../query/calcite/rel/IgniteRepeatUnion.java | 10 ++--
.../calcite/rule/RepeatUnionConverterRule.java | 5 +-
.../integration/RecursiveCteIntegrationTest.java | 55 ++++++++++++-----
.../calcite/planner/RecursiveCtePlannerTest.java | 56 ++++++++++++++++--
7 files changed, 172 insertions(+), 81 deletions(-)
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 e2777240471..7b2172c33ac 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
@@ -646,7 +646,7 @@ public class LogicalRelImplementor<Row> implements
IgniteRelVisitor<Node<Row>> {
/** {@inheritDoc} */
@Override public Node<Row> visit(IgniteRepeatUnion rel) {
- RepeatUnionNode<Row> node = new RepeatUnionNode<>(ctx,
rel.getRowType(), rel.iterationLimit());
+ RepeatUnionNode<Row> node = new RepeatUnionNode<>(ctx,
rel.getRowType(), rel.all, rel.iterationLimit());
node.register(F.asList(visit(rel.getLeft()), visit(rel.getRight())));
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
index 8a865164de8..83bc7d15ef2 100644
---
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
@@ -19,18 +19,32 @@ package
org.apache.ignite.internal.processors.query.calcite.exec.rel;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
+import
org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.GroupKey;
import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.RowTracker;
import org.apache.ignite.internal.util.GridUnsafe;
+import org.jetbrains.annotations.Nullable;
/** Query-local current and next deltas of a recursive CTE. */
public class RecursiveCteState<Row> {
+ /** Rows seen across all iterations, or null for UNION ALL. */
+ private final @Nullable Set<GroupKey<Row>> seen;
+
+ /** Row handler used for SQL grouping keys. */
+ private final RowHandler<Row> hnd;
+
+ /** Memory tracker for keys and their rows retained for duplicate
elimination. */
+ private final @Nullable RowTracker<GroupKey<Row>> seenMemoryTracker;
+
/** 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;
+ private List<Row> next = new ArrayList<>();
/** Memory tracker for rows in the current delta. */
private RowTracker<Row> curMemoryTracker;
@@ -39,33 +53,38 @@ public class RecursiveCteState<Row> {
private RowTracker<Row> nextMemoryTracker;
/** */
- public RecursiveCteState(ExecutionContext<Row> ctx) {
+ public RecursiveCteState(ExecutionContext<Row> ctx, boolean all) {
+ seen = all ? null : new HashSet<>();
+ hnd = ctx.rowHandler();
+ seenMemoryTracker = all ? null :
ctx.createNodeMemoryTracker(MemoryTrackingNode.HASH_MAP_ROW_OVERHEAD);
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;
+ /** Adds a new row to the next delta, returning false for duplicates in
DISTINCT mode. */
+ public boolean add(Row row) {
+ if (seen != null) {
+ GroupKey<Row> rowKey = GroupKey.of(row, hnd);
- next = new ArrayList<>();
- }
+ if (!seen.add(rowKey))
+ return false;
- /** Adds a row to the next delta. */
- public void add(Row row) {
- assert next != null;
+ seenMemoryTracker.onRowAdded(rowKey);
+ }
next.add(row);
- nextMemoryTracker.onRowAdded(row);
+
+ // DISTINCT already accounts for the row in seen; null charges only
the delta's reference overhead.
+ nextMemoryTracker.onRowAdded(seen == null ? row : null);
+
+ return true;
}
- /** Makes the collected delta visible to recursive scans. */
+ /** Publishes the collected delta and prepares an empty buffer for the
next iteration. */
public void commit() {
- assert next != null;
-
curMemoryTracker.reset();
cur = next;
- next = null;
+ next = new ArrayList<>();
RowTracker<Row> tracker = curMemoryTracker;
@@ -86,9 +105,14 @@ public class RecursiveCteState<Row> {
/** Clears all query-local rows. */
public void clear() {
cur = Collections.emptyList();
- next = null;
+ next = new ArrayList<>();
curMemoryTracker.reset();
nextMemoryTracker.reset();
+
+ if (seen != null) {
+ seen.clear();
+ seenMemoryTracker.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
index 9586b91a362..8e66d2ece09 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/RepeatUnionNode.java
@@ -26,7 +26,7 @@ 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. */
+/** Coordinator-side executor for recursive union. */
public class RepeatUnionNode<Row> extends AbstractNode<Row> implements
Downstream<Row> {
/** Index of the seed input. */
private static final int SEED_SOURCE = 0;
@@ -46,21 +46,22 @@ public class RepeatUnionNode<Row> extends AbstractNode<Row>
implements Downstrea
/** Number of rows still requested by downstream. */
private int waiting;
+ /** Number of rows still requested from the active source. */
+ private int pending;
+
/** 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,
+ boolean all,
int iterationLimit
) {
super(ctx, rowType);
- state = new RecursiveCteState<>(ctx);
+ state = new RecursiveCteState<>(ctx, all);
this.iterationLimit = iterationLimit;
}
@@ -87,27 +88,31 @@ public class RepeatUnionNode<Row> extends AbstractNode<Row>
implements Downstrea
/** {@inheritDoc} */
@Override public void push(Row row) throws Exception {
assert downstream() != null;
- assert waiting > 0;
- assert writing;
+ assert waiting > 0 && pending > 0 :
+ "Received a row without outstanding demand [waiting=" + waiting +
", pending=" + pending + ']';
checkState();
- waiting--;
- state.add(row);
+ pending--;
+
+ if (state.add(row)) {
+ waiting--;
+ downstream().push(row);
+ }
- downstream().push(row);
+ if (pending == 0 && waiting > 0)
+ context().execute(this::requestSource, this::onError);
}
/** {@inheritDoc} */
@Override public void end() throws Exception {
assert downstream() != null;
assert waiting > 0;
- assert writing;
checkState();
+ pending = 0;
state.commit();
- writing = false;
if (state.isEmpty()) {
finish();
@@ -115,25 +120,19 @@ public class RepeatUnionNode<Row> extends
AbstractNode<Row> implements Downstrea
return;
}
- if (curSrc == SEED_SOURCE) {
- if (iterationLimit == 0) {
- throw iterationLimitExceeded();
- }
-
- curSrc = RECURSIVE_SOURCE;
- requestSource();
-
- return;
- }
-
- iteration++;
+ if (curSrc == RECURSIVE_SOURCE)
+ iteration++;
- if (iterationLimit >= 0 && iteration == iterationLimit) {
+ if (iterationLimit >= 0 && iteration == iterationLimit)
throw iterationLimitExceeded();
- }
- source().rewind();
- requestSource();
+ if (curSrc == SEED_SOURCE)
+ curSrc = RECURSIVE_SOURCE;
+ else
+ source().rewind();
+
+ // Let the previous scan leave its push loop before requesting the
next iteration.
+ context().execute(this::requestSource, this::onError);
}
/** {@inheritDoc} */
@@ -152,8 +151,8 @@ public class RepeatUnionNode<Row> extends AbstractNode<Row>
implements Downstrea
@Override protected void rewindInternal() {
curSrc = SEED_SOURCE;
waiting = 0;
+ pending = 0;
iteration = 0;
- writing = false;
state.clear();
}
@@ -187,14 +186,14 @@ public class RepeatUnionNode<Row> extends
AbstractNode<Row> implements Downstrea
}
}
- /** Starts collecting and requests rows from the active input. */
+ /** Requests the remaining downstream demand from the active input. */
private void requestSource() throws Exception {
- if (!writing) {
- state.beginWrite();
- writing = true;
- }
+ checkState();
+
+ assert pending == 0 : "Cannot request more rows while the source
request is pending [waiting=" + waiting +
+ ", pending=" + pending + ']';
- source().request(waiting);
+ source().request(pending = waiting);
}
/** */
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
index a5689f2dbe6..b89123eff71 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRepeatUnion.java
@@ -24,7 +24,7 @@ import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.RepeatUnion;
-/** Coordinator-side iterative UNION ALL for a recursive CTE. */
+/** Coordinator-side iterative union for a recursive CTE. */
public class IgniteRepeatUnion extends RepeatUnion implements IgniteRel {
/** */
public IgniteRepeatUnion(
@@ -32,9 +32,10 @@ public class IgniteRepeatUnion extends RepeatUnion
implements IgniteRel {
RelTraitSet traits,
RelNode seed,
RelNode iterative,
+ boolean all,
int iterationLimit
) {
- super(cluster, traits, seed, iterative, true, iterationLimit, null);
+ super(cluster, traits, seed, iterative, all, iterationLimit, null);
}
/** Constructor used for deserialization. */
@@ -44,6 +45,7 @@ public class IgniteRepeatUnion extends RepeatUnion implements
IgniteRel {
input.getTraitSet().replace(IgniteConvention.INSTANCE),
input.getInputs().get(0),
input.getInputs().get(1),
+ input.getBoolean("all", true),
iterationLimit(input)
);
}
@@ -57,7 +59,7 @@ public class IgniteRepeatUnion extends RepeatUnion implements
IgniteRel {
@Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
assert inputs.size() == 2;
- return new IgniteRepeatUnion(getCluster(), traitSet, inputs.get(0),
inputs.get(1), iterationLimit);
+ return new IgniteRepeatUnion(getCluster(), traitSet, inputs.get(0),
inputs.get(1), all, iterationLimit);
}
/** {@inheritDoc} */
@@ -69,7 +71,7 @@ public class IgniteRepeatUnion extends RepeatUnion implements
IgniteRel {
@Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel>
inputs) {
assert inputs.size() == 2;
- return new IgniteRepeatUnion(cluster, getTraitSet(), inputs.get(0),
inputs.get(1), iterationLimit);
+ return new IgniteRepeatUnion(cluster, getTraitSet(), inputs.get(0),
inputs.get(1), all, iterationLimit);
}
/** Reads the optional iteration limit from a serialized plan. */
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
index ef37d92cbb0..a19f073c003 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RepeatUnionConverterRule.java
@@ -53,10 +53,6 @@ public class RepeatUnionConverterRule extends
AbstractIgniteConverterRule<Logica
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");
-
int iterationLimit =
planner.getContext().unwrap(PlanningContext.class).recursiveCteIterationLimit();
RelNode seed = unwrapSpool(rel.getSeedRel(), "seed");
@@ -71,6 +67,7 @@ public class RepeatUnionConverterRule extends
AbstractIgniteConverterRule<Logica
traits,
convert(seed, traits),
convert(iterative, traits),
+ rel.all,
iterationLimit
);
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/RecursiveCteIntegrationTest.java
index 8989bad03de..5352cdc3f40 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
@@ -51,9 +51,12 @@ public class RecursiveCteIntegrationTest extends
AbstractBasicIntegrationTest {
.returns(3)
.check();
- assertThrows("WITH " + keyword + "numbers(n) AS (" +
- "SELECT 1 UNION SELECT n + 1 FROM numbers WHERE n < 3) SELECT
* FROM numbers",
- IgniteSQLException.class, "only UNION ALL is supported");
+ assertQuery("WITH " + keyword + "numbers(n) AS (" +
+ "SELECT 1 UNION SELECT n + 1 FROM numbers WHERE n < 3) SELECT
* FROM numbers")
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .check();
}
}
@@ -244,19 +247,41 @@ public class RecursiveCteIntegrationTest extends
AbstractBasicIntegrationTest {
.check();
}
- /** */
+ /** Both DISTINCT spellings eliminate duplicates in the seed and across
recursive iterations. */
@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,
- "only UNION ALL is supported"
- );
+ public void testRecursiveCteWithDistinctUnion() {
+ for (String union : new String[] {"UNION", "UNION DISTINCT"}) {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT * FROM (VALUES (1), (1), (2)) " + union + " " +
+ "SELECT MOD(n, 3) + 1 FROM numbers" +
+ ") SELECT n FROM numbers")
+ .returns(1)
+ .returns(2)
+ .returns(3)
+ .check();
+ }
+ }
+
+ /** NULLs compare equal and all columns participate in duplicate
elimination. */
+ @Test
+ public void testRecursiveDistinctNulls() {
+ assertQuery("WITH RECURSIVE numbers(n, label) AS (" +
+ "SELECT * FROM (VALUES (1, CAST(NULL AS VARCHAR)), (1, CAST(NULL
AS VARCHAR)), (1, 'x')) " +
+ "UNION DISTINCT SELECT n, label FROM numbers" +
+ ") SELECT n, label FROM numbers")
+ .returns(1, null)
+ .returns(1, "x")
+ .check();
+ }
+
+ /** Duplicate-only input batches must keep requesting rows until the
source ends. */
+ @Test
+ public void testRecursiveDistinctLargeDuplicateBatch() {
+ assertQuery("WITH RECURSIVE numbers(n) AS (" +
+ "SELECT 1 UNION SELECT n FROM numbers CROSS JOIN
TABLE(SYSTEM_RANGE(1, 10000))" +
+ ") SELECT n FROM numbers")
+ .returns(1)
+ .check();
}
/** */
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
index 769b8233558..66c687d631a 100644
---
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
@@ -26,6 +26,9 @@ import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlWithItem;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.calcite.tools.ValidationException;
+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.exp.IgniteScalarFunction;
import
org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePlanner;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
@@ -37,6 +40,7 @@ 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.apache.ignite.testframework.GridTestUtils;
import org.junit.Test;
/** Planner tests for recursive common table expressions. */
@@ -96,11 +100,15 @@ public class RecursiveCtePlannerTest extends
AbstractPlannerTest {
assertRecursiveFlags("WITH \"Numbers\"(n) AS (SELECT 1 UNION ALL " +
"SELECT n + 1 FROM \"Numbers\" WHERE n < 3) SELECT * FROM
\"Numbers\"", true);
+ }
- assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
+ /** Subquery self-references are inferred as recursive, but rejected
during validation. */
+ @Test
+ public void testUnsupportedSubqueryRecursiveFlags() throws Exception {
+ assertUnsupportedRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION
ALL " +
"SELECT n + 1 FROM (SELECT * FROM numbers) x WHERE n < 3) SELECT *
FROM numbers", true);
- assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
+ assertUnsupportedRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION
ALL " +
"SELECT (SELECT n + 1 FROM numbers WHERE n < 3)) SELECT * FROM
numbers", true);
}
@@ -139,22 +147,36 @@ public class RecursiveCtePlannerTest extends
AbstractPlannerTest {
assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
"SELECT * FROM (WITH numbers(n) AS (SELECT 2 UNION ALL SELECT n +
1 FROM numbers WHERE n < 3) " +
"SELECT * FROM numbers)) SELECT * FROM numbers", false, true);
+ }
- assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
+ /** References to an outer recursive CTE from nested WITH queries are
inferred, but unsupported. */
+ @Test
+ public void testUnsupportedNestedCteFlags() throws Exception {
+ assertUnsupportedRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION
ALL " +
"SELECT n + 1 FROM (WITH numbers(n) AS (SELECT n FROM numbers
WHERE n < 3) " +
"SELECT * FROM numbers)) SELECT * FROM numbers", true, false);
- assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
+ assertUnsupportedRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION
ALL " +
"SELECT n + 1 FROM (WITH x AS (SELECT n FROM numbers WHERE n < 3),
numbers(n) AS (SELECT 9) " +
"SELECT * FROM x)) SELECT * FROM numbers", true, false, false);
- assertRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION ALL " +
+ assertUnsupportedRecursiveFlags("WITH numbers(n) AS (SELECT 1 UNION
ALL " +
"SELECT n + 1 FROM (WITH numbers(n) AS (SELECT n FROM numbers
WHERE n < 2 UNION ALL " +
"SELECT n + 1 FROM numbers WHERE n < 2) SELECT * FROM numbers))
SELECT * FROM numbers", true, true);
}
/** Validates real SQL and checks flags on parsed WITH items in pre-order.
*/
private void assertRecursiveFlags(String sql, boolean... expected) throws
Exception {
+ assertRecursiveFlags(sql, expected, false);
+ }
+
+ /** Checks the inferred flags and the validation error for unsupported
recursive references. */
+ private void assertUnsupportedRecursiveFlags(String sql, boolean...
expected) throws Exception {
+ assertRecursiveFlags(sql, expected, true);
+ }
+
+ /** Checks the validation outcome and flags on parsed WITH items in
pre-order. */
+ private void assertRecursiveFlags(String sql, boolean[] expected, boolean
unsupported) throws Exception {
IgniteSchema schema = createSchema(createTable("NUMBERS",
IgniteDistributions.single(), "N", Integer.class));
try (IgnitePlanner planner = plannerCtx(sql, schema).planner()) {
@@ -171,7 +193,17 @@ public class RecursiveCtePlannerTest extends
AbstractPlannerTest {
}
});
- planner.validate(node);
+ if (unsupported) {
+ ValidationException err =
(ValidationException)GridTestUtils.assertThrows(log,
+ () -> planner.validate(node), ValidationException.class,
+ "Unsupported recursive CTE: self-references inside
subqueries are not supported");
+
+ assertTrue(sql, err.getCause() instanceof IgniteSQLException);
+ assertEquals(sql, IgniteQueryErrorCode.UNSUPPORTED_OPERATION,
+ ((IgniteSQLException)err.getCause()).statusCode());
+ }
+ else
+ planner.validate(node);
assertEquals(sql, expected.length, items.size());
@@ -180,6 +212,18 @@ public class RecursiveCtePlannerTest extends
AbstractPlannerTest {
}
}
+ /** DISTINCT semantics survive conversion and plan serialization. */
+ @Test
+ public void testRecursiveDistinctPlan() throws Exception {
+ for (String union : new String[] {"UNION", "UNION DISTINCT"}) {
+ assertPlan("WITH RECURSIVE numbers(n) AS (SELECT 1 " + union +
+ " SELECT n FROM numbers) SELECT n FROM numbers",
+ new IgniteSchema(DEFAULT_SCHEMA),
isInstanceOf(IgniteRepeatUnion.class)
+ .and(rel -> !rel.all)
+ .and(hasDistribution(IgniteDistributions.single())));
+ }
+ }
+
/** A replicated source can be read on the coordinator without an
exchange. */
@Test
public void testRecursiveCteWithReplicatedTable() throws Exception {