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 getResultValues() { + return Values.deconstructRecord(getResultValue()); + } + + @Nonnull + @Override + public List 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 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 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 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 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 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 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 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 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 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}] ...