diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000000..bd98446bc0
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+fdb-record-layer
\ No newline at end of file
diff --git a/.swo b/.swo
new file mode 100644
index 0000000000..4bc239d5c4
Binary files /dev/null and b/.swo differ
diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html
new file mode 100644
index 0000000000..9545a66c37
--- /dev/null
+++ b/build/reports/problems/problems-report.html
@@ -0,0 +1,663 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gradle Configuration Cache
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/cursors/RecursiveCursor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/cursors/RecursiveCursor.java
new file mode 100644
index 0000000000..92289a5cd0
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/cursors/RecursiveCursor.java
@@ -0,0 +1,458 @@
+/*
+ * RecursiveCursor.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.cursors;
+
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.async.AsyncUtil;
+import com.apple.foundationdb.record.ByteArrayContinuation;
+import com.apple.foundationdb.record.RecordCoreException;
+import com.apple.foundationdb.record.RecordCursor;
+import com.apple.foundationdb.record.RecordCursorContinuation;
+import com.apple.foundationdb.record.RecordCursorProto;
+import com.apple.foundationdb.record.RecordCursorResult;
+import com.apple.foundationdb.record.RecordCursorStartContinuation;
+import com.apple.foundationdb.record.RecordCursorVisitor;
+import com.apple.foundationdb.tuple.ByteArrayUtil2;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ZeroCopyByteString;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * A cursor that flattens a tree of cursors.
+ * A root cursor seeds the output and then each output element is additionally mapped into a child cursor.
+ * @param the type of elements of the cursors
+ */
+@API(API.Status.EXPERIMENTAL)
+@SuppressWarnings("PMD.CloseResource")
+public class RecursiveCursor implements RecordCursor> {
+
+ @Nonnull
+ private final ChildCursorFunction childCursorFunction;
+ @Nullable
+ private final Function checkValueFunction;
+ @Nonnull
+ private final List> nodes;
+
+ private int currentDepth;
+
+ @Nullable
+ private RecordCursorResult> lastResult;
+
+ private RecursiveCursor(@Nonnull ChildCursorFunction childCursorFunction,
+ @Nullable Function checkValueFunction,
+ @Nonnull List> nodes) {
+ this.childCursorFunction = childCursorFunction;
+ this.checkValueFunction = checkValueFunction;
+ this.nodes = nodes;
+ }
+
+ /**
+ * Create a recursive cursor.
+ * @param rootCursorFunction a function that given a continuation or {@code null} returns the children of the root
+ * @param childCursorFunction a function that given a value and a continuation returns the children of that level
+ * @param checkValueFunction a function to recognize changes to the database since a continuation
+ * @param continuation a continuation from a previous cursor
+ * @param the type of elements of the cursors
+ * @return a cursor over the recursive tree determined by the cursor functions
+ */
+ @Nonnull
+ public static RecursiveCursor create(@Nonnull Function> rootCursorFunction,
+ @Nonnull ChildCursorFunction childCursorFunction,
+ @Nullable Function checkValueFunction,
+ @Nullable byte[] continuation) {
+ final List> nodes = new ArrayList<>();
+ if (continuation == null) {
+ nodes.add(RecursiveNode.forRoot(RecordCursorStartContinuation.START, rootCursorFunction.apply(null)));
+ } else {
+ RecordCursorProto.RecursiveContinuation parsed;
+ try {
+ parsed = RecordCursorProto.RecursiveContinuation.parseFrom(continuation);
+ } catch (InvalidProtocolBufferException ex) {
+ throw new RecordCoreException("error parsing continuation", ex)
+ .addLogInfo("raw_bytes", ByteArrayUtil2.loggable(continuation));
+ }
+ final int totalDepth = parsed.getLevelsCount();
+ for (int depth = 0; depth < totalDepth; depth++) {
+ final RecordCursorProto.RecursiveContinuation.LevelCursor parentLevel = parsed.getLevels(depth);
+ final RecordCursorContinuation priorContinuation;
+ if (parentLevel.hasContinuation()) {
+ priorContinuation = ByteArrayContinuation.fromNullable(parentLevel.getContinuation().toByteArray());
+ } else {
+ priorContinuation = RecordCursorStartContinuation.START;
+ }
+ if (depth == 0) {
+ nodes.add(RecursiveNode.forRoot(priorContinuation, rootCursorFunction.apply(priorContinuation.toBytes())));
+ } else {
+ byte[] checkValue = null;
+ if (checkValueFunction != null && depth < totalDepth - 1) {
+ final RecordCursorProto.RecursiveContinuation.LevelCursor childLevel = parsed.getLevels(depth + 1);
+ if (childLevel.hasCheckValue()) {
+ checkValue = childLevel.getCheckValue().toByteArray();
+ }
+ }
+ nodes.add(RecursiveNode.forContinuation(priorContinuation, checkValue));
+ }
+ }
+ }
+ return new RecursiveCursor<>(childCursorFunction, checkValueFunction, nodes);
+ }
+
+ @Nonnull
+ @Override
+ public CompletableFuture>> onNext() {
+ if (lastResult != null && !lastResult.hasNext()) {
+ return CompletableFuture.completedFuture(lastResult);
+ }
+ return AsyncUtil.whileTrue(this::recursionLoop, getExecutor()).thenApply(vignore -> lastResult);
+ }
+
+ @Override
+ public void close() {
+ for (RecursiveNode node : nodes) {
+ CompletableFuture> childFuture = node.childFuture;
+ if (childFuture != null) {
+ if (childFuture.cancel(false)) {
+ node.childFuture = null;
+ } else {
+ continue;
+ }
+ }
+ RecordCursor childCursor = node.childCursor;
+ if (childCursor != null) {
+ childCursor.close();
+ }
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ for (RecursiveNode node : nodes) {
+ if (node.childFuture != null) {
+ return false;
+ }
+ RecordCursor childCursor = node.childCursor;
+ if (childCursor != null && !childCursor.isClosed()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Nonnull
+ @Override
+ public Executor getExecutor() {
+ return nodes.get(0).childCursor.getExecutor(); // Take from the root cursor.
+ }
+
+ @Override
+ public boolean accept(@Nonnull RecordCursorVisitor visitor) {
+ if (visitor.visitEnter(this)) {
+ for (RecursiveNode node : nodes) {
+ RecordCursor childCursor = node.childCursor;
+ if (childCursor != null) {
+ childCursor.accept(visitor);
+ }
+ }
+ }
+ return visitor.visitLeave(this);
+ }
+
+ /**
+ * A value returned by a recursive descent.
+ * Includes the depth level and whether there are any descendants.
+ * @param the type of elements of the cursors
+ */
+ public static class RecursiveValue {
+ @Nullable
+ private final T value;
+ private final int depth;
+ private final boolean isLeaf;
+
+ public RecursiveValue(@Nullable T value, int depth, boolean isLeaf) {
+ this.value = value;
+ this.depth = depth;
+ this.isLeaf = isLeaf;
+ }
+
+ /**
+ * Get the value for a recursive result.
+ * @return the value associated with the given result
+ */
+ @Nullable
+ public T getValue() {
+ return value;
+ }
+
+ /**
+ * Get the depth for a recursive result.
+ * @return the 1-based depth from the root of the given result
+ */
+ public int getDepth() {
+ return depth;
+ }
+
+ /**
+ * Get whether a recursive result has any descendants.
+ * @return {@code true} if the given result is a leaf, {@code false} if it has any descendants
+ */
+ public boolean isLeaf() {
+ return isLeaf;
+ }
+ }
+
+ /**
+ * Function to generate children of a parent value.
+ * @param the type of elements of the cursors
+ */
+ @FunctionalInterface
+ public interface ChildCursorFunction {
+ /**
+ * Return recursion cursor for this level.
+ * @param value the value at this level
+ * @param depth the 1-based depth of this level
+ * @param continuation an optional continuation
+ * @return a cursor over children of the given value for the given level
+ */
+ RecordCursor apply(@Nullable T value, int depth, @Nullable byte[] continuation);
+ }
+
+ // This implementation keeps a single stack of open cursors and returns in pre-order.
+ // A child is opened and the first element checked in order to be able to include whether a node is a leaf in each result.
+ // It would also be possible to keep a tree of open cursors, opening more children before completing their siblings.
+ // It would also be possible to only have a single open cursor and return in level-order.
+ // These alternatives have even more complicated continuation restoring behavior, though.
+
+ static class RecursiveNode {
+ @Nullable
+ final T value;
+ @Nullable
+ final byte[] checkValue;
+
+ boolean emitPending;
+
+ @Nonnull
+ RecordCursorContinuation childContinuationBefore;
+ @Nonnull
+ RecordCursorContinuation childContinuationAfter;
+ @Nullable
+ RecordCursor childCursor;
+ @Nullable
+ CompletableFuture> childFuture;
+
+ private RecursiveNode(@Nullable T value, @Nullable byte[] checkValue, boolean emitPending,
+ @Nonnull RecordCursorContinuation childContinuationBefore, @Nullable RecordCursor childCursor) {
+ this.value = value;
+ this.checkValue = checkValue;
+ this.emitPending = emitPending;
+ this.childContinuationAfter = this.childContinuationBefore = childContinuationBefore;
+ this.childCursor = childCursor;
+ }
+
+ static RecursiveNode forRoot(@Nonnull RecordCursorContinuation childContinuationBefore,
+ @Nonnull RecordCursor childCursor) {
+ return new RecursiveNode<>(null, null, false, childContinuationBefore, childCursor);
+ }
+
+ static RecursiveNode forValue(@Nullable T value) {
+ return new RecursiveNode<>(value, null, true, RecordCursorStartContinuation.START, null);
+ }
+
+ public static RecursiveNode forContinuation(@Nonnull RecordCursorContinuation childContinuationBefore,
+ @Nullable byte[] checkValue) {
+ return new RecursiveNode<>(null, checkValue, false, childContinuationBefore, null);
+ }
+
+ public RecursiveNode withCheckedValue(@Nullable T value) {
+ return new RecursiveNode<>(value, null, false, childContinuationBefore, null);
+ }
+ }
+
+ /**
+ * Called to advance the recursion.
+ * @return a future that completes to {@code true} if the loop should continue or {@code false} if a result is available
+ */
+ @Nonnull
+ CompletableFuture recursionLoop() {
+ int depth = currentDepth;
+ final RecursiveNode node = nodes.get(depth);
+ if (node.childFuture == null) {
+ if (node.childCursor == null) {
+ node.childCursor = childCursorFunction.apply(node.value, depth, node.childContinuationBefore.toBytes());
+ }
+ node.childFuture = node.childCursor.onNext();
+ }
+ if (!node.childFuture.isDone()) {
+ return node.childFuture.thenApply(rignore -> true);
+ }
+ final RecordCursorResult childResult = node.childFuture.join();
+ node.childFuture = null;
+ if (childResult.hasNext()) {
+ node.childContinuationAfter = childResult.getContinuation();
+ addChildNode(childResult.get());
+ if (node.emitPending) {
+ lastResult = RecordCursorResult.withNextValue(
+ new RecursiveValue<>(node.value, depth, false),
+ buildContinuation(depth));
+ node.emitPending = false;
+ return AsyncUtil.READY_FALSE;
+ }
+ } else {
+ final NoNextReason noNextReason = childResult.getNoNextReason();
+ if (noNextReason.isOutOfBand()) {
+ final RecordCursorContinuation continuation;
+ if (node.emitPending) {
+ // Stop before parent.
+ continuation = buildContinuation(depth - 1);
+ } else {
+ // Stop before child.
+ continuation = buildContinuation(depth);
+ }
+ lastResult = RecordCursorResult.withoutNextValue(continuation, noNextReason);
+ return AsyncUtil.READY_FALSE;
+ }
+ // If the childCursorFunction added a returned row limit, that is not distinguished here.
+ // There is no provision for continuing and adding more descendants at an arbitrary depth.
+ while (nodes.size() > depth) {
+ nodes.remove(depth);
+ }
+ currentDepth = depth - 1;
+ if (depth == 0) {
+ lastResult = RecordCursorResult.exhausted();
+ return AsyncUtil.READY_FALSE;
+ }
+ final RecursiveNode parentNode = nodes.get(depth - 1);
+ parentNode.childContinuationBefore = parentNode.childContinuationAfter;
+ if (node.emitPending) {
+ lastResult = RecordCursorResult.withNextValue(
+ new RecursiveValue<>(node.value, depth, true),
+ buildContinuation(depth));
+ node.emitPending = false;
+ return AsyncUtil.READY_FALSE;
+ }
+ }
+ return AsyncUtil.READY_TRUE;
+ }
+
+ private void addChildNode(@Nullable T value) {
+ currentDepth++;
+ if (currentDepth < nodes.size()) {
+ // Have a nested continuation.
+ final RecursiveNode continuationChildNode = nodes.get(currentDepth);
+ boolean addNode = false;
+ if (checkValueFunction != null && continuationChildNode.checkValue != null) {
+ final byte[] actualCheckValue = checkValueFunction.apply(value);
+ if (actualCheckValue != null && !Arrays.equals(continuationChildNode.checkValue, actualCheckValue)) {
+ // Does not match; discard proposed continuation(s).
+ while (nodes.size() > currentDepth) {
+ nodes.remove(currentDepth);
+ }
+ addNode = true;
+ }
+ }
+ if (!addNode) {
+ // Replace check value with actual value so can open cursors below there, but using the loaded continuation.
+ nodes.set(currentDepth, continuationChildNode.withCheckedValue(value));
+ return;
+ }
+ }
+ final RecursiveNode childNode = RecursiveNode.forValue(value);
+ nodes.add(childNode);
+ }
+
+ private RecordCursorContinuation buildContinuation(int depth) {
+ final List continuations = new ArrayList<>(depth);
+ final List checkValues = checkValueFunction == null ? null : new ArrayList<>(depth - 1);
+ for (int i = 0; i < depth; i++) {
+ continuations.add(nodes.get(i).childContinuationBefore);
+ if (checkValues != null && i < depth - 1) {
+ checkValues.add(checkValueFunction.apply(nodes.get(i + 1).value));
+ }
+ }
+ return new Continuation(continuations, checkValues);
+ }
+
+ private static class Continuation implements RecordCursorContinuation {
+ @Nonnull
+ private final List continuations;
+ @Nullable
+ private final List checkValues;
+ @Nullable
+ private ByteString cachedByteString;
+ @Nullable
+ private byte[] cachedBytes;
+
+ private Continuation(@Nonnull List continuations, @Nullable List checkValues) {
+ this.continuations = continuations;
+ this.checkValues = checkValues;
+ }
+
+ @Override
+ public boolean isEnd() {
+ for (RecordCursorContinuation continuation : continuations) {
+ if (!continuation.isEnd()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Nonnull
+ @Override
+ public ByteString toByteString() {
+ if (cachedByteString == null) {
+ final RecordCursorProto.RecursiveContinuation.Builder builder = RecordCursorProto.RecursiveContinuation.newBuilder();
+ for (int i = 0; i < continuations.size(); i++) {
+ final RecordCursorProto.RecursiveContinuation.LevelCursor.Builder levelBuilder = builder.addLevelsBuilder();
+ final RecordCursorContinuation continuation = continuations.get(i);
+ if (continuation.toBytes() != null) {
+ levelBuilder.setContinuation(continuation.toByteString());
+ }
+ if (checkValues != null && i < checkValues.size()) {
+ final byte[] checkValue = checkValues.get(i);
+ if (checkValue != null) {
+ levelBuilder.setCheckValue(ZeroCopyByteString.wrap(checkValue));
+ }
+ }
+ }
+ cachedByteString = builder.build().toByteString();
+ }
+ return cachedByteString;
+ }
+
+ @Nullable
+ @Override
+ public byte[] toBytes() {
+ if (cachedBytes == null) {
+ cachedBytes = toByteString().toByteArray();
+ }
+ return cachedBytes;
+ }
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanningRuleSet.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanningRuleSet.java
index 23f92b270a..2a0382c88f 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanningRuleSet.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanningRuleSet.java
@@ -26,6 +26,7 @@
import com.apple.foundationdb.record.query.plan.cascades.rules.AdjustMatchRule;
import com.apple.foundationdb.record.query.plan.cascades.rules.DataAccessRule;
import com.apple.foundationdb.record.query.plan.cascades.rules.ImplementDeleteRule;
+import com.apple.foundationdb.record.query.plan.cascades.rules.ImplementDfsJoinRule;
import com.apple.foundationdb.record.query.plan.cascades.rules.ImplementDistinctRule;
import com.apple.foundationdb.record.query.plan.cascades.rules.ImplementDistinctUnionRule;
import com.apple.foundationdb.record.query.plan.cascades.rules.ImplementExplodeRule;
@@ -169,6 +170,7 @@ public class PlanningRuleSet extends CascadesRuleSet {
new ImplementInsertRule(),
new ImplementTempTableInsertRule(),
new ImplementUpdateRule(),
+ new ImplementDfsJoinRule(),
new ImplementRecursiveUnionRule(),
new ImplementTableFunctionRule()
);
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java
index 0edb72c532..b3c837624d 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java
@@ -65,6 +65,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlanWithExplain;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPredicatesFilterPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan;
@@ -581,6 +582,12 @@ public ExplainTokens visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPla
.iterator());
}
+ @Nonnull
+ @Override
+ public ExplainTokens visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ return addKeyword("DFS").addWhitespace(); // TODO
+ }
+
@Nonnull
private ExplainTokens visitUnionPlan(@Nonnull final RecordQueryUnionPlan unionPlan) {
visitAndJoin(() -> new ExplainTokens().addWhitespace().addToString("∪").addWhitespace(),
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/NodeInfo.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/NodeInfo.java
index f5439d83a4..36b028edfa 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/NodeInfo.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/NodeInfo.java
@@ -184,6 +184,11 @@ public class NodeInfo {
NodeIcon.COMPUTATION_OPERATOR,
"Recursive Union",
"A recursive union that processes an initial state leg, then a recursive leg repeatedly until reaching a fix point.");
+ public static final NodeInfo RECURSIVE_DFS_OPERATOR = new NodeInfo(
+ "RecursiveUnionOperator",
+ NodeIcon.COMPUTATION_OPERATOR,
+ "Recursive DFS",
+ "A recursive DFS that processes an recursion in DFS fashion.");
public static final NodeInfo STREAMING_AGGREGATE_OPERATOR = new NodeInfo(
"StreamingAggregateOperator",
NodeIcon.COMPUTATION_OPERATOR,
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/RecursiveExpression.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/RecursiveExpression.java
new file mode 100644
index 0000000000..b2858514d7
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/RecursiveExpression.java
@@ -0,0 +1,145 @@
+/*
+ * RecursiveExpression.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.expressions;
+
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
+import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
+import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.explain.InternalPlannerGraphRewritable;
+import com.apple.foundationdb.record.query.plan.cascades.explain.PlannerGraph;
+import com.apple.foundationdb.record.query.plan.cascades.values.Value;
+import com.apple.foundationdb.record.query.plan.cascades.values.Values;
+import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A recursive expression.
+ */
+@API(API.Status.EXPERIMENTAL)
+public class RecursiveExpression implements RelationalExpressionWithChildren, InternalPlannerGraphRewritable {
+ @Nonnull
+ private final Value resultValue;
+ @Nonnull
+ private final Quantifier rootQuantifier;
+ @Nonnull
+ private final Quantifier childQuantifier;
+
+ public RecursiveExpression(@Nonnull Value resultValue,
+ @Nonnull Quantifier rootQuantifier,
+ @Nonnull Quantifier childQuantifier) {
+ this.resultValue = resultValue;
+ this.rootQuantifier = rootQuantifier;
+ this.childQuantifier = childQuantifier;
+ }
+
+ @Nonnull
+ @Override
+ public Value getResultValue() {
+ return resultValue;
+ }
+
+ @Nonnull
+ public List extends Value> getResultValues() {
+ return Values.deconstructRecord(getResultValue());
+ }
+
+ @Nonnull
+ @Override
+ public List extends Quantifier> getQuantifiers() {
+ return List.of(rootQuantifier, childQuantifier);
+ }
+
+ @Override
+ public int getRelationalChildCount() {
+ return 2;
+ }
+
+ @Override
+ public boolean canCorrelate() {
+ return true;
+ }
+
+ @Nonnull
+ @Override
+ public Set getCorrelatedToWithoutChildren() {
+ return resultValue.getCorrelatedTo();
+ }
+
+ @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
+ @Override
+ public boolean equals(final Object other) {
+ return semanticEquals(other);
+ }
+
+ @Override
+ public int hashCode() {
+ return semanticHashCode();
+ }
+
+ @Override
+ @SuppressWarnings({"UnstableApiUsage", "PMD.CompareObjectsWithEquals"})
+ public boolean equalsWithoutChildren(@Nonnull RelationalExpression otherExpression,
+ @Nonnull final AliasMap aliasMap) {
+ if (this == otherExpression) {
+ return true;
+ }
+ if (getClass() != otherExpression.getClass()) {
+ return false;
+ }
+
+ return semanticEqualsForResults(otherExpression, aliasMap);
+ }
+
+ @Override
+ public int hashCodeWithoutChildren() {
+ return getResultValue().hashCode();
+ }
+
+ @Nonnull
+ @Override
+ public RelationalExpression translateCorrelations(@Nonnull final TranslationMap translationMap, final boolean shouldSimplifyValues,
+ @Nonnull final List extends Quantifier> translatedQuantifiers) {
+ final Value translatedResultValue = resultValue.translateCorrelations(translationMap, shouldSimplifyValues);
+ return new RecursiveExpression(translatedResultValue, translatedQuantifiers.get(0), translatedQuantifiers.get(1));
+ }
+
+ @Nonnull
+ @Override
+ public PlannerGraph rewriteInternalPlannerGraph(@Nonnull final List extends PlannerGraph> childGraphs) {
+ return PlannerGraph.fromNodeAndChildGraphs(
+ new PlannerGraph.LogicalOperatorNode(this,
+ "RECURSIVE " + resultValue,
+ ImmutableList.of(),
+ ImmutableMap.of()),
+ childGraphs);
+ }
+
+ @Override
+ public String toString() {
+ return "RECURSIVE " + resultValue;
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/matching/structure/RelationalExpressionMatchers.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/matching/structure/RelationalExpressionMatchers.java
index 8ccb82d31c..b021bb2bd8 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/matching/structure/RelationalExpressionMatchers.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/matching/structure/RelationalExpressionMatchers.java
@@ -35,6 +35,7 @@
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUniqueExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates;
@@ -322,4 +323,9 @@ public static BindingMatcher recursiveUnionExpression(
Extractor.of(RecursiveUnionExpression::getRecursiveStateQuantifier, name -> "recursive(" + name + ")"),
recursiveDownstream))));
}
+
+ @Nonnull
+ public static BindingMatcher recursiveExpression(@Nonnull final CollectionMatcher extends Quantifier> downstream) {
+ return ofTypeOwning(RecursiveExpression.class, downstream);
+ }
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java
index 089396c7a5..0611cefeb2 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java
@@ -44,6 +44,7 @@
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUniqueExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.MatchableSortExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionVisitor;
@@ -82,6 +83,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryMapPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPredicatesFilterPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan;
@@ -408,6 +410,19 @@ public Cardinalities visitRecordQueryTypeFilterPlan(@Nonnull final RecordQueryTy
return fromChild(typeFilterPlan);
}
+ @Nonnull
+ @Override
+ public Cardinalities visitRecordQueryRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ // TODO
+ return Cardinalities.unknownCardinalities;
+ }
+
+ @Nonnull
+ @Override
+ public Cardinalities visitRecursiveExpression(@Nonnull final RecursiveExpression element) {
+ return Cardinalities.unknownMaxCardinality();
+ }
+
@Nonnull
@Override
public Cardinalities visitRecordQueryInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
index f39d370dd2..55a4db9e48 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
@@ -62,6 +62,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInValuesJoinPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryIndexPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInsertPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan;
import com.apple.foundationdb.record.query.plan.plans.TempTableInsertPlan;
@@ -551,6 +552,13 @@ public Derivations visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan
return new Derivations(resultValuesBuilder.build(), childDerivations.getLocalValues());
}
+ @Nonnull
+ @Override
+ public Derivations visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ // TODO
+ return Derivations.EMPTY;
+ }
+
@Nonnull
@Override
public Derivations visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java
index 4118cfaa1d..979405ac6f 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java
@@ -47,6 +47,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInValuesJoinPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryIndexPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInsertPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan;
import com.apple.foundationdb.record.query.plan.plans.TempTableInsertPlan;
@@ -329,6 +330,12 @@ public Boolean visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan type
return distinctRecordsFromSingleChild(typeFilterPlan);
}
+ @Nonnull
+ @Override
+ public Boolean visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ return false;
+ }
+
@Nonnull
@Override
public Boolean visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan element) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java
index 1c9440608e..dd359326c7 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java
@@ -60,6 +60,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInValuesJoinPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryIndexPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInsertPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan;
import com.apple.foundationdb.record.query.plan.plans.TempTableScanPlan;
@@ -509,6 +510,12 @@ public Ordering visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan typ
return orderingFromSingleChild(typeFilterPlan);
}
+ @Nonnull
+ @Override
+ public Ordering visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ return Ordering.empty();
+ }
+
@Nonnull
@Override
public Ordering visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java
index 0701fabc82..204bdddf96 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java
@@ -48,6 +48,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInValuesJoinPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryIndexPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInsertPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan;
import com.apple.foundationdb.record.query.plan.plans.TempTableScanPlan;
@@ -351,6 +352,12 @@ public Optional> visitTypeFilterPlan(@Nonnull final RecordQueryTypeF
return primaryKeyFromSingleChild(typeFilterPlan);
}
+ @Nonnull
+ @Override
+ public Optional> visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ return Optional.empty(); // TODO
+ }
+
@Nonnull
@Override
public Optional> visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/RecordTypesProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/RecordTypesProperty.java
index 3e6d672e93..002aef525d 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/RecordTypesProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/RecordTypesProperty.java
@@ -31,6 +31,7 @@
import com.apple.foundationdb.record.query.plan.cascades.SimpleExpressionVisitor;
import com.apple.foundationdb.record.query.plan.cascades.expressions.FullUnorderedScanExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalUnionExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveUnionExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression;
@@ -160,7 +161,8 @@ public Set evaluateAtExpression(@Nonnull RelationalExpression expression
expression instanceof LogicalUnionExpression ||
expression instanceof RecursiveUnionExpression ||
expression instanceof SelectExpression ||
- expression instanceof RecordQueryFlatMapPlan) {
+ expression instanceof RecordQueryFlatMapPlan ||
+ expression instanceof RecursiveExpression) {
final Set union = new HashSet<>();
for (Set childResulSet : childResults) {
union.addAll(childResulSet);
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java
index 0ed00eabcf..874d3807e5 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java
@@ -45,6 +45,7 @@
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInValuesJoinPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryIndexPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryInsertPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan;
import com.apple.foundationdb.record.query.plan.plans.TempTableInsertPlan;
@@ -311,6 +312,12 @@ public Boolean visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan type
return storedRecordsFromSingleChild(typeFilterPlan);
}
+ @Nonnull
+ @Override
+ public Boolean visitRecursiveDfsPlan(@Nonnull final RecordQueryRecursiveDfsPlan element) {
+ return false; // TODO
+ }
+
@Nonnull
@Override
public Boolean visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan element) {
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementDfsJoinRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementDfsJoinRule.java
new file mode 100644
index 0000000000..d72c433041
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementDfsJoinRule.java
@@ -0,0 +1,116 @@
+/*
+ * ImplementDfsJoinRule.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.rules;
+
+import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRule;
+import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRuleCall;
+import com.apple.foundationdb.record.query.plan.cascades.PlanPartition;
+import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.Reference;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveUnionExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.TempTableInsertExpression;
+import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
+import com.apple.foundationdb.record.query.plan.plans.TempTableInsertPlan;
+
+import javax.annotation.Nonnull;
+
+import java.util.Collection;
+
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.AnyMatcher.any;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.anyPlanPartition;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.planPartitions;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.rollUpPartitions;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.QuantifierMatchers.forEachQuantifier;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.QuantifierMatchers.forEachQuantifierOverRef;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RecordQueryPlanMatchers.anyPlan;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RecordQueryPlanMatchers.tempTableInsertPlan;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RecordQueryPlanMatchers.tempTableScanPlan;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.ReferenceMatchers.exploratoryMember;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RelationalExpressionMatchers.recursiveUnionExpression;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RelationalExpressionMatchers.selectExpression;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RelationalExpressionMatchers.tempTableInsertExpression;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.SetMatcher.exactlyInAnyOrder;
+
+public class ImplementDfsJoinRule extends ImplementationCascadesRule {
+
+ @Nonnull
+ private static final BindingMatcher initialInnerPlanMatcher = anyPlan();
+
+ @Nonnull
+ private static final BindingMatcher initialPlanMatcher = tempTableInsertPlan(initialInnerPlanMatcher);
+
+ @Nonnull
+ private static final BindingMatcher innerPlanPartitionMatcher = anyPlanPartition();
+
+ @Nonnull
+ private static final BindingMatcher> innerPlanPartitionsMatcher = rollUpPartitions(any(innerPlanPartitionMatcher));
+
+ @Nonnull
+ private static final BindingMatcher innerReferenceMatcher = planPartitions(innerPlanPartitionsMatcher);
+
+ @Nonnull
+ private static final BindingMatcher innerQunMatcher = forEachQuantifierOverRef(innerReferenceMatcher);
+
+ @Nonnull
+ private static final BindingMatcher tempTableQunMatcher = forEachQuantifier(tempTableScanPlan());
+
+ @Nonnull
+ private static final BindingMatcher recursiveInnerSelectMatcher = selectExpression(
+ exactlyInAnyOrder(innerQunMatcher, tempTableQunMatcher));
+
+ @Nonnull
+ private static final BindingMatcher recursivePlanMatcher = tempTableInsertExpression(
+ forEachQuantifierOverRef(exploratoryMember(recursiveInnerSelectMatcher)));
+
+ @Nonnull
+ private static final BindingMatcher root = recursiveUnionExpression(forEachQuantifier(initialPlanMatcher),
+ forEachQuantifier(recursivePlanMatcher));
+
+ public ImplementDfsJoinRule() {
+ super(root);
+ }
+
+ @Override
+ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
+ final var recursiveUnion = call.get(root);
+ final var rootAlias = recursiveUnion.getInitialStateQuantifier().getAlias();
+ final var recursiveAlias = recursiveUnion.getRecursiveStateQuantifier().getAlias();
+
+ final var initialInnerPlan = call.get(initialInnerPlanMatcher);
+ final var recursiveInnerSelect = call.get(recursiveInnerSelectMatcher);
+ final var innerPlanPartition = call.get(innerPlanPartitionMatcher);
+ final var innerRef = call.get(innerReferenceMatcher);
+ final var innerQun = call.get(innerQunMatcher);
+ final var priorValueCorrelation = call.get(tempTableQunMatcher).getAlias();
+
+ final var rootPlanRef = call.memoizePlan(initialInnerPlan);
+ final var rootQun = Quantifier.physical(rootPlanRef, rootAlias);
+
+ final var recursivePlanRef = ImplementSimpleSelectRule.implementSelectExpression(call, recursiveInnerSelect.getResultValue(),
+ recursiveInnerSelect.getPredicates(), innerRef, innerQun, innerPlanPartition).reference();
+ final var recursiveQun = Quantifier.physical(recursivePlanRef, recursiveAlias);
+
+ call.yieldPlan(new RecordQueryRecursiveDfsPlan(rootQun, recursiveQun, priorValueCorrelation));
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveExpressionRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveExpressionRule.java
new file mode 100644
index 0000000000..908fcb9649
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveExpressionRule.java
@@ -0,0 +1,105 @@
+/*
+ * ImplementRecursiveRule.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.rules;
+
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.record.logging.KeyValueLogMessage;
+import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRule;
+import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRuleCall;
+import com.apple.foundationdb.record.query.plan.cascades.PlanPartition;
+import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.Reference;
+import com.apple.foundationdb.record.query.plan.cascades.debug.Debugger;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveExpression;
+import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.ListMatcher.exactly;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.MultiMatcher.all;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.anyPlanPartition;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.planPartitions;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlanPartitionMatchers.rollUpPartitions;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.QuantifierMatchers.anyQuantifierOverRef;
+
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RelationalExpressionMatchers.recursiveExpression;
+
+/**
+ * A rule that implements an existential nested loop join of its (already implemented) children.
+ */
+@API(API.Status.EXPERIMENTAL)
+@SuppressWarnings("PMD.TooManyStaticImports")
+public class ImplementRecursiveExpressionRule extends ImplementationCascadesRule {
+ @Nonnull
+ private static final Logger logger = LoggerFactory.getLogger(ImplementRecursiveExpressionRule.class);
+
+ @Nonnull
+ private static final BindingMatcher rootPlanPartitionsMatcher = anyPlanPartition();
+
+ @Nonnull
+ private static final BindingMatcher rootReferenceMatcher = planPartitions(rollUpPartitions(all(rootPlanPartitionsMatcher)));
+ @Nonnull
+ private static final BindingMatcher rootQuantifierMatcher = anyQuantifierOverRef(rootReferenceMatcher);
+ @Nonnull
+ private static final BindingMatcher childPlanPartitionsMatcher = anyPlanPartition();
+
+ @Nonnull
+ private static final BindingMatcher childReferenceMatcher = planPartitions(rollUpPartitions(all(childPlanPartitionsMatcher)));
+ @Nonnull
+ private static final BindingMatcher childQuantifierMatcher = anyQuantifierOverRef(childReferenceMatcher);
+ @Nonnull
+ private static final BindingMatcher root =
+ recursiveExpression(exactly(rootQuantifierMatcher, childQuantifierMatcher));
+
+ public ImplementRecursiveExpressionRule() {
+ super(root);
+ }
+
+ @Override
+ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
+ final var bindings = call.getBindings();
+ final var recursiveExpression = bindings.get(root);
+ Debugger.withDebugger(debugger -> logger.debug(KeyValueLogMessage.of("matched RecursiveExpression", "legs", recursiveExpression.getQuantifiers().size())));
+
+ final var rootQuantifier = bindings.get(rootQuantifierMatcher);
+ final var childQuantifier = bindings.get(childQuantifierMatcher);
+
+ final var rootReference = bindings.get(rootReferenceMatcher);
+ final var childReference = bindings.get(childReferenceMatcher);
+
+ final var rootPartition = bindings.get(rootPlanPartitionsMatcher);
+ final var childPartition = bindings.get(childPlanPartitionsMatcher);
+
+ final var rootAlias = rootQuantifier.getAlias();
+ final var childAlias = childQuantifier.getAlias();
+
+ var rootRef = call.memoizeMemberPlansFromOther(rootReference, rootPartition.getPlans());
+ final var newRootQuantifier = Quantifier.physicalBuilder().withAlias(rootAlias).build(rootRef);
+
+ var childRef = call.memoizeMemberPlansFromOther(childReference, childPartition.getPlans());
+ final var newChildQuantifier = Quantifier.physicalBuilder().withAlias(childAlias).build(childRef);
+
+ //call.yieldPlan(new RecordQueryRecursiveDfsPlan(newRootQuantifier, newChildQuantifier, recursiveExpression.getResultValue()));
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveUnionRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveUnionRule.java
index f0e1701675..9324b2ed1f 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveUnionRule.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementRecursiveUnionRule.java
@@ -21,13 +21,22 @@
package com.apple.foundationdb.record.query.plan.cascades.rules;
import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRule;
import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRuleCall;
import com.apple.foundationdb.record.query.plan.cascades.PlanPartition;
import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.Reference;
import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveUnionExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.TempTableScanExpression;
import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher;
+import com.apple.foundationdb.record.query.plan.cascades.predicates.PredicateWithComparisons;
+import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveDfsPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveUnionPlan;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import javax.annotation.Nonnull;
@@ -86,6 +95,57 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
final var tempTableInsertValueReference = recursiveUnionExpression.getTempTableInsertAlias();
final var recursiveUnionPlan = new RecordQueryRecursiveUnionPlan(initialPhysicalQun, recursivePhysicalQun, tempTableScanValueReference, tempTableInsertValueReference);
- call.yieldPlan(recursiveUnionPlan);
+// if (canPerformDfs(recursiveQun.getRangesOver(), tempTableScanValueReference)) {
+// final var rootPhysicalReference = call.memoizeFinalExpressions(initialQun.getRangesOver().getFinalExpressions().stream().flatMap(e -> Iterables.getOnlyElement(e.getQuantifiers()).getRangesOver().getFinalExpressions().stream()).collect(ImmutableSet.toImmutableSet()));
+// final var rootPhysicalQun = Quantifier.physical(rootPhysicalReference);
+// final var childPhysicalQun = (Quantifier.Physical)Iterables.getOnlyElement(Iterables.getOnlyElement(recursiveQun.getRangesOver().getFinalExpressions()).getQuantifiers());
+// call.yieldPlan(new RecordQueryRecursiveDfsPlan(rootPhysicalQun, childPhysicalQun, tempTableScanValueReference));
+// } else {
+ call.yieldPlan(recursiveUnionPlan);
+// }
+ }
+
+ private boolean canPerformDfs(@Nonnull final Reference recursiveQun, @Nonnull final CorrelationIdentifier tempTableScanValueReference) {
+ for (final var expression : recursiveQun.getAllMemberExpressions()) {
+ if (!hasOnlyEqualityPredicatesOverTempTable(expression, tempTableScanValueReference)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean hasOnlyEqualityPredicatesOverTempTable(@Nonnull final RelationalExpression expression, @Nonnull final CorrelationIdentifier correlationIdentifier) {
+ if (expression.getQuantifiers().isEmpty()) {
+ return true; // no quantifiers.
+ }
+ if (expression instanceof RelationalExpressionWithPredicates) {
+ final var predicatedExpression = (RelationalExpressionWithPredicates)expression;
+ final var predicates = predicatedExpression.getPredicates();
+ for (final var quantifier : expression.getQuantifiers()) {
+ for (final var innerExpressions : quantifier.getRangesOver().getAllMemberExpressions()) {
+ if (innerExpressions instanceof TempTableScanExpression) {
+ for (final var predicate : predicates) {
+ boolean nonEqualityCorrelatedPredicate = predicate.preOrderStream().anyMatch(p -> {
+ if (!p.isCorrelatedTo(quantifier.getAlias())) {
+ return false;
+ }
+ if (p instanceof PredicateWithComparisons) {
+ if (((PredicateWithComparisons)p).getComparisons()
+ .stream()
+ .anyMatch(c -> !c.getType().isEquality())) {
+ return true;
+ }
+ }
+ return false;
+ });
+ if (nonEqualityCorrelatedPredicate) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
}
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementSimpleSelectRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementSimpleSelectRule.java
index f91fd25c56..62f8f2b78c 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementSimpleSelectRule.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementSimpleSelectRule.java
@@ -24,6 +24,7 @@
import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRule;
import com.apple.foundationdb.record.query.plan.cascades.ImplementationCascadesRuleCall;
+import com.apple.foundationdb.record.query.plan.cascades.Memoizer;
import com.apple.foundationdb.record.query.plan.cascades.PlanPartition;
import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
import com.apple.foundationdb.record.query.plan.cascades.Reference;
@@ -32,6 +33,7 @@
import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
import com.apple.foundationdb.record.query.plan.cascades.values.NullValue;
import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedObjectValue;
+import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryDefaultOnEmptyPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryFirstOrDefaultPlan;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryMapPlan;
@@ -40,6 +42,8 @@
import javax.annotation.Nonnull;
+import java.util.List;
+
import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.AnyMatcher.any;
import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.ListMatcher.exactly;
import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.MultiMatcher.all;
@@ -85,28 +89,39 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
final var innerReference = bindings.get(innerReferenceMatcher);
final var quantifier = bindings.get(innerQuantifierMatcher);
final var predicates = bindings.getAll(predicateMatcher);
+ final var resultValue = selectExpression.getResultValue();
+ final var referenceBuilder = implementSelectExpression(call, resultValue, predicates, innerReference, quantifier, planPartition);
+ call.yieldPlans(referenceBuilder.members());
+ }
- var resultValue = selectExpression.getResultValue();
- var referenceBuilder = call.memoizeMemberPlansBuilder(innerReference, planPartition.getPlans());
+ @Nonnull
+ public static Memoizer.ReferenceOfPlansBuilder implementSelectExpression(@Nonnull final ImplementationCascadesRuleCall call,
+ @Nonnull final Value result,
+ @Nonnull final List extends QueryPredicate> predicates,
+ @Nonnull final Reference innerReference,
+ @Nonnull final Quantifier innerQuantifier,
+ @Nonnull final PlanPartition innerPlanPartition) {
+ var resultValue = result;
+ var referenceBuilder = call.memoizeMemberPlansBuilder(innerReference, innerPlanPartition.getPlans());
final var isSimpleResultValue =
resultValue instanceof QuantifiedObjectValue &&
- ((QuantifiedObjectValue)resultValue).getAlias().equals(quantifier.getAlias());
+ ((QuantifiedObjectValue)resultValue).getAlias().equals(innerQuantifier.getAlias());
- if (quantifier instanceof Quantifier.Existential) {
+ if (innerQuantifier instanceof Quantifier.Existential) {
referenceBuilder = call.memoizePlanBuilder(
new RecordQueryFirstOrDefaultPlan(
Quantifier.physicalBuilder()
- .withAlias(quantifier.getAlias())
+ .withAlias(innerQuantifier.getAlias())
.build(referenceBuilder.reference()),
- new NullValue(quantifier.getFlowedObjectType())));
- } else if (quantifier instanceof Quantifier.ForEach && ((Quantifier.ForEach)quantifier).isNullOnEmpty()) {
+ new NullValue(innerQuantifier.getFlowedObjectType())));
+ } else if (innerQuantifier instanceof Quantifier.ForEach && ((Quantifier.ForEach)innerQuantifier).isNullOnEmpty()) {
referenceBuilder = call.memoizePlanBuilder(
new RecordQueryDefaultOnEmptyPlan(
Quantifier.physicalBuilder()
- .withAlias(quantifier.getAlias())
+ .withAlias(innerQuantifier.getAlias())
.build(referenceBuilder.reference()),
- new NullValue(quantifier.getFlowedObjectType())));
+ new NullValue(innerQuantifier.getFlowedObjectType())));
}
final var nonTautologyPredicates =
@@ -115,15 +130,14 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
.collect(ImmutableList.toImmutableList());
if (nonTautologyPredicates.isEmpty() &&
isSimpleResultValue) {
- call.yieldPlans(referenceBuilder.members());
- return;
+ return referenceBuilder;
}
if (!nonTautologyPredicates.isEmpty()) {
referenceBuilder = call.memoizePlanBuilder(
new RecordQueryPredicatesFilterPlan(
Quantifier.physicalBuilder()
- .withAlias(quantifier.getAlias())
+ .withAlias(innerQuantifier.getAlias())
.build(referenceBuilder.reference()),
nonTautologyPredicates.stream()
.map(QueryPredicate::toResidualPredicate)
@@ -133,18 +147,18 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
if (!isSimpleResultValue) {
final Quantifier.Physical beforeMapQuantifier;
if (!nonTautologyPredicates.isEmpty()) {
- final var lowerAlias = quantifier.getAlias();
+ final var lowerAlias = innerQuantifier.getAlias();
beforeMapQuantifier = Quantifier.physical(referenceBuilder.reference());
resultValue = resultValue.rebase(AliasMap.ofAliases(lowerAlias, beforeMapQuantifier.getAlias()));
} else {
beforeMapQuantifier = Quantifier.physicalBuilder()
- .withAlias(quantifier.getAlias())
+ .withAlias(innerQuantifier.getAlias())
.build(referenceBuilder.reference());
}
referenceBuilder = call.memoizePlanBuilder(new RecordQueryMapPlan(beforeMapQuantifier, resultValue));
}
- call.yieldPlans(referenceBuilder.members());
+ return referenceBuilder;
}
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/PushRequestedOrderingThroughRecursiveExpressionRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/PushRequestedOrderingThroughRecursiveExpressionRule.java
new file mode 100644
index 0000000000..141c823c1a
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/PushRequestedOrderingThroughRecursiveExpressionRule.java
@@ -0,0 +1,81 @@
+/*
+ * PushRequestedOrderingThroughRecursiveRule.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.rules;
+
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.record.query.plan.cascades.CascadesRule;
+import com.apple.foundationdb.record.query.plan.cascades.CascadesRuleCall;
+import com.apple.foundationdb.record.query.plan.cascades.PlannerRule.PreOrderRule;
+import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.Reference;
+import com.apple.foundationdb.record.query.plan.cascades.RequestedOrdering;
+import com.apple.foundationdb.record.query.plan.cascades.RequestedOrderingConstraint;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RecursiveExpression;
+import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher;
+import com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlannerBindings;
+import com.apple.foundationdb.record.query.plan.cascades.matching.structure.ReferenceMatchers;
+import com.google.common.collect.ImmutableSet;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.MultiMatcher.all;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.QuantifierMatchers.forEachQuantifierOverRef;
+import static com.apple.foundationdb.record.query.plan.cascades.matching.structure.RelationalExpressionMatchers.recursiveExpression;
+
+/**
+ * A rule that pushes an ordering {@link RequestedOrderingConstraint} through a {@link RecursiveExpression}.
+ */
+@API(API.Status.EXPERIMENTAL)
+public class PushRequestedOrderingThroughRecursiveExpressionRule extends CascadesRule implements PreOrderRule {
+ private static final BindingMatcher lowerRefMatcher = ReferenceMatchers.anyRef();
+ private static final BindingMatcher innerQuantifierMatcher = forEachQuantifierOverRef(lowerRefMatcher);
+ private static final BindingMatcher root =
+ recursiveExpression(all(innerQuantifierMatcher));
+
+ public PushRequestedOrderingThroughRecursiveExpressionRule() {
+ super(root, ImmutableSet.of(RequestedOrderingConstraint.REQUESTED_ORDERING));
+ }
+
+ @Override
+ public void onMatch(@Nonnull final CascadesRuleCall call) {
+ final Optional> requestedOrderingOptional = call.getPlannerConstraintMaybe(RequestedOrderingConstraint.REQUESTED_ORDERING);
+ if (requestedOrderingOptional.isEmpty()) {
+ return;
+ }
+
+ // TODO: This isn't right. I think we only can do this if the requested ordering is empty, since the output order will
+ // be the cursor's pre-order. We do need that case so that the two child quantifiers get plans from the data access rule.
+
+ final PlannerBindings bindings = call.getBindings();
+ final List extends Quantifier.ForEach> rangesOverQuantifiers = bindings.getAll(innerQuantifierMatcher);
+
+ rangesOverQuantifiers
+ .stream()
+ .map(Quantifier.ForEach::getRangesOver)
+ .forEach(lowerReference ->
+ call.pushConstraint(lowerReference,
+ RequestedOrderingConstraint.REQUESTED_ORDERING,
+ requestedOrderingOptional.get()));
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RecursivePriorValue.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RecursivePriorValue.java
new file mode 100644
index 0000000000..a27bf99492
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RecursivePriorValue.java
@@ -0,0 +1,193 @@
+/*
+ * RecursivePriorValue.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.values;
+
+import com.apple.foundationdb.annotation.API;
+import com.apple.foundationdb.annotation.SpotBugsSuppressWarnings;
+import com.apple.foundationdb.record.Bindings;
+import com.apple.foundationdb.record.EvaluationContext;
+import com.apple.foundationdb.record.ObjectPlanHash;
+import com.apple.foundationdb.record.PlanDeserializer;
+import com.apple.foundationdb.record.PlanHashable;
+import com.apple.foundationdb.record.PlanSerializable;
+import com.apple.foundationdb.record.PlanSerializationContext;
+import com.apple.foundationdb.record.planprotos.PRecursivePriorValue;
+import com.apple.foundationdb.record.planprotos.PValue;
+import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
+import com.apple.foundationdb.record.query.plan.QueryPlanConstraint;
+import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
+import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
+import com.apple.foundationdb.record.query.plan.cascades.ValueEquivalence;
+import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
+import com.apple.foundationdb.record.query.plan.cascades.values.simplification.ComparisonCompensation;
+import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap;
+import com.apple.foundationdb.record.query.plan.explain.ExplainTokens;
+import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence;
+import com.apple.foundationdb.record.query.plan.plans.QueryResult;
+import com.apple.foundationdb.record.util.pair.NonnullPair;
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.Message;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * A value representing the version of a quantifier from the prior iteration of a recursion.
+ */
+@API(API.Status.EXPERIMENTAL)
+public class RecursivePriorValue extends AbstractValue implements LeafValue, PlanSerializable {
+ private static final ObjectPlanHash BASE_HASH = new ObjectPlanHash("Recursive-Prior-Value");
+
+ @Nonnull
+ private final CorrelationIdentifier alias;
+ @Nonnull
+ private final Type resultType;
+
+ private RecursivePriorValue(@Nonnull CorrelationIdentifier alias, @Nonnull Type resultType) {
+ this.alias = alias;
+ this.resultType = resultType;
+ }
+
+ @Nonnull
+ public static RecursivePriorValue of(@Nonnull CorrelationIdentifier alias, @Nonnull Type resultType) {
+ return new RecursivePriorValue(alias, resultType);
+ }
+
+ @Nonnull
+ @Override
+ public Set getCorrelatedTo() {
+ return ImmutableSet.of(alias);
+ }
+
+ @Nonnull
+ @Override
+ public Value rebaseLeaf(@Nonnull final CorrelationIdentifier targetAlias) {
+ return RecursivePriorValue.of(targetAlias, resultType);
+ }
+
+ @Nonnull
+ @Override
+ public Optional> matchAndCompensateComparisonMaybe(@Nonnull final Value candidateValue, @Nonnull final ValueEquivalence valueEquivalence) {
+ return Optional.empty();
+ }
+
+ @Nonnull
+ @Override
+ public Type getResultType() {
+ return resultType;
+ }
+
+ @Nonnull
+ @Override
+ public ExplainTokensWithPrecedence explain(@Nonnull final Iterable> explainSuppliers) {
+ return ExplainTokensWithPrecedence.of(new ExplainTokens()
+ .addWhitespace().addIdentifier("PRIOR").addWhitespace().addAliasDefinition(alias));
+ }
+
+ @Nonnull
+ @Override
+ protected Iterable extends Value> computeChildren() {
+ return List.of();
+ }
+
+ @Override
+ public Object eval(@Nonnull final FDBRecordStoreBase store, @Nonnull final EvaluationContext context) {
+ final CorrelationIdentifier priorId = CorrelationIdentifier.of("prior_" + alias.getId());
+ final var binding = (QueryResult)context.getBinding(Bindings.Internal.CORRELATION.bindingName(priorId.getId()));
+ if (resultType.isRecord()) {
+ return binding.getDatum() == null ? null : binding.getMessage();
+ } else {
+ return binding.getDatum();
+ }
+ }
+
+ @Override
+ public int hashCodeWithoutChildren() {
+ return PlanHashable.objectsPlanHash(PlanHashable.CURRENT_FOR_CONTINUATION, BASE_HASH);
+ }
+
+ @Override
+ public int planHash(@Nonnull final PlanHashMode mode) {
+ return PlanHashable.objectsPlanHash(mode, BASE_HASH);
+ }
+
+ @Override
+ public String toString() {
+ return "Prior(" + alias + ")";
+ }
+
+ @Override
+ public int hashCode() {
+ return semanticHashCode();
+ }
+
+ @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
+ @SpotBugsSuppressWarnings("EQ_UNUSUAL")
+ @Override
+ public boolean equals(final Object other) {
+ return semanticEquals(other, AliasMap.emptyMap());
+ }
+
+ @Nonnull
+ @Override
+ public PRecursivePriorValue toProto(@Nonnull final PlanSerializationContext serializationContext) {
+ return PRecursivePriorValue.newBuilder()
+ .setAlias(alias.getId())
+ .setResultType(resultType.toTypeProto(serializationContext))
+ .build();
+ }
+
+ @Nonnull
+ @Override
+ public PValue toValueProto(@Nonnull PlanSerializationContext serializationContext) {
+ return PValue.newBuilder().setRecursivePriorValue(toProto(serializationContext)).build();
+ }
+
+ @Nonnull
+ public static RecursivePriorValue fromProto(@Nonnull final PlanSerializationContext serializationContext, @Nonnull final PRecursivePriorValue recursivePriorValue) {
+ return new RecursivePriorValue(CorrelationIdentifier.of(Objects.requireNonNull(recursivePriorValue.getAlias())),
+ Type.fromTypeProto(serializationContext, Objects.requireNonNull(recursivePriorValue.getResultType())));
+ }
+
+ /**
+ * Deserializer.
+ */
+ @AutoService(PlanDeserializer.class)
+ public static class Deserializer implements PlanDeserializer {
+ @Nonnull
+ @Override
+ public Class getProtoMessageClass() {
+ return PRecursivePriorValue.class;
+ }
+
+ @Nonnull
+ @Override
+ public RecursivePriorValue fromProto(@Nonnull final PlanSerializationContext serializationContext,
+ @Nonnull final PRecursivePriorValue recursivePriorProto) {
+ return RecursivePriorValue.fromProto(serializationContext, recursivePriorProto);
+ }
+ }
+}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/translation/TranslationMap.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/translation/TranslationMap.java
index 3ed6d228c3..2f6cad976e 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/translation/TranslationMap.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/translation/TranslationMap.java
@@ -26,8 +26,10 @@
import com.apple.foundationdb.record.query.plan.cascades.values.LeafValue;
import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedObjectValue;
import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedValue;
+import com.apple.foundationdb.record.query.plan.cascades.values.RecursivePriorValue;
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.google.common.base.Verify;
+import com.google.common.collect.Iterables;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -104,7 +106,14 @@ static TranslationFunction adjustValueType(@Nonnull final Value translationTarge
}
Verify.verify(!(translationTargetType instanceof Type.Erasable) ||
!((Type.Erasable)translationTargetType).isErased());
- return (ignored, ignored2) -> translationTargetValue;
+ return (ignored, ignored2) -> {
+ System.out.println(ignored2);
+ if (ignored2 instanceof RecursivePriorValue) {
+ final var targetCorrelation = Iterables.getOnlyElement(translationTargetValue.getCorrelatedTo());
+ return ignored2.rebaseLeaf(targetCorrelation);
+ }
+ return translationTargetValue;
+ };
}
}
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryRecursiveDfsPlan.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryRecursiveDfsPlan.java
new file mode 100644
index 0000000000..1b68d4320f
--- /dev/null
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryRecursiveDfsPlan.java
@@ -0,0 +1,199 @@
+/*
+ * RecordQueryRecursiveDfsPlan.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed 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 com.apple.foundationdb.record.query.plan.plans;
+
+import com.apple.foundationdb.record.Bindings;
+import com.apple.foundationdb.record.EvaluationContext;
+import com.apple.foundationdb.record.ExecuteProperties;
+import com.apple.foundationdb.record.PlanSerializationContext;
+import com.apple.foundationdb.record.RecordCursor;
+import com.apple.foundationdb.record.cursors.RecursiveCursor;
+import com.apple.foundationdb.record.planprotos.PRecordQueryPlan;
+import com.apple.foundationdb.record.provider.common.StoreTimer;
+import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
+import com.apple.foundationdb.record.query.plan.AvailableFields;
+import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
+import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
+import com.apple.foundationdb.record.query.plan.cascades.Quantifier;
+import com.apple.foundationdb.record.query.plan.cascades.explain.NodeInfo;
+import com.apple.foundationdb.record.query.plan.cascades.explain.PlannerGraph;
+import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
+import com.apple.foundationdb.record.query.plan.cascades.values.Value;
+import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Message;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class RecordQueryRecursiveDfsPlan implements RecordQueryPlanWithChildren {
+
+ @Nonnull
+ private final Quantifier.Physical root;
+
+ @Nonnull
+ private final Quantifier.Physical recursive;
+
+ @Nonnull
+ private final Value resultValue;
+
+ @Nonnull
+ private final CorrelationIdentifier priorValueCorrelation;
+
+ public RecordQueryRecursiveDfsPlan(@Nonnull final Quantifier.Physical root,
+ @Nonnull final Quantifier.Physical recursive,
+ @Nonnull final CorrelationIdentifier priorValueCorrelation) {
+ this.root = root;
+ this.recursive = recursive;
+ this.priorValueCorrelation = priorValueCorrelation;
+ this.resultValue = RecordQuerySetPlan.mergeValues(ImmutableList.of(root, recursive));
+ }
+
+ @Override
+ public boolean canCorrelate() {
+ return true;
+ }
+
+ @Override
+ public int planHash(@Nonnull final PlanHashMode hashMode) {
+ return 0;
+ }
+
+ @Override
+ public int getRelationalChildCount() {
+ return 2;
+ }
+
+ @Nonnull
+ @Override
+ public Set getCorrelatedToWithoutChildren() {
+ return resultValue.getCorrelatedToWithoutChildren();
+ }
+
+ @Nonnull
+ @Override
+ public RecordCursor executePlan(@Nonnull final FDBRecordStoreBase store, @Nonnull final EvaluationContext context,
+ @Nullable final byte[] continuation, @Nonnull final ExecuteProperties executeProperties) {
+ final var nestedExecuteProperties = executeProperties.clearSkipAndLimit();
+ return RecursiveCursor.create(
+ rootContinuation ->
+ root.getRangesOverPlan().executePlan(store, context, rootContinuation, nestedExecuteProperties),
+ (parentResult, depth, innerContinuation) -> {
+ final var child2Context = context.withBinding(Bindings.Internal.CORRELATION.bindingName(priorValueCorrelation.getId()), parentResult);
+ return recursive.getRangesOverPlan().executePlan(store, child2Context, innerContinuation, nestedExecuteProperties);
+ },
+ null,
+ continuation
+ ).skipThenLimit(executeProperties.getSkip(), executeProperties.getReturnedRowLimit())
+ .map(RecursiveCursor.RecursiveValue::getValue);
+ }
+
+ @Nonnull
+ @Override
+ public List getChildren() {
+ return ImmutableList.of(root.getRangesOverPlan(), recursive.getRangesOverPlan());
+ }
+
+ @Nonnull
+ @Override
+ public AvailableFields getAvailableFields() {
+ return AvailableFields.ALL_FIELDS;
+ }
+
+ @Nonnull
+ @Override
+ public Message toProto(@Nonnull final PlanSerializationContext serializationContext) {
+ return null;
+ }
+
+ @Nonnull
+ @Override
+ public PRecordQueryPlan toRecordQueryPlanProto(@Nonnull final PlanSerializationContext serializationContext) {
+ return null;
+ }
+
+ @Nonnull
+ @Override
+ public PlannerGraph rewritePlannerGraph(@Nonnull final List extends PlannerGraph> childGraphs) {
+ return PlannerGraph.fromNodeAndChildGraphs(
+ new PlannerGraph.OperatorNodeWithInfo(this, NodeInfo.RECURSIVE_DFS_OPERATOR),
+ childGraphs);
+ }
+
+ @Override
+ public boolean isReverse() {
+ return false;
+ }
+
+ @Override
+ public void logPlanStructure(final StoreTimer timer) {
+ }
+
+ @Override
+ public int getComplexity() {
+ return 0;
+ }
+
+ @Nonnull
+ @Override
+ public Value getResultValue() {
+ return resultValue;
+ }
+
+ @Nonnull
+ @Override
+ public List extends Quantifier> getQuantifiers() {
+ return ImmutableList.of(root, recursive);
+ }
+
+ @Override
+ public boolean equalsWithoutChildren(@Nonnull final RelationalExpression otherExpression, @Nonnull final AliasMap equivalences) {
+ if (this == otherExpression) {
+ return true;
+ }
+ if (!(otherExpression instanceof RecordQueryRecursiveDfsPlan)) {
+ return false;
+ }
+ final var otherRecursiveDfsPlan = (RecordQueryRecursiveDfsPlan)otherExpression;
+ return this.priorValueCorrelation.equals(otherRecursiveDfsPlan.priorValueCorrelation);
+ }
+
+ @Override
+ public int hashCodeWithoutChildren() {
+ return Objects.hash(42);
+ }
+
+ @Nonnull
+ @Override
+ public RelationalExpression translateCorrelations(@Nonnull final TranslationMap translationMap, final boolean shouldSimplifyValues,
+ @Nonnull final List extends Quantifier> translatedQuantifiers) {
+ Verify.verify(translatedQuantifiers.size() == 2);
+ Verify.verify(!translationMap.containsSourceAlias(priorValueCorrelation));
+ final var translatedRootQuantifier = translatedQuantifiers.get(0).narrow(Quantifier.Physical.class);
+ final var translatedRecursiveQuantifier = translatedQuantifiers.get(1).narrow(Quantifier.Physical.class);
+ return new RecordQueryRecursiveDfsPlan(translatedRootQuantifier, translatedRecursiveQuantifier,
+ priorValueCorrelation);
+ }
+}
diff --git a/fdb-record-layer-core/src/main/proto/record_cursor.proto b/fdb-record-layer-core/src/main/proto/record_cursor.proto
index 04095bf779..f0a7df10d5 100644
--- a/fdb-record-layer-core/src/main/proto/record_cursor.proto
+++ b/fdb-record-layer-core/src/main/proto/record_cursor.proto
@@ -165,3 +165,11 @@ message OneOfTypedState {
message RangeCursorContinuation {
optional int64 nextPosition = 1;
}
+
+message RecursiveContinuation {
+ message LevelCursor {
+ optional bytes continuation = 1;
+ optional bytes check_value = 2;
+ }
+ repeated LevelCursor levels = 1;
+}
diff --git a/fdb-record-layer-core/src/main/proto/record_query_plan.proto b/fdb-record-layer-core/src/main/proto/record_query_plan.proto
index 5936ddd6f8..ee3500f022 100644
--- a/fdb-record-layer-core/src/main/proto/record_query_plan.proto
+++ b/fdb-record-layer-core/src/main/proto/record_query_plan.proto
@@ -254,6 +254,7 @@ message PValue {
PRangeValue range_value = 48;
PFirstOrDefaultStreamingValue first_or_default_streaming_value = 49;
PEvaluatesToValue evaluates_to_value = 50;
+ PRecursivePriorValue recursive_prior_value = 51;
}
}
@@ -456,6 +457,11 @@ message PEvaluatesToValue {
optional PEvaluation evaluation = 2;
}
+message PRecursivePriorValue {
+ optional string alias = 1;
+ optional PType result_type = 2;
+}
+
message PFieldValue {
optional PValue child_value = 1;
optional PFieldPath field_path = 2;
diff --git a/yaml-tests/src/test/resources/recursive-cte.yamsql b/yaml-tests/src/test/resources/recursive-cte.yamsql
index 0a8ed60bb7..19195e67d7 100644
--- a/yaml-tests/src/test/resources/recursive-cte.yamsql
+++ b/yaml-tests/src/test/resources/recursive-cte.yamsql
@@ -29,7 +29,6 @@ test_block:
select id, parent from t1 where parent = -1
union all
select b.id, b.parent from c1 as a, t1 as b where a.id = b.parent) select id from c1
- - explain: "RUNION q0, q1 { INITIAL { ISCAN(PARENTIDX [EQUALS promote(@c15 AS LONG)]) | INSERT INTO TEMP q1 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q2 -> { TEMP SCAN base() | FILTER _.ID EQUALS q2.PARENT AS q3 RETURN (q2.ID AS ID, q2.PARENT AS PARENT) } | INSERT INTO TEMP q1 }} | MAP (_.ID AS ID)"
- unorderedResult: [{ID: 1},
{ID: 10},
{ID: 20},
@@ -39,93 +38,4 @@ test_block:
{ID: 100},
{ID: 210},
{ID: 250}]
- -
- - query: with recursive c1 as (
- select id, parent from t1 where id = 250
- union all
- select b.id, b.parent from c1 as a, t1 as b where a.parent = b.id) select id from c1
- - explain: "RUNION q0, q1 { INITIAL { ISCAN(CHILDIDX [EQUALS promote(@c15 AS LONG)]) | INSERT INTO TEMP q1 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q2 -> { TEMP SCAN base() | FILTER _.PARENT EQUALS q2.ID AS q3 RETURN (q2.ID AS ID, q2.PARENT AS PARENT) } | INSERT INTO TEMP q1 }} | MAP (_.ID AS ID)"
- - result: [{ID: 250},
- {ID: 50},
- {ID: 10},
- {ID: 1}]
- -
- - query: with recursive allDescendants as (
- with recursive ancestorsOf250 as (
- select id, parent from t1 where id = 250
- union all
- select b.id, b.parent from ancestorsOf250 as a, t1 as b where a.parent = b.id) select id, parent from ancestorsOf250
- union all
- select b.id, b.parent from allDescendants as a, t1 as b where a.id = b.parent) select id, parent from allDescendants
- - explain: "RUNION q0, q1 { INITIAL { RUNION q2, q3 { INITIAL { ISCAN(CHILDIDX [EQUALS promote(@c20 AS LONG)]) | INSERT INTO TEMP q3 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q4 -> { TEMP SCAN base() | FILTER _.PARENT EQUALS q4.ID AS q5 RETURN (q4.ID AS ID, q4.PARENT AS PARENT) } | INSERT INTO TEMP q3 }} | MAP (_.ID AS ID, _.PARENT AS PARENT) | INSERT INTO TEMP q1 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q6 -> { TEMP SCAN base() | FILTER _.ID EQUALS q6.PARENT AS q7 RETURN (q6.ID AS ID, q6.PARENT AS PARENT) } | INSERT INTO TEMP q1 }} | MAP (_.ID AS ID, _.PARENT AS PARENT)"
- - result: [{250, 50},
- {50, 10},
- {10, 1},
- {1, -1},
- {10, 1},
- {20, 1},
- {40, 10},
- {50, 10},
- {70, 10},
- {250, 50},
- {40, 10},
- {50, 10},
- {70, 10},
- {100, 20},
- {210, 20},
- {250, 50},
- {250, 50}]
- -
- - query: with recursive c1 as (
- select id, parent from t1 where parent = -1
- union all
- select b.id, b.parent from c1 as a, t1 as b where a.id = b.parent) select id from c1
- - explain: RUNION q0, q1 { INITIAL { ISCAN(PARENTIDX [EQUALS promote(@c15 AS LONG)]) | INSERT INTO TEMP q1 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q2 -> { TEMP SCAN base() | FILTER _.ID EQUALS q2.PARENT AS q3 RETURN (q2.ID AS ID, q2.PARENT AS PARENT) } | INSERT INTO TEMP q1 }} | MAP (_.ID AS ID)
- - maxRows: 1
- - result: [{ID: 1}]
- - result: [{ID: 10}]
- - result: [{ID: 20}]
- - result: [{ID: 40}]
- - result: [{ID: 50}]
- - result: [{ID: 70}]
- - result: [{ID: 100}]
- - result: [{ID: 210}]
- - result: [{ID: 250}]
- - result: []
- -
- - query: with recursive allDescendants as (
- with recursive ancestorsOf250 as (
- select id, parent from t1 where id = 250
- union all
- select b.id, b.parent from ancestorsOf250 as a, t1 as b where a.parent = b.id) select id, parent from ancestorsOf250
- union all
- select b.id, b.parent from allDescendants as a, t1 as b where a.id = b.parent) select id, parent from allDescendants
- - explain: "RUNION q0, q1 { INITIAL { RUNION q2, q3 { INITIAL { ISCAN(CHILDIDX [EQUALS promote(@c20 AS LONG)]) | INSERT INTO TEMP q3 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q4 -> { TEMP SCAN base() | FILTER _.PARENT EQUALS q4.ID AS q5 RETURN (q4.ID AS ID, q4.PARENT AS PARENT) } | INSERT INTO TEMP q3 }} | MAP (_.ID AS ID, _.PARENT AS PARENT) | INSERT INTO TEMP q1 } RECURSIVE { ISCAN(CHILDIDX <,>) | FLATMAP q6 -> { TEMP SCAN base() | FILTER _.ID EQUALS q6.PARENT AS q7 RETURN (q6.ID AS ID, q6.PARENT AS PARENT) } | INSERT INTO TEMP q1 }} | MAP (_.ID AS ID, _.PARENT AS PARENT)"
- - maxRows: 1
- - result: [{250, 50}]
- - result: [{50, 10}]
- - result: [{10, 1}]
- - result: [{1, -1}]
- - result: [{10, 1}]
- - result: [{20, 1}]
- - result: [{40, 10}]
- - result: [{50, 10}]
- - result: [{70, 10}]
- - result: [{250, 50}]
- - result: [{40, 10}]
- - result: [{50, 10}]
- - result: [{70, 10}]
- - result: [{100, 20}]
- - result: [{210, 20}]
- - result: [{250, 50}]
- - result: [{250, 50}]
- - result: []
-# -
-# does not currently work due to bug in NLJ planning, see https://github.com/FoundationDB/fdb-record-layer/issues/2997
-# - query: with recursive c1 as (
-# select id, parent from t1 where id = 250
-# union all
-# select b.id, b.parent from c1 as a, t1 as b where a.parent = b.id and b.id > 40) select id from c1
-# - result: [{ID: 250},
-# {ID: 50}]
...