diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ComparisonRange.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ComparisonRange.java index 6568810a71..9bb6a8bbdf 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ComparisonRange.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ComparisonRange.java @@ -380,7 +380,6 @@ public MergeResult merge(@Nonnull final Comparisons.Comparison comparison) { @Nonnull public MergeResult merge(@Nonnull ComparisonRange comparisonRange) { - final ImmutableList.Builder residualPredicatesBuilder = ImmutableList.builder(); if (comparisonRange.isEmpty()) { return MergeResult.of(this); } @@ -389,15 +388,16 @@ public MergeResult merge(@Nonnull ComparisonRange comparisonRange) { return MergeResult.of(comparisonRange); } - if (isEquality()) { + if (comparisonRange.isEquality()) { return merge(comparisonRange.getEqualityComparison()); } - Verify.verify(isInequality()); + Verify.verify(comparisonRange.isInequality()); final List comparisons = Objects.requireNonNull(comparisonRange.getInequalityComparisons()); ComparisonRange resultRange = this; + final ImmutableList.Builder residualPredicatesBuilder = ImmutableList.builder(); for (final Comparisons.Comparison comparison : comparisons) { MergeResult mergeResult = resultRange.merge(comparison); resultRange = mergeResult.getComparisonRange(); @@ -465,6 +465,9 @@ public static ComparisonRange fromInequalities(@Nonnull Iterable combinedResiduals = ImmutableList.builderWithExpectedSize(residualComparisons.size() + mergedRange.getResidualComparisons().size()) + .addAll(residualComparisons) + .addAll(mergedRange.getResidualComparisons()) + .build(); + + return MergeResult.of(mergedRange.getComparisonRange(), combinedResiduals); + } + } + @Nonnull public ComparisonRange getComparisonRange() { return comparisonRange; @@ -486,15 +506,23 @@ public List getResidualComparisons() { return residualComparisons; } + @Nonnull + public static MergeResult empty() { + return MergeResult.EMPTY; + } + + @Nonnull public static MergeResult of(@Nonnull final ComparisonRange comparisonRange) { return of(comparisonRange, ImmutableList.of()); } + @Nonnull public static MergeResult of(@Nonnull final ComparisonRange comparisonRange, @Nonnull final Comparisons.Comparison residualComparison) { return new MergeResult(comparisonRange, ImmutableList.of(residualComparison)); } + @Nonnull public static MergeResult of(@Nonnull final ComparisonRange comparisonRange, @Nonnull final List residualComparisons) { return new MergeResult(comparisonRange, residualComparisons); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java index d374bc5e11..6edf5116c6 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java @@ -32,6 +32,7 @@ import com.apple.foundationdb.record.query.expressions.Comparisons; 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.ComparisonRange; import com.apple.foundationdb.record.query.plan.cascades.ConstrainedBoolean; import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier; import com.apple.foundationdb.record.query.plan.cascades.PredicateMultiMap.PredicateCompensationFunction; @@ -343,22 +344,40 @@ public Optional impliesCandidatePredicateMaybe(@Nonnull final final var alias = ((WithAlias)candidatePredicateWithValuesAndRanges).getParameterAlias(); final var predicateMappingBuilder = PredicateMapping.regularMappingBuilder(originalQueryPredicate, this, candidatePredicate) - .setPredicateCompensation((ignore, boundParameterPrefixMap, pullUp) -> { - if (boundParameterPrefixMap.containsKey(alias)) { - return PredicateCompensationFunction.noCompensationNeeded(); - } - return computeCompensationFunctionForLeaf(pullUp); - }) .setParameterAlias(alias) .setConstraint(constraint); Verify.verify(isSargable() == compensatedQueryPredicate.isSargable()); + final QueryPredicate residualPredicate; if (compensatedQueryPredicate.isSargable()) { predicateMappingBuilder.setParameterAlias(alias); - predicateMappingBuilder.setComparisonRange( - Iterables.getOnlyElement(compensatedQueryPredicate.getRanges()) - .asComparisonRange()); + final ComparisonRange.MergeResult mergeResult = Iterables.getOnlyElement(compensatedQueryPredicate.getRanges()).asMergedComparisonRange(); + predicateMappingBuilder.setComparisonRange(mergeResult.getComparisonRange()); + if (mergeResult.getResidualComparisons().isEmpty()) { + residualPredicate = null; + } else { + final RangeConstraints.Builder newRangeConstraintsBuilder = RangeConstraints.newBuilder(); + boolean allComparisonsAdded = mergeResult.getResidualComparisons().stream().allMatch(newRangeConstraintsBuilder::addComparisonMaybe); + final Optional newRangeConstraints = newRangeConstraintsBuilder.build(); + if (allComparisonsAdded && newRangeConstraints.isPresent()) { + residualPredicate = new PredicateWithValueAndRanges(((PredicateWithValueAndRanges)candidatePredicate).getValue(), ImmutableSet.of(newRangeConstraints.get())); + } else { + residualPredicate = compensatedQueryPredicate; + } + } + } else { + residualPredicate = null; } + predicateMappingBuilder.setPredicateCompensation(((partialMatch, boundParameterPrefixMap, pullUp) -> { + if (boundParameterPrefixMap.containsKey(alias)) { + if (residualPredicate == null) { + return PredicateCompensationFunction.noCompensationNeeded(); + } else { + return residualPredicate.computeCompensationFunction(partialMatch, originalQueryPredicate, boundParameterPrefixMap, pullUp); + } + } + return computeCompensationFunctionForLeaf(pullUp); + })); return Optional.of(predicateMappingBuilder.build()); } else { return Optional.empty(); @@ -374,20 +393,38 @@ public Optional impliesCandidatePredicateMaybe(@Nonnull final final var alias = ((WithAlias)candidatePredicateWithValuesAndRanges).getParameterAlias(); final var predicateMappingBuilder = PredicateMapping.regularMappingBuilder(originalQueryPredicate, this, candidatePredicate) - .setPredicateCompensation((ignore, boundParameterPrefixMap, pullUp) -> { - if (boundParameterPrefixMap.containsKey(alias)) { - return PredicateCompensationFunction.noCompensationNeeded(); - } - return computeCompensationFunctionForLeaf(pullUp); - }) .setConstraint(constraint.compose(captureConstraint(candidatePredicateWithValuesAndRanges))); Verify.verify(isSargable() == compensatedQueryPredicate.isSargable()); + final QueryPredicate residualPredicate; if (compensatedQueryPredicate.isSargable()) { predicateMappingBuilder.setParameterAlias(alias); - predicateMappingBuilder.setComparisonRange( - Iterables.getOnlyElement(compensatedQueryPredicate.getRanges()) - .asComparisonRange()); + final ComparisonRange.MergeResult mergeResult = Iterables.getOnlyElement(compensatedQueryPredicate.getRanges()).asMergedComparisonRange(); + predicateMappingBuilder.setComparisonRange(mergeResult.getComparisonRange()); + if (mergeResult.getResidualComparisons().isEmpty()) { + residualPredicate = null; + } else { + final RangeConstraints.Builder newRangeConstraintsBuilder = RangeConstraints.newBuilder(); + boolean allComparisonsAdded = mergeResult.getResidualComparisons().stream().allMatch(newRangeConstraintsBuilder::addComparisonMaybe); + final Optional newRangeConstraints = newRangeConstraintsBuilder.build(); + if (allComparisonsAdded && newRangeConstraints.isPresent()) { + residualPredicate = new PredicateWithValueAndRanges(((PredicateWithValueAndRanges)candidatePredicate).getValue(), ImmutableSet.of(newRangeConstraints.get())); + } else { + residualPredicate = compensatedQueryPredicate; + } + } + } else { + residualPredicate = null; } + predicateMappingBuilder.setPredicateCompensation(((partialMatch, boundParameterPrefixMap, pullUp) -> { + if (boundParameterPrefixMap.containsKey(alias)) { + if (residualPredicate == null) { + return PredicateCompensationFunction.noCompensationNeeded(); + } else { + return residualPredicate.computeCompensationFunction(partialMatch, compensatedQueryPredicate, boundParameterPrefixMap, pullUp); + } + } + return computeCompensationFunctionForLeaf(pullUp); + })); return Optional.of(predicateMappingBuilder.build()); } else { return Optional.of( diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/RangeConstraints.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/RangeConstraints.java index 050bb65dbd..76c6e142b2 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/RangeConstraints.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/RangeConstraints.java @@ -149,18 +149,21 @@ public Set getDeferredRanges() { } /** - * Returns an equivalent {@link ComparisonRange}. + * Returns an equivalent {@link ComparisonRange} along with any residual comparisons that + * could not be pushed into the range. Values that cannot be combined into the single range + * will need to be compensated for by the caller. * Note: This method is created for compatibility reasons. * - * @return An equivalent {@link ComparisonRange}. + * @return An equivalent {@link ComparisonRange} along with a set of residual comparisons + * that require compensation */ @Nonnull - public ComparisonRange asComparisonRange() { - var resultRange = ComparisonRange.EMPTY; - for (final var comparison : getComparisons()) { - resultRange = resultRange.merge(comparison).getComparisonRange(); + public ComparisonRange.MergeResult asMergedComparisonRange() { + ComparisonRange.MergeResult mergeResult = ComparisonRange.MergeResult.empty(); + for (final Comparisons.Comparison comparison : getComparisons()) { + mergeResult = mergeResult.merge(comparison); } - return resultRange; + return mergeResult; } @Nonnull diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/MergeComparisonRangesTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/MergeComparisonRangesTest.java new file mode 100644 index 0000000000..6b0b249d3d --- /dev/null +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/MergeComparisonRangesTest.java @@ -0,0 +1,540 @@ +/* + * MergeComparisonRangesTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2026 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; + +import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.query.expressions.Comparisons; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link ComparisonRange#merge}. + * + *

+ * These tests exercise all meaningful paths through both the + * {@link ComparisonRange#merge(Comparisons.Comparison)} and + * {@link ComparisonRange.MergeResult#merge(Comparisons.Comparison)} methods, including cases + * that produce residual comparisons. + *

+ */ +class MergeComparisonRangesTest { + + // + // ComparisonRange.merge(Comparison) + // + + /** + * Merging any comparison into an EMPTY range always succeeds: the result is a range + * derived from the comparison with no residuals. + */ + @Test + void mergeIntoEmptyRangeProducesRangeWithNoResiduals() { + final Comparisons.Comparison eq = eq(42); + final ComparisonRange.MergeResult result = ComparisonRange.EMPTY.merge(eq); + + assertThat(result.getResidualComparisons()) + .isEmpty(); + assertThat(result.getComparisonRange().isEquality()) + .isTrue(); + assertThat(result.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + } + + /** + * Merging an inequality comparison into an EMPTY range produces an INEQUALITY range. + */ + @Test + void mergeInequalityIntoEmptyRangeProducesInequalityRange() { + final Comparisons.Comparison gt = gt(10); + final ComparisonRange.MergeResult result = ComparisonRange.EMPTY.merge(gt); + + assertThat(result.getResidualComparisons()) + .isEmpty(); + assertThat(result.getComparisonRange().isInequality()) + .isTrue(); + assertThatThrownBy(() -> result.getComparisonRange().getEqualityComparison()) + .isInstanceOf(RecordCoreException.class) + .hasMessageContaining("tried to get non-existent equality comparison from ComparisonRange"); + assertThat(result.getComparisonRange().getInequalityComparisons()) + .containsExactly(gt); + } + + /** + * A {@code NONE}-type comparison (e.g., NOT_EQUALS) cannot be merged into any range. + * It is always returned as a residual and the range is unchanged. + */ + @Test + void mergeNoneTypeComparisonAlwaysProducesResidual() { + final Comparisons.Comparison ne = ne(5); + + // into EMPTY + final ComparisonRange.MergeResult fromEmpty = ComparisonRange.EMPTY.merge(ne); + assertThat(fromEmpty.getResidualComparisons()) + .containsExactly(ne); + assertThat(fromEmpty.getComparisonRange().isEmpty()) + .isTrue(); + + // into EQUALITY + final ComparisonRange equalityRange = ComparisonRange.from(eq(5)); + final ComparisonRange.MergeResult fromEquality = equalityRange.merge(ne); + assertThat(fromEquality.getResidualComparisons()) + .containsExactly(ne); + assertThat(fromEquality.getComparisonRange()) + .isEqualTo(equalityRange); + + // into INEQUALITY + final ComparisonRange inequalityRange = ComparisonRange.from(gt(3)); + final ComparisonRange.MergeResult fromInequality = inequalityRange.merge(ne); + assertThat(fromInequality.getResidualComparisons()) + .containsExactly(ne); + assertThat(fromInequality.getComparisonRange()) + .isEqualTo(inequalityRange); + } + + /** + * Merging the same equality comparison into an EQUALITY range is idempotent. There should be + * no residuals, and the equality comparison should be combined with the existing one. + */ + @Test + void mergingSameEqualityIntoEqualityRangeIsIdempotent() { + final Comparisons.Comparison eq = eq(7); + final ComparisonRange range = ComparisonRange.from(eq); + final ComparisonRange.MergeResult merged = range.merge(eq); + + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange()) + .isEqualTo(range); + } + + /** + * Merging a different equality comparison into an EQUALITY range cannot be absorbed. + * The incoming comparison becomes a residual. Theoretically, this could become a contradiction. + */ + @Test + void mergingDifferentEqualityIntoEqualityRangeProducesResidual() { + final Comparisons.Comparison eq7 = eq(7); + final Comparisons.Comparison eq9 = eq(9); + + // First, equals 9, then equals 7 + final ComparisonRange range7 = ComparisonRange.from(eq7); + final ComparisonRange.MergeResult merge9Into7 = range7.merge(eq9); + assertThat(merge9Into7.getResidualComparisons()) + .containsExactly(eq9); + assertThat(merge9Into7.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merge9Into7.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq7); + + // Second, equals 7, then equals 9 + final ComparisonRange range9 = ComparisonRange.from(eq9); + final ComparisonRange.MergeResult merge7Into9 = range9.merge(eq7); + assertThat(merge7Into9.getResidualComparisons()) + .containsExactly(eq7); + assertThat(merge7Into9.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merge7Into9.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq9); + } + + /** + * Merging an inequality comparison into an EQUALITY range. The inequality cannot be + * absorbed by the equality; it becomes a residual. + */ + @Test + void mergingInequalityIntoEqualityRangeProducesResidual() { + final Comparisons.Comparison eq = eq(7); + final Comparisons.Comparison lt = lt(10); + final ComparisonRange range = ComparisonRange.from(eq); + final ComparisonRange.MergeResult merged = range.merge(lt); + + assertThat(merged.getResidualComparisons()) + .containsExactly(lt); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + } + + /** + * Merging an equality comparison into an INEQUALITY range: the result becomes a new + * EQUALITY range (the equality wins), and all existing inequality comparisons are + * returned as residuals. + */ + @Test + void mergingEqualityIntoInequalityRangePromotesToEqualityWithResiduals() { + final Comparisons.Comparison gt = gt(3); + final Comparisons.Comparison lt = lt(10); + final Comparisons.Comparison eq = eq(7); + final ComparisonRange range = ComparisonRange.fromInequalities(List.of(gt, lt)); + final ComparisonRange.MergeResult merged = range.merge(eq); + + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(gt, lt); + } + + /** + * Merging a new (non-duplicate) inequality into an INEQUALITY range absorbs it into the + * range without producing residuals. + */ + @Test + void mergingNewInequalityIntoInequalityRangeExpandsRangeWithNoResiduals() { + final Comparisons.Comparison gt = gt(3); + final Comparisons.Comparison lt = lt(10); + final ComparisonRange range = ComparisonRange.from(gt); + final ComparisonRange.MergeResult merged = range.merge(lt); + + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange().isInequality()) + .isTrue(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .hasSize(2) + .containsExactlyInAnyOrder(gt, lt); + } + + /** + * Merging a duplicate inequality into an INEQUALITY range is idempotent. + * There are no residuals, and the range size unchanged. + */ + @Test + void mergingDuplicateInequalityIntoInequalityRangeIsIdempotent() { + final Comparisons.Comparison gt = gt(3); + final ComparisonRange range = ComparisonRange.from(gt); + final ComparisonRange.MergeResult merged = range.merge(gt); + + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .hasSize(1); + } + + /** + * Merging could result in a range being narrowed. Theoretically, this could + * reduce the number of inequality comparisons, removing the now extraneous + * ones. + */ + @Test + void mergingInequalityCouldNarrowRange() { + final Comparisons.Comparison lt10 = lt(10); + final Comparisons.Comparison gt = gt(3); + final ComparisonRange merged = ComparisonRange.fromInequalities(List.of(gt, lt10)); + + final Comparisons.Comparison lt8 = lt(8); + final ComparisonRange.MergeResult result = merged.merge(lt8); + + assertThat(result.getResidualComparisons()) + .isEmpty(); + assertThat(result.getComparisonRange().getInequalityComparisons()) + // Could be modified to just gt and lt8 instead + .containsExactlyInAnyOrder(gt, lt8, lt10); + } + + // + // ComparisonRange.MergeResult.merge(Comparison) -- residual accumulation + // + + /** + * When the existing MergeResult has no residuals and the new merge also produces no + * residuals, the combined result has no residuals. + */ + @Test + void mergeResultWithNoResidualsStaysCleanWhenNewMergeHasNone() { + ComparisonRange.MergeResult merged = ComparisonRange.MergeResult.empty(); + + final Comparisons.Comparison gt = gt(1); + merged = merged.merge(gt); + + final Comparisons.Comparison lt = lt(50); + merged = merged.merge(lt); + + assertThat(merged.getResidualComparisons()).isEmpty(); + assertThat(merged.getComparisonRange().isInequality()) + .isTrue(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lt); + } + + /** + * When the existing MergeResult already has residuals and the new merge produces none, + * the existing residuals are preserved unchanged. + */ + @Test + void mergeResultPreservesExistingResidualsWhenNewMergeHasNone() { + final Comparisons.Comparison ne = ne(99); // NONE type -> becomes residual + final Comparisons.Comparison gt = gt(1); + final Comparisons.Comparison lt = lt(50); + + // Start with EMPTY, merge a NONE comparison -> residuals = [ne] + ComparisonRange.MergeResult merged = ComparisonRange.MergeResult.empty(); + merged = merged.merge(ne); + assertThat(merged.getResidualComparisons()) + .containsExactly(ne); + + // Now merge an inequality that CAN be absorbed -> still residuals = [ne] + merged = merged.merge(gt); + assertThat(merged.getResidualComparisons()) + .containsExactly(ne); + + // And another absorbable inequality. + merged = merged.merge(lt); + assertThat(merged.getResidualComparisons()) + .containsExactly(ne); + assertThat(merged.getComparisonRange().isInequality()) + .isTrue(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lt); + } + + /** + * When the existing MergeResult has residuals AND the new merge also produces residuals, + * both sets are accumulated together. + */ + @Test + void mergeResultAccumulatesResidualsFromBothExistingAndNewMerge() { + final Comparisons.Comparison ne1 = ne(1); + final Comparisons.Comparison ne2 = ne(2); + final Comparisons.Comparison gt = gt(5); + final Comparisons.Comparison eq = eq(10); + + // Build a MergeResult with an existing residual. + ComparisonRange.MergeResult merged = ComparisonRange.MergeResult.empty(); + merged = merged.merge(ne1); // ne1 -> residual; range stays EMPTY + assertThat(merged.getResidualComparisons()) + .containsExactly(ne1); + + // Now merge an equality so that the range becomes EQUALITY. + merged = merged.merge(eq); + assertThat(merged.getResidualComparisons()) + .containsExactly(ne1); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + + // Now merge another NONE-type comparison -- it also becomes a residual. + merged = merged.merge(ne2); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(ne1, ne2); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + + // And an inequality -- into an EQUALITY range it also becomes a residual. + merged = merged.merge(gt); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(ne1, ne2, gt); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + } + + /** + * Calling {@link ComparisonRange.MergeResult#empty()} produces a result with an empty + * range and no residual comparisons. + */ + @Test + void emptyMergeResultHasEmptyRangeAndNoResiduals() { + final ComparisonRange.MergeResult empty = ComparisonRange.MergeResult.empty(); + + assertThat(empty.getComparisonRange().isEmpty()) + .isTrue(); + assertThat(empty.getResidualComparisons()) + .isEmpty(); + } + + // + // ComparisonRange.merge(ComparisonRange) -- range-level merges + // + + /** + * Merging an EMPTY range into any range is a no-op: the original range is returned as-is. + */ + @Test + void mergingEmptyComparisonRangeIsNoOp() { + final ComparisonRange range = ComparisonRange.from(gt(5)); + final ComparisonRange.MergeResult merged = range.merge(ComparisonRange.EMPTY); + + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange()) + .isEqualTo(range); + } + + /** + * Merging a non-empty range into an EMPTY range returns the non-empty range. + */ + @Test + void mergingIntoEmptyComparisonRangeReturnsOther() { + final ComparisonRange other = ComparisonRange.from(gt(5)); + final ComparisonRange.MergeResult result = ComparisonRange.EMPTY.merge(other); + + assertThat(result.getResidualComparisons()) + .isEmpty(); + assertThat(result.getComparisonRange()) + .isEqualTo(other); + } + + /** + * Merging two INEQUALITY ranges whose comparisons are all compatible accumulates all + * comparisons with no residuals. + */ + @Test + void mergingCompatibleInequalityRangesAccumulatesComparisons() { + final Comparisons.Comparison gt = gt(3); + final Comparisons.Comparison lt = lt(20); + final Comparisons.Comparison gte = gte(5); + + final ComparisonRange range1 = ComparisonRange.fromInequalities(List.of(gt, lt)); + final ComparisonRange range2 = ComparisonRange.from(gte); + final ComparisonRange.MergeResult result = range1.merge(range2); + + assertThat(result.getResidualComparisons()) + .isEmpty(); + assertThat(result.getComparisonRange().isInequality()) + .isTrue(); + assertThat(result.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lt, gte); + } + + /** + * Merging an INEQUALITY range with an EQUALITY range: the equality absorbs the + * INEQUALITY range, and the prior inequalities appear as residuals. + */ + @Test + void mergingInequalityWithEqualityRangePromotesToEqualityWithResiduals() { + final Comparisons.Comparison gt = gt(3); + final Comparisons.Comparison lt = lt(20); + final Comparisons.Comparison eq = eq(10); + + final ComparisonRange inequalityRange = ComparisonRange.fromInequalities(List.of(gt, lt)); + final ComparisonRange equalityRange = ComparisonRange.from(eq); + final ComparisonRange.MergeResult merged = inequalityRange.merge(equalityRange); + + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(gt, lt); + } + + /** + * Full chain test using {@link ComparisonRange.MergeResult#empty()} as the starting + * point, exercising multiple residuals produced from several incompatible comparisons. + */ + @Test + void fullChainWithMultipleResidualsFromIncompatibleComparisons() { + final var gt = gt(0); + final var lte = lte(100); + final var ne1 = ne(10); + final var ne2 = ne(20); + final var eq = eq(50); + + ComparisonRange.MergeResult merged = ComparisonRange.MergeResult.empty(); + + // Absorb gt -> inequality range [gt], no residuals. + merged = merged.merge(gt); + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange().isEquality()) + .isFalse(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactly(gt); + + // Absorb lte -> inequality range [gt, lte], no residuals. + merged = merged.merge(lte); + assertThat(merged.getResidualComparisons()) + .isEmpty(); + assertThat(merged.getComparisonRange().isEquality()) + .isFalse(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lte); + + // Merge ne1 (NONE type) -> becomes residual. + merged = merged.merge(ne1); + assertThat(merged.getResidualComparisons()) + .containsExactly(ne1); + assertThat(merged.getComparisonRange().isEquality()) + .isFalse(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lte); + + // Merge ne2 (NONE type) -> also becomes residual; total = 2. + merged = merged.merge(ne2); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(ne1, ne2); + assertThat(merged.getComparisonRange().isEquality()) + .isFalse(); + assertThat(merged.getComparisonRange().getInequalityComparisons()) + .containsExactlyInAnyOrder(gt, lte); + + // Merge eq into an INEQUALITY range -> eq wins and the existing inequalities [gt, lte] + // become residuals, adding 2 more; total = 4. + merged = merged.merge(eq); + assertThat(merged.getResidualComparisons()) + .containsExactlyInAnyOrder(ne1, ne2, gt, lte); + assertThat(merged.getComparisonRange().isEquality()) + .isTrue(); + assertThat(merged.getComparisonRange().getEqualityComparison()) + .isEqualTo(eq); + } + + // + // Helpers + // + + private static Comparisons.Comparison eq(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.EQUALS, value); + } + + private static Comparisons.Comparison ne(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.NOT_EQUALS, value); + } + + private static Comparisons.Comparison gt(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.GREATER_THAN, value); + } + + private static Comparisons.Comparison gte(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.GREATER_THAN_OR_EQUALS, value); + } + + private static Comparisons.Comparison lt(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.LESS_THAN, value); + } + + private static Comparisons.Comparison lte(int value) { + return new Comparisons.SimpleComparison(Comparisons.Type.LESS_THAN_OR_EQUALS, value); + } +} diff --git a/yaml-tests/src/test/resources/join-tests.metrics.binpb b/yaml-tests/src/test/resources/join-tests.metrics.binpb new file mode 100644 index 0000000000..2677d2e2f2 --- /dev/null +++ b/yaml-tests/src/test/resources/join-tests.metrics.binpb @@ -0,0 +1,196 @@ +) + + +join-testsvEXPLAIN select emp.id, fname from emp, dept where emp.dept_id = dept.id and dept.id in (1, 2) and emp.id not in (1, 5)( +1 P(08@LISCAN(DEPTNAME <,>) | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(EMPDEPT [EQUALS q0.ID] -> [DEPT_ID: KEY:[0], ID: KEY:[2]]) | FILTER NOT _.ID IN promote(@c35 AS ARRAY(LONG)) AND q0.ID EQUALS q1 | FETCH AS q2 RETURN q2 } AS q3 RETURN (q3.ID AS ID, q3.FNAME AS FNAME) }%digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Nested Loop Join
FLATMAP (q2.ID AS ID, q2.FNAME AS FNAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME)" ]; + 2 [ label=<
Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 3 [ label=<
Index
DEPTNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 4 [ label=<
Nested Loop Join
FLATMAP q599
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 5 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 6 [ label=<
Value Computation
EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG)))
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG)" ]; + 7 [ label=<
Predicate Filter
WHERE NOT q639.ID IN promote(@c35 AS ARRAY(LONG)) AND q6.ID EQUALS q382
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 8 [ label=<
Covering Index Scan
comparisons: [EQUALS q6.ID]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 9 [ label=<
Index
EMPDEPT
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ label=< q599> label="q599" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ label=< q382> label="q382" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 5 [ label=< q641> label="q641" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 8 -> 7 [ label=< q639> label="q639" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 9 -> 8 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 1 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + { + 7 -> 4 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 6 -> 5 [ color="red" style="invis" ]; + } + { + 8 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + 7 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 2 -> 4 [ color="red" style="invis" ]; + } +}0 + + +join-testsvEXPLAIN select emp.id, fname from emp, dept where dept.id = emp.dept_id and dept.id in (1, 2) and emp.id not in (1, 5)/ +* C(08@FCOVERING(EMPDEPT <,> -> [DEPT_ID: KEY:[0], ID: KEY:[2]]) | FILTER NOT _.ID IN promote(@c35 AS ARRAY(LONG)) | FETCH | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(DEPTNAME <,> -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.ID EQUALS q1 AND _.ID EQUALS q0.DEPT_ID | MAP (1 AS _0) AS q2 RETURN (1 AS _0) } AS q3 RETURN (q0.ID AS ID, q0.FNAME AS FNAME) },digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Nested Loop Join
FLATMAP (q2.ID AS ID, q2.FNAME AS FNAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME)" ]; + 2 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 3 [ label=<
Predicate Filter
WHERE NOT q608.ID IN promote(@c35 AS ARRAY(LONG))
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 4 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 5 [ label=<
Index
EMPDEPT
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 6 [ label=<
Nested Loop Join
FLATMAP (1 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS _0)" ]; + 7 [ label=<
Value Computation
EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG)))
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG)" ]; + 8 [ label=<
Value Computation
MAP (1 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS _0)" ]; + 9 [ label=<
Predicate Filter
WHERE q488.ID EQUALS q347 AND q488.ID EQUALS q2.DEPT_ID
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 10 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 11 [ label=<
Index
DEPTNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 3 -> 2 [ label=< q610> label="q610" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q608> label="q608" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 6 [ label=< q347> label="q347" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 8 -> 6 [ label=< q353> label="q353" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 9 -> 8 [ label=< q492> label="q492" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 10 -> 9 [ label=< q488> label="q488" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 11 -> 10 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 1 [ label=< q579> label="q579" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + { + 9 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 2 -> 6 [ color="red" style="invis" ]; + } + { + 9 -> 6 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 7 -> 8 [ color="red" style="invis" ]; + } +}0 +y + +join-testskEXPLAIN select emp.id from emp, dept where emp.dept_id = dept.id and dept.id = 1 and emp.id in (10, 20, 30)/ +1 I(0Œ8@ICOVERING(DEPTNAME <,> -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.ID EQUALS promote(@c22 AS LONG) | FETCH | FLATMAP q0 -> { ISCAN(EMPDEPT [EQUALS q0.ID]) | FLATMAP q1 -> { EXPLODE arrayDistinct(promote(@c28 AS ARRAY(LONG))) | FILTER q1.ID EQUALS _ | MAP (1 AS _0) AS q2 RETURN q1 } AS q1 RETURN (q1.ID AS ID) },digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Nested Loop Join
FLATMAP (q2.ID AS ID)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID)" ]; + 2 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 3 [ label=<
Predicate Filter
WHERE q800.ID EQUALS promote(@c22 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 4 [ label=<
Covering Index Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 5 [ label=<
Index
DEPTNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 6 [ label=<
Nested Loop Join
FLATMAP q2
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 7 [ label=<
Index Scan
comparisons: [EQUALS q6.ID]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 8 [ label=<
Value Computation
MAP (1 AS _0)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS _0)" ]; + 9 [ label=<
Index
EMPDEPT
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 10 [ label=<
Predicate Filter
WHERE q2.ID EQUALS q434
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG)" ]; + 11 [ label=<
Value Computation
EXPLODE arrayDistinct(promote(@c28 AS ARRAY(LONG)))
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG)" ]; + 3 -> 2 [ label=< q802> label="q802" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q800> label="q800" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 6 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 8 -> 6 [ label=< q436> label="q436" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 9 -> 7 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 10 -> 8 [ label=< q701> label="q701" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 11 -> 10 [ label=< q434> label="q434" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 1 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + { + 10 -> 6 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 7 -> 8 [ color="red" style="invis" ]; + } + { + 7 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + 10 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + 6 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 2 -> 6 [ color="red" style="invis" ]; + } +} + + +join-testszEXPLAIN select emp.fname, emp.lname from emp, dept where emp.lname > dept.name and dept.name = 'Sales' and emp.lname < 'W' +Ε ǕM(\08@ +ISCAN(DEPTNAME [EQUALS promote(@c26 AS STRING)]) | FLATMAP q0 -> { ISCAN(EMPNAME [[GREATER_THAN q0.NAME && LESS_THAN promote(@c32 AS STRING)]]) AS q1 RETURN (q1.FNAME AS FNAME, q1.LNAME AS LNAME) }digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Nested Loop Join
FLATMAP (q2.FNAME AS FNAME, q2.LNAME AS LNAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS FNAME, STRING AS LNAME)" ]; + 2 [ label=<
Index Scan
comparisons: [EQUALS promote(@c26 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 3 [ label=<
Index
DEPTNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 4 [ label=<
Index Scan
comparisons: [[GREATER_THAN q6.NAME && LESS_THAN promote(@c32 AS STRING)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 5 [ label=<
Index
EMPNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q6> label="q6" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 1 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + { + 4 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 2 -> 4 [ color="red" style="invis" ]; + } +} + + +join-testszEXPLAIN select emp.fname, emp.lname from emp, dept where dept.name < emp.lname and dept.name = 'Sales' and emp.lname < 'W' + & ׅ (d0ȟ8@ISCAN(EMPNAME [[LESS_THAN promote(@c32 AS STRING)]]) | FLATMAP q0 -> { COVERING(DEPTNAME [EQUALS promote(@c26 AS STRING)] -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.NAME LESS_THAN q0.LNAME | FETCH AS q1 RETURN (q0.FNAME AS FNAME, q0.LNAME AS LNAME) }digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Nested Loop Join
FLATMAP (q2.FNAME AS FNAME, q2.LNAME AS LNAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(STRING AS FNAME, STRING AS LNAME)" ]; + 2 [ label=<
Index Scan
comparisons: [[LESS_THAN promote(@c32 AS STRING)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 3 [ label=<
Index
EMPNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS FNAME, STRING AS LNAME, LONG AS DEPT_ID)" ]; + 4 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 5 [ label=<
Predicate Filter
WHERE q164.NAME LESS_THAN q2.LNAME
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 6 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c26 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 7 [ label=<
Index
DEPTNAME
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, STRING AS NAME)" ]; + 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q2> label="q2" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ label=< q166> label="q166" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 5 [ label=< q164> label="q164" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 6 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 1 [ label=< q6> label="q6" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + { + 5 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + } + { + rank=same; + rankDir=LR; + 2 -> 4 [ color="red" style="invis" ]; + } +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/join-tests.metrics.yaml b/yaml-tests/src/test/resources/join-tests.metrics.yaml new file mode 100644 index 0000000000..6338afbac9 --- /dev/null +++ b/yaml-tests/src/test/resources/join-tests.metrics.yaml @@ -0,0 +1,79 @@ +join-tests: +- query: EXPLAIN select emp.id, fname from emp, dept where emp.dept_id = dept.id + and dept.id in (1, 2) and emp.id not in (1, 5) + ref: join-tests.yamsql:334 + explain: 'ISCAN(DEPTNAME <,>) | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 + AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(EMPDEPT [EQUALS q0.ID] -> [DEPT_ID: + KEY:[0], ID: KEY:[2]]) | FILTER NOT _.ID IN promote(@c35 AS ARRAY(LONG)) AND + q0.ID EQUALS q1 | FETCH AS q2 RETURN q2 } AS q3 RETURN (q3.ID AS ID, q3.FNAME + AS FNAME) }' + task_count: 6305 + task_total_time_ms: 577 + transform_count: 1881 + transform_time_ms: 168 + transform_yield_count: 356 + insert_time_ms: 36 + insert_new_count: 1117 + insert_reused_count: 76 +- query: EXPLAIN select emp.id, fname from emp, dept where dept.id = emp.dept_id + and dept.id in (1, 2) and emp.id not in (1, 5) + ref: join-tests.yamsql:343 + explain: 'COVERING(EMPDEPT <,> -> [DEPT_ID: KEY:[0], ID: KEY:[2]]) | FILTER NOT + _.ID IN promote(@c35 AS ARRAY(LONG)) | FETCH | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 + AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(DEPTNAME <,> -> [ID: KEY:[2], + NAME: KEY:[0]]) | FILTER _.ID EQUALS q1 AND _.ID EQUALS q0.DEPT_ID | MAP (1 + AS _0) AS q2 RETURN (1 AS _0) } AS q3 RETURN (q0.ID AS ID, q0.FNAME AS FNAME) + }' + task_count: 5420 + task_total_time_ms: 474 + transform_count: 1630 + transform_time_ms: 142 + transform_yield_count: 312 + insert_time_ms: 37 + insert_new_count: 933 + insert_reused_count: 70 +- query: EXPLAIN select emp.id from emp, dept where emp.dept_id = dept.id and dept.id + = 1 and emp.id in (10, 20, 30) + ref: join-tests.yamsql:352 + explain: 'COVERING(DEPTNAME <,> -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.ID + EQUALS promote(@c22 AS LONG) | FETCH | FLATMAP q0 -> { ISCAN(EMPDEPT [EQUALS + q0.ID]) | FLATMAP q1 -> { EXPLODE arrayDistinct(promote(@c28 AS ARRAY(LONG))) + | FILTER q1.ID EQUALS _ | MAP (1 AS _0) AS q2 RETURN q1 } AS q1 RETURN (q1.ID + AS ID) }' + task_count: 6343 + task_total_time_ms: 520 + transform_count: 1848 + transform_time_ms: 154 + transform_yield_count: 369 + insert_time_ms: 39 + insert_new_count: 1107 + insert_reused_count: 73 +- query: EXPLAIN select emp.fname, emp.lname from emp, dept where emp.lname > dept.name + and dept.name = 'Sales' and emp.lname < 'W' + ref: join-tests.yamsql:428 + explain: ISCAN(DEPTNAME [EQUALS promote(@c26 AS STRING)]) | FLATMAP q0 -> { ISCAN(EMPNAME + [[GREATER_THAN q0.NAME && LESS_THAN promote(@c32 AS STRING)]]) AS q1 RETURN + (q1.FNAME AS FNAME, q1.LNAME AS LNAME) } + task_count: 1145 + task_total_time_ms: 316 + transform_count: 288 + transform_time_ms: 161 + transform_yield_count: 92 + insert_time_ms: 14 + insert_new_count: 161 + insert_reused_count: 10 +- query: EXPLAIN select emp.fname, emp.lname from emp, dept where dept.name < emp.lname + and dept.name = 'Sales' and emp.lname < 'W' + ref: join-tests.yamsql:436 + explain: 'ISCAN(EMPNAME [[LESS_THAN promote(@c32 AS STRING)]]) | FLATMAP q0 -> + { COVERING(DEPTNAME [EQUALS promote(@c26 AS STRING)] -> [ID: KEY:[2], NAME: + KEY:[0]]) | FILTER _.NAME LESS_THAN q0.LNAME | FETCH AS q1 RETURN (q0.FNAME + AS FNAME, q0.LNAME AS LNAME) }' + task_count: 1231 + task_total_time_ms: 80 + transform_count: 312 + transform_time_ms: 25 + transform_yield_count: 100 + insert_time_ms: 2 + insert_new_count: 176 + insert_reused_count: 8 diff --git a/yaml-tests/src/test/resources/join-tests.yamsql b/yaml-tests/src/test/resources/join-tests.yamsql index 8322e4ec0d..06f1cc2571 100644 --- a/yaml-tests/src/test/resources/join-tests.yamsql +++ b/yaml-tests/src/test/resources/join-tests.yamsql @@ -20,7 +20,12 @@ --- schema_template: create table emp(id bigint, fname string, lname string, dept_id bigint, primary key(id)) + create index empDept as select dept_id from emp + create index empName as select lname, fname from emp order by lname, fname + create table dept(id bigint, name string, primary key(id)) + create index deptName as select name from dept + create table project(id bigint, name string, dsc string, emp_id bigint, primary key(id)) create table a(ida bigint, a1 bigint, a2 bigint, a3 bigint, primary key(ida)) create table b(idb bigint, b1 bigint, b2 bigint, b3 bigint, primary key(idb)) @@ -62,6 +67,7 @@ setup: - query: INSERT INTO jud VALUES(1, 4, 5) --- test_block: + name: join-tests tests: - - query: select ida from a where exists (select ida from a where ida = 1); @@ -325,6 +331,16 @@ test_block: - # Join with both IN and NOT IN conditions - query: select emp.id, fname from emp, dept where emp.dept_id = dept.id and dept.id in (1, 2) and emp.id not in (1, 5) + - explain: "ISCAN(DEPTNAME <,>) | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(EMPDEPT [EQUALS q0.ID] -> [DEPT_ID: KEY:[0], ID: KEY:[2]]) | FILTER NOT _.ID IN promote(@c35 AS ARRAY(LONG)) AND q0.ID EQUALS q1 | FETCH AS q2 RETURN q2 } AS q3 RETURN (q3.ID AS ID, q3.FNAME AS FNAME) }" + - unorderedResult: [{ID: 2, FNAME: "Thomas"}, + {ID: 3, FNAME: "Emily"}, + {ID: 4, FNAME: "Amelia"}, + {ID: 6, FNAME: "Chloe"}, + {ID: 7, FNAME: "Charlotte"}] + - + # Join with both IN and NOT IN conditions + - query: select emp.id, fname from emp, dept where dept.id = emp.dept_id and dept.id in (1, 2) and emp.id not in (1, 5) + - explain: "COVERING(EMPDEPT <,> -> [DEPT_ID: KEY:[0], ID: KEY:[2]]) | FILTER NOT _.ID IN promote(@c35 AS ARRAY(LONG)) | FETCH | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c24 AS ARRAY(LONG))) | FLATMAP q1 -> { COVERING(DEPTNAME <,> -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.ID EQUALS q1 AND _.ID EQUALS q0.DEPT_ID | MAP (1 AS _0) AS q2 RETURN (1 AS _0) } AS q3 RETURN (q0.ID AS ID, q0.FNAME AS FNAME) }" - unorderedResult: [{ID: 2, FNAME: "Thomas"}, {ID: 3, FNAME: "Emily"}, {ID: 4, FNAME: "Amelia"}, @@ -333,6 +349,7 @@ test_block: - # Join with IN on non-existent values (should return empty) - query: select emp.id from emp, dept where emp.dept_id = dept.id and dept.id = 1 and emp.id in (10, 20, 30) + - explain: "COVERING(DEPTNAME <,> -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.ID EQUALS promote(@c22 AS LONG) | FETCH | FLATMAP q0 -> { ISCAN(EMPDEPT [EQUALS q0.ID]) | FLATMAP q1 -> { EXPLODE arrayDistinct(promote(@c28 AS ARRAY(LONG))) | FILTER q1.ID EQUALS _ | MAP (1 AS _0) AS q2 RETURN q1 } AS q1 RETURN (q1.ID AS ID) }" - result: [] - # inner join with using: select one field @@ -404,4 +421,33 @@ test_block: - query: select * from (select c11 as c1, c3 from (select c3 - 2 as c11, c3 from jub) as Z) as Y join (select c2 - 1 as c1, c5 from jua) as X using (c1); - supported_version: 4.10.4.0 - result: [{c1: 1, c3: 3, c5: 5}] + + - + # Query using the name indexes to join the emp and dept tables. Note that there are join and non-join predicates on dept.name + - query: select emp.fname, emp.lname from emp, dept where emp.lname > dept.name and dept.name = 'Sales' and emp.lname < 'W' + - explain: "ISCAN(DEPTNAME [EQUALS promote(@c26 AS STRING)]) | FLATMAP q0 -> { ISCAN(EMPNAME [[GREATER_THAN q0.NAME && LESS_THAN promote(@c32 AS STRING)]]) AS q1 RETURN (q1.FNAME AS FNAME, q1.LNAME AS LNAME) }" + - result: [ + { FNAME: "Harry", LNAME: "Smith" }, + ] + + - + # Same query as above but with one comparison flipped. This should not result in a different result set + - query: select emp.fname, emp.lname from emp, dept where dept.name < emp.lname and dept.name = 'Sales' and emp.lname < 'W' + - explain: "ISCAN(EMPNAME [[LESS_THAN promote(@c32 AS STRING)]]) | FLATMAP q0 -> { COVERING(DEPTNAME [EQUALS promote(@c26 AS STRING)] -> [ID: KEY:[2], NAME: KEY:[0]]) | FILTER _.NAME LESS_THAN q0.LNAME | FETCH AS q1 RETURN (q0.FNAME AS FNAME, q0.LNAME AS LNAME) }" + - initialVersionLessThan: !current_version + # Prior to !current_version, we used to drop the "dept.name < emp.lname" predicate due to: https://github.com/FoundationDB/fdb-record-layer/pull/3965 + - unorderedResult: [ + { FNAME: "Thomas", LNAME: "Johnson" }, + { FNAME: "Emily", LNAME: "Martinez" }, + { FNAME: "Amelia", LNAME: "Johnson" }, + { FNAME: "Daniel", LNAME: "Miller" }, + { FNAME: "Chloe", LNAME: "Jones" }, + { FNAME: "Charlotte", LNAME: "Garcia" }, + { FNAME: "Megan", LNAME: "Miller" }, + { FNAME: "Harry", LNAME: "Smith" }, + ] + - initialVersionAtLeast: !current_version + - result: [ + { FNAME: "Harry", LNAME: "Smith" }, + ] ... diff --git a/yaml-tests/src/test/resources/recursive-cte.metrics.binpb b/yaml-tests/src/test/resources/recursive-cte.metrics.binpb index c81d508a7d..09cf916e70 100644 --- a/yaml-tests/src/test/resources/recursive-cte.metrics.binpb +++ b/yaml-tests/src/test/resources/recursive-cte.metrics.binpb @@ -492,32 +492,35 @@ rankDir=LR; 4 -> 3 [ color="red" style="invis" ]; } -}& +}*  -recursive-cte-tests-with-hintsEXPLAIN with recursive c1 as ( select id, parent, 0 as level from t1 use index (childIdx) where id = 250 and parent is not null union all select b.id, b.parent, a.y as level from (select parent, id from t1 use index (childIdx) where parent is not null and id is not null) as b, (select id, parent, level + 1 as y from c1) as a where b.id = a.parent) traversal order pre_order select id, parent, level from c1" -  (C0;8@RUNION-DFS PREORDER q0 { COVERING(CHILDIDX [EQUALS promote(@c24 AS LONG), [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, @c11 AS LEVEL) } { RECURSIVE COVERING(CHILDIDX [EQUALS q0.PARENT, [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS ID, _.PARENT AS PARENT, _.LEVEL AS LEVEL)digraph G { +recursive-cte-tests-with-hintsEXPLAIN with recursive c1 as ( select id, parent, 0 as level from t1 use index (childIdx) where id = 250 and parent is not null union all select b.id, b.parent, a.y as level from (select parent, id from t1 use index (childIdx) where parent is not null and id is not null) as b, (select id, parent, level + 1 as y from c1) as a where b.id = a.parent) traversal order pre_order select id, parent, level from c1& + + +(E0÷8@RUNION-DFS PREORDER q0 { COVERING(CHILDIDX [EQUALS promote(@c24 AS LONG), [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, @c11 AS LEVEL) } { RECURSIVE COVERING(CHILDIDX [EQUALS q0.PARENT, [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | FILTER _.ID NOT_NULL | MAP (_.ID AS ID, _.PARENT AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS ID, _.PARENT AS PARENT, _.LEVEL AS LEVEL)"digraph G { fontname=courier; rankdir=BT; splines=line; 1 [ label=<
Value Computation
MAP (q24.ID AS ID, q24.PARENT AS PARENT, q24.LEVEL AS LEVEL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; 2 [ label=<
Nested Loop Join
RECURSIVE derived(q18, q20)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; - 3 [ label=<
Value Computation
MAP (q153.ID AS ID, q153.PARENT AS PARENT, q12.LEVEL + @c81 AS LEVEL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; - 4 [ label=<
Value Computation
MAP (q140.ID AS ID, q140.PARENT AS PARENT, @c11 AS LEVEL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; - 5 [ label=<
Covering Index Scan
comparisons: [EQUALS q12.PARENT, [NOT_NULL]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; - 6 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c24 AS LONG), [NOT_NULL]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; + 3 [ label=<
Value Computation
MAP (q152.ID AS ID, q152.PARENT AS PARENT, @c11 AS LEVEL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; + 4 [ label=<
Value Computation
MAP (q165.ID AS ID, q165.PARENT AS PARENT, q12.LEVEL + @c81 AS LEVEL)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT, INT AS LEVEL)" ]; + 5 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c24 AS LONG), [NOT_NULL]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; + 6 [ label=<
Predicate Filter
WHERE q103.ID NOT_NULL
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; 7 [ label=<
Index
CHILDIDX
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; - 8 [ label=<
Index
CHILDIDX
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; - 3 -> 2 [ label=< q20> label="q20" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 4 -> 2 [ label=< q18> label="q18" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 5 -> 3 [ label=< q153> label="q153" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 6 -> 4 [ label=< q140> label="q140" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 8 [ label=<
Covering Index Scan
comparisons: [EQUALS q12.PARENT, [NOT_NULL]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; + 9 [ label=<
Index
CHILDIDX
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS ID, LONG AS PARENT)" ]; + 3 -> 2 [ label=< q18> label="q18" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 2 [ label=< q20> label="q20" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 3 [ label=< q152> label="q152" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 4 [ label=< q165> label="q165" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 7 -> 5 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 8 -> 6 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 8 -> 6 [ label=< q103> label="q103" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 9 -> 8 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 2 -> 1 [ label=< q24> label="q24" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; { rank=same; rankDir=LR; - 4 -> 3 [ color="red" style="invis" ]; + 3 -> 4 [ color="red" style="invis" ]; } }  diff --git a/yaml-tests/src/test/resources/recursive-cte.metrics.yaml b/yaml-tests/src/test/resources/recursive-cte.metrics.yaml index 0d74ed9d13..0d5d40e22a 100644 --- a/yaml-tests/src/test/resources/recursive-cte.metrics.yaml +++ b/yaml-tests/src/test/resources/recursive-cte.metrics.yaml @@ -369,16 +369,16 @@ recursive-cte-tests-with-hints: explain: 'RUNION-DFS PREORDER q0 { COVERING(CHILDIDX [EQUALS promote(@c24 AS LONG), [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, @c11 AS LEVEL) } { RECURSIVE COVERING(CHILDIDX [EQUALS q0.PARENT, - [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT - AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS ID, _.PARENT AS PARENT, - _.LEVEL AS LEVEL)' - task_count: 1203 - task_total_time_ms: 18 - transform_count: 297 - transform_time_ms: 7 - transform_yield_count: 67 - insert_time_ms: 0 - insert_new_count: 149 + [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | FILTER _.ID NOT_NULL | MAP + (_.ID AS ID, _.PARENT AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS + ID, _.PARENT AS PARENT, _.LEVEL AS LEVEL)' + task_count: 1255 + task_total_time_ms: 90 + transform_count: 305 + transform_time_ms: 21 + transform_yield_count: 69 + insert_time_ms: 3 + insert_new_count: 156 insert_reused_count: 3 - query: EXPLAIN with recursive allDescendants as ( select * from t1 where id = 50 union all select c.* from allDescendants as p, t1 as c use index (childIdxNoNulls) diff --git a/yaml-tests/src/test/resources/recursive-cte.yamsql b/yaml-tests/src/test/resources/recursive-cte.yamsql index 24a4de4285..108f1fdc8a 100644 --- a/yaml-tests/src/test/resources/recursive-cte.yamsql +++ b/yaml-tests/src/test/resources/recursive-cte.yamsql @@ -457,7 +457,7 @@ test_block: traversal order pre_order select id, parent, level from c1 - supported_version: 4.10.1.0 - - explain: "RUNION-DFS PREORDER q0 { COVERING(CHILDIDX [EQUALS promote(@c24 AS LONG), [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, @c11 AS LEVEL) } { RECURSIVE COVERING(CHILDIDX [EQUALS q0.PARENT, [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS ID, _.PARENT AS PARENT, _.LEVEL AS LEVEL)" + - explain: "RUNION-DFS PREORDER q0 { COVERING(CHILDIDX [EQUALS promote(@c24 AS LONG), [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | MAP (_.ID AS ID, _.PARENT AS PARENT, @c11 AS LEVEL) } { RECURSIVE COVERING(CHILDIDX [EQUALS q0.PARENT, [NOT_NULL]] -> [ID: KEY:[0], PARENT: KEY:[1]]) | FILTER _.ID NOT_NULL | MAP (_.ID AS ID, _.PARENT AS PARENT, q0.LEVEL + @c81 AS LEVEL) } | MAP (_.ID AS ID, _.PARENT AS PARENT, _.LEVEL AS LEVEL)" - result: [{250, 50, 0}, {50, 10, 1}, {10, 1, 2}, diff --git a/yaml-tests/src/test/resources/sql-functions.metrics.binpb b/yaml-tests/src/test/resources/sql-functions.metrics.binpb index 851f12d444..26c2b7b534 100644 --- a/yaml-tests/src/test/resources/sql-functions.metrics.binpb +++ b/yaml-tests/src/test/resources/sql-functions.metrics.binpb @@ -61,19 +61,34 @@ M 3 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 2 -> 1 [ label=< q182> label="q182" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; -} +} O -basic-sql-function-tests3EXPLAIN select * from f1(103, 'b') where col1 = 101 - -쁠3 (n08@COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), EQUALS promote(@c13 AS LONG)] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2) digraph G { +basic-sql-function-tests3EXPLAIN select * from f1(103, 'b') where col1 = 101 + 5 (r08@COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)digraph G { fontname=courier; rankdir=BT; - splines=polyline; - 1 [ label=<
Value Computation
MAP (q193.COL1 AS COL1, q193.COL2 AS COL2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2)" ]; - 2 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c8 AS STRING), EQUALS promote(@c13 AS LONG)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 3 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 2 -> 1 [ label=< q193> label="q193" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + splines=line; + 1 [ label=<
Value Computation
MAP (q214.COL1 AS COL1, q214.COL2 AS COL2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2)" ]; + 2 [ label=<
Predicate Filter
WHERE q209.COL1 LESS_THAN promote(@c6 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 3 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 4 [ label=<
Index
T1_IDX4
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 3 -> 2 [ label=< q209> label="q209" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q214> label="q214" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +O +basic-sql-function-tests3EXPLAIN select * from f1(103, 'c') where col1 = 104 + 5 (r08@COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)digraph G { + fontname=courier; + rankdir=BT; + splines=line; + 1 [ label=<
Value Computation
MAP (q214.COL1 AS COL1, q214.COL2 AS COL2)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2)" ]; + 2 [ label=<
Predicate Filter
WHERE q209.COL1 LESS_THAN promote(@c6 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 3 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 4 [ label=<
Index
T1_IDX4
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 3 -> 2 [ label=< q209> label="q209" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q214> label="q214" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; } B basic-sql-function-tests&EXPLAIN select * from f1(103 + 1, 'b') @@ -98,48 +113,56 @@ h 3 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 2 -> 1 [ label=< q208> label="q208" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; -} +}  -basic-sql-function-tests{EXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(103, 'b') A, f1(103, 'b') B where A.col1 = B.col1 -o (08@ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }digraph G { +basic-sql-function-tests{EXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(103, 'b') A, f1(103, 'b') B where A.col1 = B.col1 +m "(08@ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c30 AS STRING), EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c28 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }digraph G { fontname=courier; rankdir=BT; splines=line; 1 [ label=<
Nested Loop Join
FLATMAP (q12.COL1 AS W, q12.COL2 AS X, q25.COL1 AS Y, q25.COL2 AS Z)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS W, STRING AS X, LONG AS Y, STRING AS Z)" ]; 2 [ label=<
Index Scan
comparisons: [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 AS LONG)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 4 [ label=<
Index Scan
comparisons: [EQUALS promote(@c30 AS STRING), EQUALS q25.COL1]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 5 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 4 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 5 [ label=<
Predicate Filter
WHERE q262.COL1 LESS_THAN promote(@c28 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 6 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c30 AS STRING), EQUALS q25.COL1]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 7 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 2 -> 1 [ label=< q25> label="q25" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ label=< q264> label="q264" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 5 [ label=< q262> label="q262" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 6 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 4 -> 1 [ label=< q12> label="q12" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; { - 4 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + 6 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; } { rank=same; rankDir=LR; 2 -> 4 [ color="red" style="invis" ]; } -} +}  -basic-sql-function-testsEXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(a => 103, b => 'b') A, f1(a => 103, b => 'b') B where A.col1 = B.col1 -i !(08@ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }digraph G { +basic-sql-function-testsEXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(a => 103, b => 'b') A, f1(a => 103, b => 'b') B where A.col1 = B.col1 +H (08@ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c34 AS STRING), EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c30 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }digraph G { fontname=courier; rankdir=BT; splines=line; 1 [ label=<
Nested Loop Join
FLATMAP (q12.COL1 AS W, q12.COL2 AS X, q25.COL1 AS Y, q25.COL2 AS Z)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS W, STRING AS X, LONG AS Y, STRING AS Z)" ]; 2 [ label=<
Index Scan
comparisons: [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 AS LONG)]]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 4 [ label=<
Index Scan
comparisons: [EQUALS promote(@c34 AS STRING), EQUALS q25.COL1]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; - 5 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 4 [ label=<
Fetch Records
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="12" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 5 [ label=<
Predicate Filter
WHERE q262.COL1 LESS_THAN promote(@c30 AS LONG)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 6 [ label=<
Covering Index Scan
comparisons: [EQUALS promote(@c34 AS STRING), EQUALS q25.COL1]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; + 7 [ label=<
Index
T1_IDX1
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(LONG AS COL1, STRING AS COL2, INT AS COL3)" ]; 3 -> 2 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 2 -> 1 [ label=< q25> label="q25" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; - 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ label=< q264> label="q264" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 6 -> 5 [ label=< q262> label="q262" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 7 -> 6 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; 4 -> 1 [ label=< q12> label="q12" color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; { - 4 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; + 6 -> 1 [ color="blue" style="dotted" arrowhead="none" tailport="nw" headport="s" constraint="false" ]; } { rank=same; diff --git a/yaml-tests/src/test/resources/sql-functions.metrics.yaml b/yaml-tests/src/test/resources/sql-functions.metrics.yaml index a4efc34e7c..fcbcbbfa2d 100644 --- a/yaml-tests/src/test/resources/sql-functions.metrics.yaml +++ b/yaml-tests/src/test/resources/sql-functions.metrics.yaml @@ -65,19 +65,32 @@ basic-sql-function-tests: insert_reused_count: 11 - query: EXPLAIN select * from f1(103, 'b') where col1 = 101 ref: sql-functions.yamsql:83 - explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), EQUALS promote(@c13 - AS LONG)] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS - COL1, _.COL2 AS COL2)' - task_count: 1320 - task_total_time_ms: 107 - transform_count: 299 - transform_time_ms: 31 - transform_yield_count: 110 - insert_time_ms: 7 - insert_new_count: 190 - insert_reused_count: 18 -- query: EXPLAIN select * from f1(103 + 1, 'b') + explain: 'COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS + STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 + LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' + task_count: 1424 + task_total_time_ms: 111 + transform_count: 317 + transform_time_ms: 27 + transform_yield_count: 114 + insert_time_ms: 4 + insert_new_count: 206 + insert_reused_count: 20 +- query: EXPLAIN select * from f1(103, 'c') where col1 = 104 ref: sql-functions.yamsql:87 + explain: 'COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS + STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 + LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' + task_count: 1424 + task_total_time_ms: 111 + transform_count: 317 + transform_time_ms: 27 + transform_yield_count: 114 + insert_time_ms: 4 + insert_new_count: 206 + insert_reused_count: 20 +- query: EXPLAIN select * from f1(103 + 1, 'b') + ref: sql-functions.yamsql:95 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c10 AS STRING), [LESS_THAN promote(@c6 + @c8 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -91,7 +104,7 @@ basic-sql-function-tests: insert_reused_count: 11 - query: EXPLAIN select * from (select * from f1(103 + 1, 'b')) as x where col1 < 105 - ref: sql-functions.yamsql:91 + ref: sql-functions.yamsql:99 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c14 AS STRING), [LESS_THAN promote(@c10 + @c12 AS LONG) && LESS_THAN promote(@c22 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -105,37 +118,39 @@ basic-sql-function-tests: insert_reused_count: 20 - query: EXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(103, 'b') A, f1(103, 'b') B where A.col1 = B.col1 - ref: sql-functions.yamsql:95 - explain: ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 - AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), - EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 - AS Z) } - task_count: 2673 - task_total_time_ms: 234 - transform_count: 612 - transform_time_ms: 69 - transform_yield_count: 221 + ref: sql-functions.yamsql:103 + explain: 'ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 + AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c30 AS STRING), + EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER + _.COL1 LESS_THAN promote(@c28 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, + q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }' + task_count: 2771 + task_total_time_ms: 229 + transform_count: 630 + transform_time_ms: 72 + transform_yield_count: 225 insert_time_ms: 6 - insert_new_count: 418 + insert_new_count: 432 insert_reused_count: 17 - query: EXPLAIN select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(a => 103, b => 'b') A, f1(a => 103, b => 'b') B where A.col1 = B.col1 - ref: sql-functions.yamsql:99 - explain: ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 - AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), - EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 - AS Z) } - task_count: 2673 - task_total_time_ms: 221 - transform_count: 612 - transform_time_ms: 70 - transform_yield_count: 221 - insert_time_ms: 11 - insert_new_count: 418 + ref: sql-functions.yamsql:107 + explain: 'ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 + AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c34 AS STRING), + EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER + _.COL1 LESS_THAN promote(@c30 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, + q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }' + task_count: 2771 + task_total_time_ms: 151 + transform_count: 630 + transform_time_ms: 44 + transform_yield_count: 225 + insert_time_ms: 4 + insert_new_count: 432 insert_reused_count: 17 - query: EXPLAIN with x(y, z) as (select * from f1(b => 'b', a => 103)) select * from x - ref: sql-functions.yamsql:103 + ref: sql-functions.yamsql:111 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c17 AS STRING), [LESS_THAN promote(@c21 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS Y, _.COL2 AS Z)' @@ -148,7 +163,7 @@ basic-sql-function-tests: insert_new_count: 152 insert_reused_count: 11 - query: EXPLAIN with x(y, z) as (select * from f1(103, 'b')) select * from x - ref: sql-functions.yamsql:107 + ref: sql-functions.yamsql:115 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c17 AS STRING), [LESS_THAN promote(@c15 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS Y, _.COL2 AS Z)' @@ -161,7 +176,7 @@ basic-sql-function-tests: insert_new_count: 152 insert_reused_count: 11 - query: EXPLAIN select * from t2 where exists (select * from f2(t2.z)) - ref: sql-functions.yamsql:111 + ref: sql-functions.yamsql:119 explain: ISCAN(T2_IDX1 <,>) | FLATMAP q0 -> { ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS q0.Z | DEFAULT NULL | FILTER _ NOT_NULL AS q1 RETURN q0 } task_count: 1082 @@ -173,7 +188,7 @@ basic-sql-function-tests: insert_new_count: 153 insert_reused_count: 5 - query: EXPLAIN select * from t2 where exists (select * from f2(k => t2.z)) - ref: sql-functions.yamsql:115 + ref: sql-functions.yamsql:123 explain: ISCAN(T2_IDX1 <,>) | FLATMAP q0 -> { ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS q0.Z | DEFAULT NULL | FILTER _ NOT_NULL AS q1 RETURN q0 } task_count: 1082 @@ -185,7 +200,7 @@ basic-sql-function-tests: insert_new_count: 153 insert_reused_count: 5 - query: EXPLAIN select * from f3(103, 'b', 4) - ref: sql-functions.yamsql:119 + ref: sql-functions.yamsql:127 explain: ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS promote(@c10 AS LONG) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c6 AS LONG)]]) AS q1 RETURN (q1.COL1 AS COL1, q1.COL2 AS COL2, q0.COL3 @@ -199,7 +214,7 @@ basic-sql-function-tests: insert_new_count: 491 insert_reused_count: 23 - query: EXPLAIN select * from f3(103, 'b', 4) - ref: sql-functions.yamsql:123 + ref: sql-functions.yamsql:131 explain: ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS promote(@c10 AS LONG) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c6 AS LONG)]]) AS q1 RETURN (q1.COL1 AS COL1, q1.COL2 AS COL2, q0.COL3 @@ -213,7 +228,7 @@ basic-sql-function-tests: insert_new_count: 491 insert_reused_count: 23 - query: EXPLAIN select * from f4(103, 'b', 2, 2) - ref: sql-functions.yamsql:127 + ref: sql-functions.yamsql:135 explain: ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS promote(@c10 AS LONG) + promote(@c10 AS LONG) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c6 AS LONG)]]) AS q1 RETURN (q1.COL1 AS COL1, @@ -227,7 +242,7 @@ basic-sql-function-tests: insert_new_count: 1307 insert_reused_count: 88 - query: EXPLAIN select * from f4(a => 103, b => 'b', c => 2, d => 2) - ref: sql-functions.yamsql:131 + ref: sql-functions.yamsql:139 explain: ISCAN(T1_IDX1 <,>) | FILTER promote(_.COL3 AS LONG) EQUALS promote(@c16 AS LONG) + promote(@c16 AS LONG) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c12 AS STRING), [LESS_THAN promote(@c8 AS LONG)]]) AS q1 RETURN (q1.COL1 AS COL1, @@ -241,7 +256,7 @@ basic-sql-function-tests: insert_new_count: 1307 insert_reused_count: 88 - query: EXPLAIN select * from f5(); - ref: sql-functions.yamsql:135 + ref: sql-functions.yamsql:143 explain: 'COVERING(T1_IDX1 [EQUALS promote(''b'' AS STRING), [LESS_THAN promote(103 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -254,7 +269,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(103); - ref: sql-functions.yamsql:139 + ref: sql-functions.yamsql:147 explain: 'COVERING(T1_IDX1 [EQUALS promote(''b'' AS STRING), [LESS_THAN promote(@c6 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -267,7 +282,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(b => 'b'); - ref: sql-functions.yamsql:143 + ref: sql-functions.yamsql:151 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(103 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -280,7 +295,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(b => 'b', a => 103); - ref: sql-functions.yamsql:147 + ref: sql-functions.yamsql:155 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c12 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -293,7 +308,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(b => 'b', a => 102); - ref: sql-functions.yamsql:151 + ref: sql-functions.yamsql:159 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c12 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -306,7 +321,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(b => 'a', a => 102); - ref: sql-functions.yamsql:155 + ref: sql-functions.yamsql:163 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c12 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -319,7 +334,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(a => 102, b => 'a'); - ref: sql-functions.yamsql:159 + ref: sql-functions.yamsql:167 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c12 AS STRING), [LESS_THAN promote(@c8 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -332,7 +347,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(102); - ref: sql-functions.yamsql:163 + ref: sql-functions.yamsql:171 explain: 'COVERING(T1_IDX1 [EQUALS promote(''b'' AS STRING), [LESS_THAN promote(@c6 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' @@ -345,7 +360,7 @@ basic-sql-function-tests: insert_new_count: 148 insert_reused_count: 11 - query: EXPLAIN select * from f5(102, 'a'); - ref: sql-functions.yamsql:167 + ref: sql-functions.yamsql:175 explain: 'COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), [LESS_THAN promote(@c6 AS LONG)]] -> [COL1: KEY[1], COL2: KEY[0], COL3: KEY[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)' diff --git a/yaml-tests/src/test/resources/sql-functions.yamsql b/yaml-tests/src/test/resources/sql-functions.yamsql index 8f1b5bae27..8a8da8d8b4 100644 --- a/yaml-tests/src/test/resources/sql-functions.yamsql +++ b/yaml-tests/src/test/resources/sql-functions.yamsql @@ -80,8 +80,16 @@ test_block: - result: [{101, 'b'}, {102, 'b'}] - - query: select * from f1(103, 'b') where col1 = 101 - - explain: "COVERING(T1_IDX1 [EQUALS promote(@c8 AS STRING), EQUALS promote(@c13 AS LONG)] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)" + - explain: "COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)" - result: [{101, 'b'}] + - + - query: select * from f1(103, 'c') where col1 = 104 + - explain: "COVERING(T1_IDX4 [EQUALS promote(@c13 AS LONG), EQUALS promote(@c8 AS STRING)] -> [COL1: KEY:[0], COL2: KEY:[1], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c6 AS LONG) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)" + - initialVersionLessThan: !current_version + # Prior to !current_version, we used to drop the < 103 predicate from the function due to: https://github.com/FoundationDB/fdb-record-layer/pull/3965 + - result: [{104, 'c'}] + - initialVersionAtLeast: !current_version + - result: [] - - query: select * from f1(103 + 1, 'b') - explain: "COVERING(T1_IDX1 [EQUALS promote(@c10 AS STRING), [LESS_THAN promote(@c6 + @c8 AS LONG)]] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | MAP (_.COL1 AS COL1, _.COL2 AS COL2)" @@ -92,11 +100,11 @@ test_block: - result: [{101, 'b'}, {102, 'b'}, {103, 'b'}] - - query: select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(103, 'b') A, f1(103, 'b') B where A.col1 = B.col1 - - explain: "ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }" + - explain: "ISCAN(T1_IDX1 [EQUALS promote(@c30 AS STRING), [LESS_THAN promote(@c28 AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c30 AS STRING), EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c28 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }" - result: [{W: 101, X: 'b', Y: 101, Z: 'b'}, {W: 102, X: 'b', Y: 102, Z: 'b'}] - - query: select A.col1 AS W, A.col2 AS X, B.col1 AS Y, B.col2 AS Z from f1(a => 103, b => 'b') A, f1(a => 103, b => 'b') B where A.col1 = B.col1 - - explain: "ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 AS LONG)]]) | FLATMAP q0 -> { ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), EQUALS q0.COL1]) AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }" + - explain: "ISCAN(T1_IDX1 [EQUALS promote(@c34 AS STRING), [LESS_THAN promote(@c30 AS LONG)]]) | FLATMAP q0 -> { COVERING(T1_IDX1 [EQUALS promote(@c34 AS STRING), EQUALS q0.COL1] -> [COL1: KEY:[1], COL2: KEY:[0], COL3: KEY:[2]]) | FILTER _.COL1 LESS_THAN promote(@c30 AS LONG) | FETCH AS q1 RETURN (q1.COL1 AS W, q1.COL2 AS X, q0.COL1 AS Y, q0.COL2 AS Z) }" - result: [{W: 101, X: 'b', Y: 101, Z: 'b'}, {W: 102, X: 'b', Y: 102, Z: 'b'}] - - query: with x(y, z) as (select * from f1(b => 'b', a => 103)) select * from x