From 883f137987748268e2d25a66659ba846e7b96335 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Thu, 1 Oct 2026 14:56:46 +0100 Subject: [PATCH 01/11] changes --- .../KeyExpressionExpansionVisitor.java | 33 ++++- .../cascades/ScalarTranslationVisitor.java | 17 ++- .../cascades/ScanWithFetchMatchCandidate.java | 68 ++++++++-- .../cascades/ValueIndexExpansionVisitor.java | 93 +++++++++++-- .../plan/cascades/predicates/Placeholder.java | 2 +- .../rules/AbstractDataAccessRule.java | 20 +-- .../SyntheticRecordTypeExpansionTest.java | 53 ++++++++ .../IndexEntryTranslatorEquivalenceTest.java | 60 +++++++++ .../src/test/java/YamlIntegrationTests.java | 6 + ...unnested-record-type-indexes.metrics.binpb | Bin 0 -> 15678 bytes .../unnested-record-type-indexes.metrics.yaml | 110 +++++++++++++++ .../unnested-record-type-indexes.yamsql | 126 ++++++++++++++++++ 12 files changed, 555 insertions(+), 33 deletions(-) create mode 100644 yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb create mode 100644 yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml create mode 100644 yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/KeyExpressionExpansionVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/KeyExpressionExpansionVisitor.java index 156a010289d..6c88564fe5a 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/KeyExpressionExpansionVisitor.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/KeyExpressionExpansionVisitor.java @@ -41,6 +41,7 @@ import com.apple.foundationdb.record.query.plan.cascades.values.EmptyValue; import com.apple.foundationdb.record.query.plan.cascades.values.ExplodeValue; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; +import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue; import com.apple.foundationdb.record.query.plan.cascades.values.Value; import com.apple.foundationdb.record.util.ProtoUtils; import com.google.common.base.Verify; @@ -482,7 +483,37 @@ public GraphExpansion visitExpression(@Nonnull final ThenKeyExpression thenKeyEx @Nonnull @Override public GraphExpansion visitExpression(@Nonnull final ListKeyExpression listKeyExpression) { - throw new UnsupportedOperationException("visitor method for this key expression is not implemented"); + final ImmutableList.Builder expandedPredicatesBuilder = ImmutableList.builder(); + final VisitorState state = getCurrentState(); + int currentOrdinal = state.getCurrentOrdinal(); + for (KeyExpression child : listKeyExpression.getChildren()) { + final VisitorState childState = state.withCurrentOrdinal(currentOrdinal); + final GraphExpansion childExpansion = pop(child.expand(push(childState.forFunctionalExpansion()))); + if (!childExpansion.getQuantifiers().isEmpty()) { + throw new UnsupportedOperationException("cannot expand a list whose child introduces quantifiers"); + } + final var childValues = childExpansion.getResultColumns() + .stream() + .map(Column::getValue) + .collect(ImmutableList.toImmutableList()); + // a list places its child in a nested tuple, so the value of the position is the record of the child's + // column values -- a record of one for a single-column child, as that tuple is nested all the same + final var value = childState.registerValue(RecordConstructorValue.ofUnnamed(childValues)); + // the child's position contributes what a scalar field at that position would + final var isSargable = childState.isKey() && !childState.isInternalExpansion(); + if (childState.isSelectStar()) { + expandedPredicatesBuilder.add(isSargable + ? GraphExpansion.ofPlaceholder(value.asPlaceholder(newParameterAlias())) + : GraphExpansion.empty()); + } else { + expandedPredicatesBuilder.add(isSargable + ? GraphExpansion.ofResultColumnAndPlaceholder(Column.unnamedOf(value), + value.asPlaceholder(newParameterAlias())) + : GraphExpansion.ofResultColumn(Column.unnamedOf(value))); + } + currentOrdinal++; + } + return GraphExpansion.ofOthers(expandedPredicatesBuilder.build()); } /** diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScalarTranslationVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScalarTranslationVisitor.java index ece0240d9ed..7846fa75111 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScalarTranslationVisitor.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScalarTranslationVisitor.java @@ -34,6 +34,7 @@ import com.apple.foundationdb.record.query.plan.cascades.values.EmptyValue; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedObjectValue; +import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue; import com.apple.foundationdb.record.query.plan.cascades.values.Value; import com.apple.foundationdb.record.util.ProtoUtils; import com.google.common.collect.ImmutableList; @@ -206,7 +207,21 @@ public Value visitExpression(@Nonnull final ThenKeyExpression thenKeyExpression) @Nonnull @Override public Value visitExpression(@Nonnull final ListKeyExpression listKeyExpression) { - throw new UnsupportedOperationException("visitor method for this key expression is not implemented"); + if (listKeyExpression.getColumnSize() > 1) { + throw new RecordCoreException("cannot expand ListKeyExpression in scalar expansion"); + } + + final ScalarVisitorState state = getCurrentState(); + final KeyExpression child = Iterables.getOnlyElement(listKeyExpression.getChildren()); + + // The child occupies a single position whose value is the nested tuple of the child's own columns, so it is a + // record of their values -- a record of one for a single-column child, as that tuple is nested all the same. + final List positions = child.normalizeKeyForPositions(); + final ImmutableList.Builder valuesBuilder = ImmutableList.builder(); + for (final KeyExpression position : positions) { + valuesBuilder.add(pop(position.expand(push(state)))); + } + return RecordConstructorValue.ofUnnamed(valuesBuilder.build()); } @Nonnull diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java index 19901f4662d..15244c901d7 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java @@ -22,6 +22,7 @@ import com.apple.foundationdb.record.EvaluationContext; import com.apple.foundationdb.record.metadata.RecordType; +import com.apple.foundationdb.record.metadata.expressions.ListKeyExpression; import com.apple.foundationdb.record.query.plan.IndexKeyValueToPartialRecord; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; @@ -150,17 +151,20 @@ static Optional computeIn for (int i = 0; i < indexKeyValues.size(); i++) { final Value keyValue = indexKeyValues.get(i); - final var extractFromIndexEntryPairOptional = - keyValue.extractFromIndexEntryMaybe(baseObjectValue, EvaluationContext.empty(), AliasMap.emptyMap(), - ImmutableSet.of(), IndexKeyValueToPartialRecord.TupleSource.KEY, ImmutableIntArray.of(i)); - if (extractFromIndexEntryPairOptional.isPresent()) { - final var extractFromIndexEntryPair = extractFromIndexEntryPairOptional.get(); - if (!addCoveringField(builder, extractFromIndexEntryPair.getKey(), - extractFromIndexEntryPair.getValue())) { - return Optional.empty(); + for (final var datum : entryData(keyValue, ImmutableIntArray.of(i))) { + final var extractFromIndexEntryPairOptional = + datum.value().extractFromIndexEntryMaybe(baseObjectValue, EvaluationContext.empty(), + AliasMap.emptyMap(), ImmutableSet.of(), + IndexKeyValueToPartialRecord.TupleSource.KEY, datum.ordinalPath()); + if (extractFromIndexEntryPairOptional.isPresent()) { + final var extractFromIndexEntryPair = extractFromIndexEntryPairOptional.get(); + if (!addCoveringField(builder, extractFromIndexEntryPair.getKey(), + extractFromIndexEntryPair.getValue())) { + return Optional.empty(); + } + recordCoveredField(covered, extractFromIndexEntryPair); + logicalKeyValuesBuilder.add(extractFromIndexEntryPair.getLeft()); } - recordCoveredField(covered, extractFromIndexEntryPair); - logicalKeyValuesBuilder.add(extractFromIndexEntryPair.getLeft()); } } @@ -192,6 +196,50 @@ static Optional computeIn indexEntryToRecordValue(baseType, covered))); } + /** + * Decomposes one column of the index entry into the data it holds that can be read individually. + * + *

A column usually holds a single datum, at {@code tuple.get(ordinal)}. A {@link ListKeyExpression} is the + * exception: it places its child in a nested tuple rather than flattening it, so the data sit one level deeper, + * one element per column of that child. The {@link Value} registered for such a column is the + * {@link RecordConstructorValue} of those columns' values, which is what {@code KeyExpressionExpansionVisitor} + * builds for it, so the record mirrors the tuple and the recursion can follow the value at every level. + * + * @param value the value registered for the column + * @param ordinalPath the path to the column + * @return the data that column holds + */ + @Nonnull + private static List entryData(@Nonnull final Value value, + @Nonnull final ImmutableIntArray ordinalPath) { + if (!(value instanceof RecordConstructorValue)) { + return ImmutableList.of(new EntryDatum(value, ordinalPath)); + } + final var columns = ((RecordConstructorValue)value).getColumns(); + final var dataBuilder = ImmutableList.builder(); + for (int i = 0; i < columns.size(); i++) { + dataBuilder.addAll(entryData(columns.get(i).getValue(), appendOrdinal(ordinalPath, i))); + } + return dataBuilder.build(); + } + + @Nonnull + private static ImmutableIntArray appendOrdinal(@Nonnull final ImmutableIntArray ordinalPath, final int ordinal) { + return ImmutableIntArray.builder(ordinalPath.length() + 1) + .addAll(ordinalPath) + .add(ordinal) + .build(); + } + + /** + * A datum of an index entry that can be read on its own. + * + * @param value the value that datum holds + * @param ordinalPath the path at which it sits in the entry's tuple + */ + record EntryDatum(@Nonnull Value value, @Nonnull ImmutableIntArray ordinalPath) { + } + /** * Records what the given extraction covers, descending a node per field of the path it fills, which may run into * nested messages. Every name is present here, {@code addCoveringField} having already refused the extraction diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java index 163274cc4d7..26155d521ab 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java @@ -24,6 +24,7 @@ import com.apple.foundationdb.record.metadata.Index; import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.metadata.RecordType; +import com.apple.foundationdb.record.metadata.SyntheticRecordType; import com.apple.foundationdb.record.metadata.expressions.GroupingKeyExpression; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; import com.apple.foundationdb.record.metadata.expressions.KeyWithValueExpression; @@ -32,9 +33,12 @@ import com.apple.foundationdb.record.query.plan.cascades.predicates.Placeholder; import com.apple.foundationdb.record.query.plan.cascades.predicates.PredicateWithValueAndRanges; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; +import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue; import com.apple.foundationdb.record.query.plan.cascades.values.Value; +import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import javax.annotation.Nonnull; @@ -43,6 +47,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; @@ -72,6 +77,62 @@ public ValueIndexExpansionVisitor(@Nonnull Index index, @Nonnull Collection> syntheticRecordTypeMaybe( + @Nonnull final Collection indexedRecordTypes) { + if (indexedRecordTypes.size() != 1) { + return Optional.empty(); + } + final RecordType indexedRecordType = Iterables.getOnlyElement(indexedRecordTypes); + return indexedRecordType instanceof final SyntheticRecordType syntheticRecordType + ? Optional.of(syntheticRecordType) + : Optional.empty(); + } + + /** + * Merges the expansion of an index key into the expansion of the synthetic type the index is defined on, if it is + * defined on one. An index on a stored type has no base expansion, and its key expansion is returned as it stands. + * + *

The key's values are written over {@code baseQuantifier}, which does not survive into the merged graph, so + * each is re-expressed over the record the base expansion assembles and simplified: + * {@code base.unnesting_0.reviewer} over {@code (parent AS parent, explode._0 AS unnesting_0, ...)} becomes + * {@code explode._0.reviewer}, the very value a query navigating the same unnesting flows. + * + *

Predicates and placeholders are translated separately because a placeholder is held in both lists and + * {@link GraphExpansion#seal()} pairs them up by value; translating one alone would leave a parameter whose + * predicate the select does not hold. + * + * @param baseExpansion the expansion assembling the synthetic type's records, or {@code null} if there is none + * @param keyExpansion the expansion of the index key, expressed over {@code baseQuantifier} + * @param baseQuantifier the quantifier {@code keyExpansion} is expressed over + * @return the key expansion merged into the base expansion, or the key expansion itself if there is no base + */ + @Nonnull + private static GraphExpansion mergeWithBaseIfNeeded(@Nullable final GraphExpansion baseExpansion, + @Nonnull final GraphExpansion keyExpansion, + @Nonnull final Quantifier.ForEach baseQuantifier) { + if (baseExpansion == null) { + return keyExpansion; + } + final var ontoBaseRecord = TranslationMap.regularBuilder() + .when(baseQuantifier.getAlias()) + .then((sourceAlias, leafValue) -> RecordConstructorValue.ofColumns(baseExpansion.getResultColumns())) + .build(); + return GraphExpansion.ofOthers( + baseExpansion.toBuilder().removeAllResultColumns().build(), + keyExpansion.toBuilder() + .removeAllPredicates() + .addAllPredicates(keyExpansion.getPredicates().stream() + .map(predicate -> predicate.translateCorrelations(ontoBaseRecord, true)) + .collect(ImmutableList.toImmutableList())) + .replacePlaceholder(placeholder -> + placeholder.translateLeafPredicate(ontoBaseRecord, true)) + .build()); + } + @Nonnull @Override public MatchCandidate expand(@Nonnull final Set availableRecordTypeNames, @@ -87,12 +148,22 @@ public MatchCandidate expand(@Nonnull final Set availableRecordTypeNames // the instantiation of the type filter below to create a placeholder for the record type key parameter // alias and reuse it here. Similar to what we currently do for primary scans. // - final var baseQuantifier = Quantifier.forEach(ExpansionVisitor.createBaseRef(availableRecordTypeNames, - queriedRecordTypeNames, baseType, null, accessHint)); + @Nullable final GraphExpansion baseExpansion = + syntheticRecordTypeMaybe(queriedRecordTypes).map(recordType -> recordType.expand(accessHint)).orElse(null); final var allExpansionsBuilder = ImmutableList.builder(); - - // add the value for the flow of records - allExpansionsBuilder.add(GraphExpansion.ofQuantifier(baseQuantifier)); + final Quantifier.ForEach baseQuantifier; + final Value baseObjectValue; + if (baseExpansion == null) { + baseQuantifier = Quantifier.forEach(ExpansionVisitor.createBaseRef(availableRecordTypeNames, + queriedRecordTypeNames, baseType, null, accessHint)); + baseObjectValue = baseQuantifier.getFlowedObjectValue(); + // add the value for the flow of records + allExpansionsBuilder.add(GraphExpansion.ofQuantifier(baseQuantifier)); + } else { + // the base expansion's own quantifiers reach the graph when the key expansion is merged into it + baseQuantifier = Quantifier.forEach(Reference.initialOf(baseExpansion.seal().buildSelect())); + baseObjectValue = RecordConstructorValue.ofColumns(baseExpansion.getResultColumns()); + } var rootExpression = index.getRootExpression(); @@ -105,8 +176,7 @@ public MatchCandidate expand(@Nonnull final Set availableRecordTypeNames } final int keyValueSplitPoint; - if (rootExpression instanceof KeyWithValueExpression) { - final KeyWithValueExpression keyWithValueExpression = (KeyWithValueExpression)rootExpression; + if (rootExpression instanceof final KeyWithValueExpression keyWithValueExpression) { keyValueSplitPoint = keyWithValueExpression.getSplitPoint(); rootExpression = keyWithValueExpression.getInnerKey(); } else { @@ -190,15 +260,18 @@ public MatchCandidate expand(@Nonnull final Set availableRecordTypeNames } } - final var completeExpansion = GraphExpansion.ofOthers(allExpansionsBuilder.build()); - final var sealedExpansion = completeExpansion.seal(); + final var sealedExpansion = mergeWithBaseIfNeeded( + baseExpansion, + GraphExpansion.ofOthers(allExpansionsBuilder.build()), + baseQuantifier) + .seal(); final var parameters = sealedExpansion.getPlaceholders() .stream() .map(Placeholder::getParameterAlias) .collect(ImmutableList.toImmutableList()); final var matchableSortExpression = new MatchableSortExpression(parameters, isReverse, - sealedExpansion.buildSelectWithResultValue(baseQuantifier.getFlowedObjectValue())); + sealedExpansion.buildSelectWithResultValue(baseObjectValue)); return new ValueIndexScanMatchCandidate(index, queriedRecordTypes, Traversal.withRoot(Reference.initialOf(matchableSortExpression)), diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/Placeholder.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/Placeholder.java index a43f08c8482..edd544d06ff 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/Placeholder.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/Placeholder.java @@ -113,7 +113,7 @@ public Placeholder withExtraRanges(@Nonnull final Set ranges) @Nonnull @Override public Placeholder translateLeafPredicate(@Nonnull final TranslationMap translationMap, final boolean shouldSimplifyValues) { - return new Placeholder(getValue().translateCorrelations(translationMap), + return new Placeholder(getValue().translateCorrelations(translationMap, shouldSimplifyValues), getRanges().stream() .map(range -> range.translateCorrelations(translationMap, shouldSimplifyValues)) .collect(ImmutableSet.toImmutableSet()), getParameterAlias()); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java index 2a2c48846c9..a8b71431340 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java @@ -68,7 +68,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; @@ -157,7 +156,7 @@ public void onMatch(@Nonnull final CascadesRuleCall call) { final var aliasToQuantifierMap = Quantifiers.aliasToQuantifierMap(expression.getQuantifiers()); final var aliases = aliasToQuantifierMap.keySet(); - // group all successful matches by their sets of compensated aliases + // group all successful matches by the set of for-each quantifiers they compensate final var matchPartitionByMatchAliasMap = completeMatches .stream() @@ -171,10 +170,11 @@ public void onMatch(@Nonnull final CascadesRuleCall call) { .filter(matchedAlias -> Objects.requireNonNull(aliasToQuantifierMap.get(matchedAlias)) instanceof Quantifier.ForEach) .collect(ImmutableSet.toImmutableSet()); - if (matchedForEachAliases.size() == 1) { - return Stream.of(NonnullPair.of(Iterables.getOnlyElement(matchedForEachAliases), match)); + if (matchedForEachAliases.isEmpty()) { + // only existential quantifiers... + return Stream.empty(); } - return Stream.empty(); + return Stream.of(NonnullPair.of(matchedForEachAliases, match)); }) .collect(Collectors.groupingBy( Pair::getLeft, @@ -203,11 +203,11 @@ public void onMatch(@Nonnull final CascadesRuleCall call) { ImmutableList.toImmutableList())); // - // Note that this works because there is only one for-each and potentially 0 - n existential quantifiers - // that are covered by the match partition. Even though that logically forms a join, the existential - // quantifiers do not mutate the result of the join, they only cause filtering, that is, the resulting - // record is exactly what the for each quantifier produced filtered by the predicates expressed on the - // existential quantifiers. + // Note that the matches in this partition all compensate the same set of for-each quantifiers, plus + // potentially 0 - n existential quantifiers. A match may therefore stand in for a join, which is the case + // for a candidate over a synthetic record type. That is sound because a match reaching here compensates + // every quantifier the expression owns, so the data access accounts for the whole expression rather than + // for one of its legs; existential quantifiers only cause filtering and do not contribute to the result. // for (final var matchPartitionEntry : matchPartitionsForAliasesByPredicates.entrySet()) { final var matchPartition = matchPartitionEntry.getValue(); diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/SyntheticRecordTypeExpansionTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/SyntheticRecordTypeExpansionTest.java index 8df12b02760..58f046b2042 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/SyntheticRecordTypeExpansionTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/SyntheticRecordTypeExpansionTest.java @@ -40,6 +40,7 @@ import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.TableFunctionExpression; import com.apple.foundationdb.record.query.plan.cascades.predicates.ValuePredicate; +import com.apple.foundationdb.record.query.plan.cascades.typing.PseudoField; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.values.ExplodeValue; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; @@ -75,6 +76,8 @@ class SyntheticRecordTypeExpansionTest { @Nonnull private static final String OUTER = "OuterRecord"; @Nonnull + private static final String OTHER = "OtherRecord"; + @Nonnull private static final String PARENT = "parent"; @Nonnull private static final String UNNESTED_MAP = "UnnestedMap"; @@ -395,6 +398,56 @@ void expandOnAnOuterJoinedConstituentIsUnsupported() { assertEquals("cannot expand an outer-joined constituent of a JoinedRecordType", exception.getMessage()); } + @Test + void getPlannerTypeForRecordTypeDescribesASyntheticType() { + final RecordMetaData metaData = mapMetaData(addTwoMapsType()); + final UnnestedRecordType type = unnestedType(metaData, TWO_UNNESTED_MAPS); + + // A synthetic type's name cannot be resolved against the stored types, so only the type-taking overload can + // describe it. + assertThrows(MetaDataException.class, () -> metaData.getPlannerType(TWO_UNNESTED_MAPS)); + final Type.Record plannerType = metaData.getPlannerTypeForRecordType(type); + assertEquals(Type.Record.fromDescriptor(type.getDescriptor()), plannerType); + assertThat(plannerType.getFields().stream().map(Type.Record.Field::getFieldName).collect(Collectors.toList()), + contains(PARENT, "entry_one", "entry_two", UnnestedRecordType.POSITIONS_FIELD)); + } + + @Test + void getPlannerTypeForRecordTypeMatchesTheNameTakingOverloads() { + final RecordMetaData metaData = mapMetaData(metaDataBuilder -> metaDataBuilder.setStoreRecordVersions(true)); + + assertEquals(metaData.getPlannerType(OUTER), + metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER))); + assertEquals(metaData.getPlannerType(List.of(OUTER)), + metaData.getPlannerTypeForRecordTypes(List.of(metaData.getRecordType(OUTER)))); + assertEquals(metaData.getPlannerType(List.of(OUTER, OTHER)), + metaData.getPlannerTypeForRecordTypes( + List.of(metaData.getRecordType(OUTER), metaData.getRecordType(OTHER)))); + + // Storing record versions adds the pseudo field to every stored type, including the union of several of them. + final String versionField = PseudoField.ROW_VERSION.getFieldName(); + assertTrue(metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER)) + .getFieldNameFieldMap().containsKey(versionField)); + assertTrue(metaData.getPlannerTypeForRecordTypes( + List.of(metaData.getRecordType(OUTER), metaData.getRecordType(OTHER))) + .getFieldNameFieldMap().containsKey(versionField)); + } + + @Test + void getPlannerTypeForRecordTypesUnionsTheirFields() { + final RecordMetaData metaData = mapMetaData(metaDataBuilder -> { }); + final Type.Record unionType = metaData.getPlannerTypeForRecordTypes( + List.of(metaData.getRecordType(OUTER), metaData.getRecordType(OTHER))); + + // `rec_id` and `other_id` are shared, so the union has them once; the remaining fields come from one type each. + assertThat(unionType.getFields().stream().map(Type.Record.Field::getFieldName).collect(Collectors.toList()), + contains("rec_id", "other_id", "map", "other_value")); + + // A single type is described exactly as the type-taking overload would describe it on its own. + assertEquals(metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER)), + metaData.getPlannerTypeForRecordTypes(List.of(metaData.getRecordType(OUTER)))); + } + @Nonnull private static RecordMetaData mapMetaData(@Nonnull Consumer hook) { final RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder() diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/IndexEntryTranslatorEquivalenceTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/IndexEntryTranslatorEquivalenceTest.java index e11547f03b9..2b98c333907 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/IndexEntryTranslatorEquivalenceTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/IndexEntryTranslatorEquivalenceTest.java @@ -46,6 +46,7 @@ import static com.apple.foundationdb.record.metadata.Key.Expressions.concat; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; import static com.apple.foundationdb.record.metadata.Key.Expressions.field; +import static com.apple.foundationdb.record.metadata.Key.Expressions.list; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -151,6 +152,65 @@ void bothTranslatorsKeepTheFirstColumnOfARepeatedField() { .findFirst().orElseThrow().getValue().toString()); } + /** + * A {@link com.apple.foundationdb.record.metadata.expressions.ListKeyExpression} position holds its child in a + * nested tuple instead of flattening it, the way the primary key of a synthetic record type holds each of its + * constituents. The data of such a position therefore sit one level deeper, one element per column of the child: + *

+     *   primary key : list(header.(path, num), header.rec_no)
+     *   index str   : str_value
+     *   entry key   : ( "abc", ("/a", 1), (7) )   with the primary key appended
+     *   record      : MyRecord { str_value: "abc", header: { path: "/a", num: 1, rec_no: 7 } }
+     * 
+ * Each of the three data of the two list positions is read on its own, at {@code [1, 0]}, {@code [1, 1]} and + * {@code [2, 0]} respectively. + */ + @Test + void bothTranslatorsDescendIntoAListPosition() { + final var metaDataBuilder = RecordMetaData.newBuilder() + .setRecords(TestRecordsWithHeaderProto.getDescriptor()); + metaDataBuilder.getRecordType("MyRecord").setPrimaryKey(list( + field("header").nest(concatenateFields("path", "num")), + field("header").nest(field("rec_no")))); + metaDataBuilder.addIndex("MyRecord", new Index("str", field("str_value"))); + final var metaData = metaDataBuilder.build(); + final var recordType = metaData.getRecordType("MyRecord"); + final var descriptor = recordType.getDescriptor(); + final var baseType = Type.Record.fromDescriptor(descriptor); + final var baseAlias = Quantifier.current(); + final var baseObjectValue = QuantifiedObjectValue.of(baseAlias, baseType); + + // One value per entry key column, as the index expansion produces them. A list position holds its child in a + // nested tuple, so its value is the record of the child's column values, however many there are. + final var indexKeyValues = ImmutableList.of( + FieldValue.ofFieldNames(baseObjectValue, List.of("str_value")), + RecordConstructorValue.ofUnnamed(ImmutableList.of( + FieldValue.ofFieldNames(baseObjectValue, List.of("header", "path")), + FieldValue.ofFieldNames(baseObjectValue, List.of("header", "num")))), + RecordConstructorValue.ofUnnamed(ImmutableList.of( + FieldValue.ofFieldNames(baseObjectValue, List.of("header", "rec_no"))))); + + final var translators = ScanWithFetchMatchCandidate.computeIndexEntryToLogicalRecord(List.of(recordType), + baseAlias, baseType, indexKeyValues, ImmutableList.of()).orElseThrow(); + final var recordValue = translators.indexEntryToRecordValue(); + assertNotNull(recordValue, "a list position should still yield a value based translator"); + + final var entry = new IndexEntry(metaData.getIndex("str"), + Tuple.from("abc", Tuple.from("/a", 1L), Tuple.from(7L)), TupleHelpers.EMPTY); + + final var byCopiers = translators.indexKeyValueToPartialRecord().toRecord(descriptor, entry); + final var byValue = evaluate(recordValue, entry); + + // Non-vacuous: the three columns nested inside the two list positions are all recovered. + final var header = (Message)byCopiers.getField(descriptor.findFieldByName("header")); + final var headerDescriptor = descriptor.findFieldByName("header").getMessageType(); + assertEquals("/a", header.getField(headerDescriptor.findFieldByName("path"))); + assertEquals(1, header.getField(headerDescriptor.findFieldByName("num"))); + assertEquals(7L, header.getField(headerDescriptor.findFieldByName("rec_no"))); + + assertSameRecord(descriptor, byCopiers, byValue); + } + @Nonnull private static Message evaluate(@Nonnull final Value recordValue, @Nonnull final IndexEntry entry) { final var typeRepository = TypeRepository.newBuilder().addTypeIfNeeded(recordValue.getResultType()).build(); diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 104b7984a8e..8a2e898f2a8 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -498,6 +498,11 @@ public void unionEmptyTables(YamlTest.Runner runner) throws Exception { runner.runYamsql("union-empty-tables.yamsql"); } + @TestTemplate + public void unnestedRecordTypeIndexes(YamlTest.Runner runner) throws Exception { + runner.runYamsql("unnested-record-type-indexes.yamsql"); + } + @TestTemplate public void updateDeleteReturning(YamlTest.Runner runner) throws Exception { runner.runYamsql("update-delete-returning.yamsql"); @@ -531,6 +536,7 @@ public void uuidNonPreparedTest(YamlTest.Runner runner) throws Exception { * @see MetaDataExportUtilityTests#createValidIdentifiersMetaData() for how the custom meta-data is generated */ @TestTemplate + @MaintainYamlTestConfig(YamlTestConfigFilters.CORRECT_METRICS) public void validIdentifierTests(YamlTest.Runner runner) throws Exception { runner.runYamsql("valid-identifiers.yamsql"); } diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb new file mode 100644 index 0000000000000000000000000000000000000000..f1ed390fd3c6a3b42196e2250d9a9b789a44545c GIT binary patch literal 15678 zcmeI3e`p(39KcO0V;<_#ppvzJ_~pbkfwW7TABMIG&AK>?UAv`Sb*&{_l3RQ1dY9c@ zH|G=-bO@Uux}gjv(?4{A$`l8aiVDiy1{QS_MBM(Ef;z_j7)}I1{Jy(Oa(8Q+bgpUA z-5&{e?|t99yYKhC@B7^MeecHx>SqUa$CMy2lWevn;F@tH9^cd#@9l@dL|>w32nIK| z(h|FqV|TFEXVAQ+<#;j~-#V*B3XL`W7tJM;Lv8$-d(_;456mrzv|%8GK+ODf^K|wJNjIUo5?%$IrPFvlDKPvWwKFP5T!|mr$H@J7>*}J0_v7He>yEH z69w0SS2pb)vtF=_>(^$LQ@fUue0nw8#@9-t^mg=5Pb`ripA`twyrZ@Z_yJ z=Yf-#N8DTB{Gq)Md7Aw1ocUr!@s(#8|@ z4X;QM`kYgzpGwO%p4fQue~`1<@FZ-&lW@iHn%KN0-iJH0k~k?UtfxB@XeS`0?fH7gZqZbc3(GZCBvcmKpw!%G zW9`C5n?**O45kAy5+`e!S|ujz(nq{%e{HmR$L1Y1BhZqRb`$eQLI&OmRi1agcTvBX zc;|=O;~j|iuQz4l7BY4T6JdgnGSTe%gD}zAdgq~!r=M^ibifB+e;4vR=s*4T^S4Jr z-Sw_h3uhu9T=eleG(*nW~7HN=|5-sS4xOkbYatrZE%} zKWOLVlCKFY>~MWTd+l>_mBo<4fnp3P9H@8~I{rNxNM zmofy2jyf>`=2D&WtkeNI#&Mr59_ORxRfU;FGBksTvi>===OXF`?d Gng0MWt{)Kq literal 0 HcmV?d00001 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml new file mode 100644 index 00000000000..0224732f02a --- /dev/null +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -0,0 +1,110 @@ +unnamed-3: +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer", + "restaurant"."region", SQ."rating" + ref: unnested-record-type-indexes.yamsql:55 + explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: + [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: + KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, + _.unnesting_0.rating AS rating)' + task_count: 261 + task_total_time_ms: 181 + transform_count: 78 + transform_time_ms: 111 + transform_yield_count: 24 + insert_time_ms: 13 + insert_new_count: 26 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + ref: unnested-record-type-indexes.yamsql:66 + explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: + [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: + KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, + _.unnesting_0.rating AS rating)' + task_count: 290 + task_total_time_ms: 190 + transform_count: 85 + transform_time_ms: 115 + transform_yield_count: 27 + insert_time_ms: 13 + insert_new_count: 31 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."reviewer" FROM "restaurant", (SELECT "reviewer", "rating" + FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer" + ref: unnested-record-type-indexes.yamsql:78 + explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: + [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: + KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)' + task_count: 268 + task_total_time_ms: 177 + transform_count: 82 + transform_time_ms: 113 + transform_yield_count: 25 + insert_time_ms: 13 + insert_new_count: 27 + insert_reused_count: 0 +- query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM + "restaurant", "restaurant"."reviews" AS "r" ORDER BY "r"."reviewer", "restaurant"."region", + "r"."rating" + ref: unnested-record-type-indexes.yamsql:91 + explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: + [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: + KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, + _.unnesting_0.rating AS rating)' + task_count: 236 + task_total_time_ms: 43 + transform_count: 73 + transform_time_ms: 22 + transform_yield_count: 22 + insert_time_ms: 0 + insert_new_count: 21 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + = 'Alice' + ref: unnested-record-type-indexes.yamsql:104 + explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], + unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer + AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 397 + task_total_time_ms: 189 + transform_count: 129 + transform_time_ms: 110 + transform_yield_count: 35 + insert_time_ms: 7 + insert_new_count: 47 + insert_reused_count: 1 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + = 'Alice' AND "restaurant"."region" = 'north' + ref: unnested-record-type-indexes.yamsql:113 + explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 + AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], + rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) + | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating + AS rating)' + task_count: 467 + task_total_time_ms: 193 + transform_count: 149 + transform_time_ms: 112 + transform_yield_count: 37 + insert_time_ms: 8 + insert_new_count: 53 + insert_reused_count: 3 +- query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM + "restaurant", "restaurant"."reviews" AS "r" WHERE "r"."reviewer" = 'Alice' + ref: unnested-record-type-indexes.yamsql:122 + explain: 'COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], + unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer + AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 362 + task_total_time_ms: 190 + transform_count: 123 + transform_time_ms: 115 + transform_yield_count: 32 + insert_time_ms: 5 + insert_new_count: 39 + insert_reused_count: 1 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql new file mode 100644 index 00000000000..41f9ae29e39 --- /dev/null +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -0,0 +1,126 @@ +# +# unnested-record-type-indexes.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-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. + +--- +options: + supported_version: !current_version +--- +# End-to-end tests for an index over a single unnesting, and for predicates on top of it. +# +# An index over an unnesting only needs a synthetic record type when two or more columns reached through the same +# unnesting are not adjacent in the index key. `mv_split` is such an index: `reviewer` and `rating` both come from one +# review, but `region` (a column of the parent row) sits between them. Evaluating that key as a plain fan-out would +# produce the cross product of the reviews with themselves, so the index is instead defined over a synthetic type +# whose records pair the parent row with a single review. +schema_template: + CREATE TYPE AS STRUCT "review" ("reviewer" STRING, "rating" BIGINT) + + CREATE TABLE "restaurant" ("rest_no" BIGINT, "region" STRING, "reviews" "review" ARRAY, PRIMARY KEY ("rest_no")) + CREATE INDEX "mv_split" AS + SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" +--- +setup: + steps: + - query: INSERT INTO "restaurant" + VALUES (1, 'north', [('Alice', 5), ('Bob', 3)]), + (2, 'south', [('Carol', 4)]), + (3, 'north', [('Dave', 2), ('Alice', 4)]) +--- +test_block: + tests: + - + # The query the index was defined for. The index supplies the requested ordering, so no sort is needed, and + # every selected column is in the index key, so the scan is covering. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" + - explain: "COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}, + {reviewer: 'Bob', region: 'north', rating: 3}, + {reviewer: 'Carol', region: 'south', rating: 4}, + {reviewer: 'Dave', region: 'north', rating: 2}] + - + # The same unnesting without an ORDER BY. The index still answers it; the result order is not guaranteed. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + - explain: "COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - unorderedResult: [ + {reviewer: 'Alice', region: 'north', rating: 5}, + {reviewer: 'Bob', region: 'north', rating: 3}, + {reviewer: 'Carol', region: 'south', rating: 4}, + {reviewer: 'Dave', region: 'north', rating: 2}, + {reviewer: 'Alice', region: 'north', rating: 4}] + - + # Selecting a prefix of the key rather than all of it. + - query: SELECT SQ."reviewer" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + ORDER BY SQ."reviewer" + - explain: "COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)" + - result: [ + {reviewer: 'Alice'}, + {reviewer: 'Alice'}, + {reviewer: 'Bob'}, + {reviewer: 'Carol'}, + {reviewer: 'Dave'}] + - + # PartiQL-style unnesting binds the array element directly rather than packing it into a subquery tuple. The + # index is defined with the subquery spelling, but both describe the same unnesting. + - query: SELECT "r"."reviewer", "restaurant"."region", "r"."rating" + FROM "restaurant", "restaurant"."reviews" AS "r" + ORDER BY "r"."reviewer", "restaurant"."region", "r"."rating" + - explain: "COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}, + {reviewer: 'Bob', region: 'north', rating: 3}, + {reviewer: 'Carol', region: 'south', rating: 4}, + {reviewer: 'Dave', region: 'north', rating: 2}] + - + # An equality on the leading key column. The index's placeholders live on the same select as the constituents' + # quantifiers, so a predicate over the unnesting can be matched against them and bind the scan. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' + - explain: "COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}] + - + # A full key prefix, spanning the unnesting and the parent row, binds both key columns. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' + - explain: "COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}] + - + # An equality on the unnesting reached through the PartiQL spelling of the same unnesting. + - query: SELECT "r"."reviewer", "restaurant"."region", "r"."rating" + FROM "restaurant", "restaurant"."reviews" AS "r" + WHERE "r"."reviewer" = 'Alice' + - explain: "COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}] +... From 71e6b67f419d04617fd9201239c973bf1fd3cb5d Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Thu, 1 Oct 2026 15:28:52 +0100 Subject: [PATCH 02/11] changes --- .../src/test/java/YamlIntegrationTests.java | 1 - ...unnested-record-type-indexes.metrics.binpb | Bin 15678 -> 36675 bytes .../unnested-record-type-indexes.metrics.yaml | 160 +++++++++++++++++- .../unnested-record-type-indexes.yamsql | 142 ++++++++++++++++ 4 files changed, 295 insertions(+), 8 deletions(-) diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 8a2e898f2a8..21b106ad1d5 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -536,7 +536,6 @@ public void uuidNonPreparedTest(YamlTest.Runner runner) throws Exception { * @see MetaDataExportUtilityTests#createValidIdentifiersMetaData() for how the custom meta-data is generated */ @TestTemplate - @MaintainYamlTestConfig(YamlTestConfigFilters.CORRECT_METRICS) public void validIdentifierTests(YamlTest.Runner runner) throws Exception { runner.runYamsql("valid-identifiers.yamsql"); } diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index f1ed390fd3c6a3b42196e2250d9a9b789a44545c..bfaeadafd06b2b06e29a132a4fabef21e0209f4a 100644 GIT binary patch literal 36675 zcmeHQdu$ZP8Lv%+=rsxUwp7Nb>2yo+Svntn*8*(WA#P%oM`9CFaM5Y)TkI3-TeG)@ z(zH@ZX%q>lk^pLiLP|@N(5kJHHl%53Leo@%L@8;3Dh&bD5-OTBL~4{YMJQ>%Z)R_A zc5c^aADr*u{R6X)nc3ZMUcdYOzHjEQm4P#5flrLZFgCwU|kkggQuF2kDPQM)gF@gfC1iBK=?^_;mQf zNc|%6mF_i*`ddPze%-nd8C485ZbA}f4(o0M+(BbS;<|OEIYb&Ty`!XlgPOrg(nvEg zYg?D#5$kHnH_7s@+ek1H<}!zq^@3JP9*0OI%w7m?W9_h;c!akc+3Az%eR@<|{o?h} z*pQ)&4wGBRT>(PY>v0oXM*E;XX2euuF0Sf=%t)qe?VN&)ejVC8h!YqD0YRL-BT5 zQ`ehv$7)3zQ^`_&WOU4gx7OoJ7Mjrx_=9O((o?f-Y;JUZxz(VZhQm~zrb{bAG#>XJ z3+b14bdW(^)8W$xG-YsuOcKM&sM;=%YDz2)XXX|S&Jwy7i^^nuOw*jHZQIM1@gWgZ zM!Hrg)tb}#E%)w6O%ux}N%W9K*V*H{}^w$e(&>Rcx4u-+PTU2jYE zO{xJkOM2o__3NZ>P{~wt(9nj|MPz|yF0MO&@53Pie-;dxi}P=2+k~up!|=yho>{r8 zx3_DJYmL~Mnf+7MmB2&R{+uPU;UGkuIWaZYn^U*Bimtr$*p51 zhg&Y$Wcm$u7}eQvV3-&-2Vi($J%tWj6Ye~=af`N2Vamc@Y>gr?jBja#F;^I7k^{Pj zT@=I6HxH{yv|Wzt#)zWHgkCb0n1+|)U|Ssg`tXGQ!b_nDtIyHyN71qmPkeZC zHITE=;7Kb7PgwUpA<$l1Xo2$>?u9SHVS;X|TM6j9?ye zrsR|?o+iCNJ1}?F+hrAR{o?qit9r;EpZ-H2_?h~joo#NL;}aIsg|KL9%ZMyOWFw^V z)>FhHe8R#fEQ*$U_o0g@MixIDEimZP)RuxSO`!iyv7YiyW#EVdx;!C5mxAyVx%1ZU z-fpLx2x%uZU0Q5Vt&x6Q5tuh?=ZuQ~?moP>YHJzUbLdoa@C)@9_dP$OZDD7bbkM~c zq`&+2JGz#EGHd7~x~T>`2b&rZNHNW~cHJ~}6**U`t|BcHf;N!K8`_9P_|PWD&}I!!-f-ZZzZ4(dAzdqO zc6j329=nK%z`@5Hu94nEOmuQa#oi|mf30dy8Tt9qKeq(GSpVXq+dkgb(m6vqHhD}G zBI!eLn~#~QKPpws)Y>!=ItWsEVTXh$C@WT78K~lec$FcIZnhA46jV(UjsV-fPXhL|WWjwqCH@Qh6%-k)p z%~+0?`JYkwpcbY_JHt|nESx9jBv&q&Gxl!_=`b8%XUy>M)I@p3h>b!#JYa!PN89qN(k~j(}>u zNM)Q%$oPWy<0R`Voj{3xSBeWT zSTFURoHK@fj?V~$H%%8YqA1fQy3?joisT&^cIBxl)) zav6hN%(Yu!tWTaJvA0Qq*F8|u^CC*JNExfojSrJp+VLPya=98OkijY6+ZTHBk7g(c zJfe5JyzNHth`xOC?(4uK`qJy2)FawQK?2c{Hiefi3KD&slem$acMwE69Hm`QkXQ_B z^Ry6aBFl0B&ruYYJ!Lwe5yQmd6l^adkz=}`R0F(=d7jL1l}{|-%SS;LFMuPF&^xaR z;}#eS%CrCUQ&QkH4;1vAh=K}{E4q_&tWIJiPBeSf@~pma<~Iw`v-;HcF3kbY>MKY0 zlvB^@XFRx|TzOZQ8d3B?sS-t^ue29L;vKGCAd*;&50SJ6eyn_P-B5ng_bs*aG@ma&Djv}K9uSJ=8(h%l?z@|owF#55oF7ka=6WcWC|j{l1*5wpVaE<38N zzW;_&U6aST60vA5P7re%Oq9aXOqT!YHO2N=b)UMxDm^*>T9xcmKbh7&^%lUa9?efj z%*LYnWmSb>dhBXAg7Nv|5Z!8B$1#q-rEiX_2HwMA5dTk~3|*gtNjW(zHBWLEF-_rW zWZdwZWe3ZZNHz^xK!GtxdDhV`RR*5)Ktd0RNJvJALc3%-6r%Uo5G-J!BxZ64JJ?Yn zFSmNo1RuiS4lWt=@|n3HkexVv8J7&&d0`P1$b7g{!f;2Bu4t3QhGb0_obn~W9U-F+ zcaj@uWZWTR@-$B2@3b%tCcA0@Z9l*EgWL6*Dyz&oOagP&Z)TFSf5IO^`~cjG4o_ zVYyQ)JUJvjCY8sfaOnstVn7i+b>F@!polK~{a;8C9sQt=Qbaq`p_27|*=u6>A!(Wz zeKl`Uj9S9hz(NGg2>X7b@ocHgd%I3d9mce5u1^5PZtvAeAB@-OfvAts=i*&@Fn#DE z7mIO>k!Ul##R&^x!D?9!)7Nm0PU&IT3V4CVs`ca~&Z^nh$FkSQtlsTnV^JVmA2XbF zk?f(^_)>}$s}k&k){uvX=`z@4O2ZPk_L_PYcM;PRx;kbJ#aZ^RTIs6L5kU~LLfF42f`KqC?fn7-vI!(w5Wt)CL}59Jk6d>$=ieY59;Kek&=JJh zKXLb=x!^Cf{gopD@E1Dtw@&IWkPyP_z2r}cCc<;?A)C^D#OS`M6&GU;!Q?wNn|nxyW0Xk|oIqg`%* b)rFy9Ogad0GU}TTt$b*;cw=*ZcXIzfqQ|_( delta 8 PcmX>+k7-}k20L2-66FKO diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml index 0224732f02a..157a15c04ae 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -2,7 +2,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" - ref: unnested-record-type-indexes.yamsql:55 + ref: unnested-record-type-indexes.yamsql:99 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -17,7 +17,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:66 + ref: unnested-record-type-indexes.yamsql:110 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -32,7 +32,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:78 + ref: unnested-record-type-indexes.yamsql:122 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)' @@ -47,7 +47,7 @@ unnamed-3: - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" ORDER BY "r"."reviewer", "restaurant"."region", "r"."rating" - ref: unnested-record-type-indexes.yamsql:91 + ref: unnested-record-type-indexes.yamsql:135 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -63,7 +63,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:104 + ref: unnested-record-type-indexes.yamsql:148 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -79,7 +79,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' - ref: unnested-record-type-indexes.yamsql:113 + ref: unnested-record-type-indexes.yamsql:157 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -95,7 +95,7 @@ unnamed-3: insert_reused_count: 3 - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" WHERE "r"."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:122 + ref: unnested-record-type-indexes.yamsql:166 explain: 'COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -108,3 +108,149 @@ unnamed-3: insert_time_ms: 5 insert_new_count: 39 insert_reused_count: 1 +escaped-identifiers: +- query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") + AS SQ ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + ref: unnested-record-type-indexes.yamsql:181 + explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: + KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, + _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)' + task_count: 261 + task_total_time_ms: 63 + transform_count: 78 + transform_time_ms: 27 + transform_yield_count: 24 + insert_time_ms: 1 + insert_new_count: 26 + insert_reused_count: 0 +- query: EXPLAIN SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" + FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" ORDER BY "r"."re.viewer", + "resta.urant$t"."re.gion", "r"."ra$ting" + ref: unnested-record-type-indexes.yamsql:191 + explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: + KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, + _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)' + task_count: 236 + task_total_time_ms: 62 + transform_count: 73 + transform_time_ms: 25 + transform_yield_count: 22 + insert_time_ms: 1 + insert_new_count: 21 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") + AS SQ WHERE SQ."re.viewer" = 'Alice' + ref: unnested-record-type-indexes.yamsql:202 + explain: 'COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, + 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer + AS re.viewer, _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)' + task_count: 397 + task_total_time_ms: 99 + transform_count: 129 + transform_time_ms: 45 + transform_yield_count: 35 + insert_time_ms: 4 + insert_new_count: 47 + insert_reused_count: 1 +deeply-nested-array: +- query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT + "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ ORDER BY SQ."label", + "item"."kind", SQ."weight" + ref: unnested-record-type-indexes.yamsql:214 + explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], + weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, + _.unnesting_0.weight AS weight)' + task_count: 261 + task_total_time_ms: 58 + transform_count: 78 + transform_time_ms: 24 + transform_yield_count: 24 + insert_time_ms: 1 + insert_new_count: 26 + insert_reused_count: 0 +- query: EXPLAIN SELECT "t"."label", "item"."kind", "t"."weight" FROM "item", "item"."meta"."tag_set"."tags" + AS "t" ORDER BY "t"."label", "item"."kind", "t"."weight" + ref: unnested-record-type-indexes.yamsql:223 + explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], + weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, + _.unnesting_0.weight AS weight)' + task_count: 236 + task_total_time_ms: 53 + transform_count: 73 + transform_time_ms: 22 + transform_yield_count: 22 + insert_time_ms: 0 + insert_new_count: 21 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT + "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" + = 'hammer' + ref: unnested-record-type-indexes.yamsql:232 + explain: 'COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: + [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind + AS kind, _.unnesting_0.weight AS weight)' + task_count: 397 + task_total_time_ms: 85 + transform_count: 129 + transform_time_ms: 38 + transform_yield_count: 35 + insert_time_ms: 3 + insert_new_count: 47 + insert_reused_count: 1 +deeply-nested-element: +- query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" + FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", + "a"."score" + ref: unnested-record-type-indexes.yamsql:244 + explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, + 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], + name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title + AS title, _.unnesting_0.score AS score)' + task_count: 236 + task_total_time_ms: 50 + transform_count: 73 + transform_time_ms: 22 + transform_yield_count: 22 + insert_time_ms: 0 + insert_new_count: 21 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" FROM + "book", (SELECT "name", "score" FROM "book"."authors") AS SQ ORDER BY SQ."name"."last", + "book"."title", SQ."score" + ref: unnested-record-type-indexes.yamsql:254 + explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, + 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], + name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title + AS title, _.unnesting_0.score AS score)' + task_count: 261 + task_total_time_ms: 53 + transform_count: 78 + transform_time_ms: 22 + transform_yield_count: 24 + insert_time_ms: 1 + insert_new_count: 26 + insert_reused_count: 0 +- query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" + FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER + BY "a"."name"."last", "book"."title", "a"."score" + ref: unnested-record-type-indexes.yamsql:264 + explain: 'COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], + unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last + AS last, _.parent.title AS title, _.unnesting_0.score AS score)' + task_count: 322 + task_total_time_ms: 70 + transform_count: 113 + transform_time_ms: 39 + transform_yield_count: 29 + insert_time_ms: 1 + insert_new_count: 31 + insert_reused_count: 0 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index 41f9ae29e39..aca2b74f217 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -28,6 +28,14 @@ options: # review, but `region` (a column of the parent row) sits between them. Evaluating that key as a plain fan-out would # produce the cross product of the reviews with themselves, so the index is instead defined over a synthetic type # whose records pair the parent row with a single review. +# +# The other three tables repeat that shape over an unnesting that is harder to reach: +# `resta.urant$t` spells every name on the way to the unnesting with characters a protobuf field cannot be named +# after, so the metadata has to escape them into storage names; +# `item` holds its array under a chain of non-repeated structs, so the constituent's nesting expression is +# a path of several fields rather than one; +# `book` holds structs inside the elements of its array, so a key column is a path into the exploded +# element rather than a field of it. schema_template: CREATE TYPE AS STRUCT "review" ("reviewer" STRING, "rating" BIGINT) @@ -36,6 +44,33 @@ schema_template: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" + + CREATE TYPE AS STRUCT "re.view$t" ("re.viewer" STRING, "ra$ting" BIGINT) + CREATE TABLE "resta.urant$t" ("rest.no" BIGINT, "re.gion" STRING, "re.views" "re.view$t" ARRAY, + PRIMARY KEY ("rest.no")) + CREATE INDEX "mv.split$esc" AS + SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ + ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + + CREATE TYPE AS STRUCT "tag" ("label" STRING, "weight" BIGINT) + CREATE TYPE AS STRUCT "tag_set" ("tags" "tag" ARRAY, "note" STRING) + CREATE TYPE AS STRUCT "item_meta" ("tag_set" "tag_set", "owner" STRING) + + CREATE TABLE "item" ("item_no" BIGINT, "kind" STRING, "meta" "item_meta", PRIMARY KEY ("item_no")) + CREATE INDEX "mv_deep_array" AS + SELECT SQ."label", "item"."kind", SQ."weight" + FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ + ORDER BY SQ."label", "item"."kind", SQ."weight" + + CREATE TYPE AS STRUCT "author_name" ("first" STRING, "last" STRING) + CREATE TYPE AS STRUCT "author" ("name" "author_name", "score" BIGINT) + + CREATE TABLE "book" ("book_no" BIGINT, "title" STRING, "authors" "author" ARRAY, PRIMARY KEY ("book_no")) + CREATE INDEX "mv_deep_element" AS + SELECT "a"."name"."last", "book"."title", "a"."score" + FROM "book", "book"."authors" AS "a" + ORDER BY "a"."name"."last", "book"."title", "a"."score" --- setup: steps: @@ -43,6 +78,15 @@ setup: VALUES (1, 'north', [('Alice', 5), ('Bob', 3)]), (2, 'south', [('Carol', 4)]), (3, 'north', [('Dave', 2), ('Alice', 4)]) + - query: INSERT INTO "resta.urant$t" + VALUES (1, 'north', [('Alice', 5), ('Bob', 3)]), + (2, 'south', [('Carol', 4)]) + - query: INSERT INTO "item" + VALUES (1, 'tool', (([('hammer', 7), ('nail', 2)], 'n1'), 'ann')), + (2, 'toy', (([('kite', 4)], 'n2'), 'bob')) + - query: INSERT INTO "book" + VALUES (1, 'Dune', [(('Frank', 'Herbert'), 9), (('Brian', 'Herbert'), 5)]), + (2, 'Emma', [(('Jane', 'Austen'), 8)]) --- test_block: tests: @@ -123,4 +167,102 @@ test_block: - result: [ {reviewer: 'Alice', region: 'north', rating: 4}, {reviewer: 'Alice', region: 'north', rating: 5}] +--- +# Identifiers a protobuf field cannot be named after. Every name on the path to the unnesting -- the table, the array, +# and the columns of its elements -- needs escaping, and the index only matches if the candidate and the query agree on +# the escaped names. +test_block: + name: escaped-identifiers + tests: + - + - query: SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ + ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + - explain: "COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)" + - result: [ + {'re.viewer': 'Alice', 're.gion': 'north', 'ra$ting': 5}, + {'re.viewer': 'Bob', 're.gion': 'north', 'ra$ting': 3}, + {'re.viewer': 'Carol', 're.gion': 'south', 'ra$ting': 4}] + - + # The PartiQL spelling of the same unnesting, with the array reached through an escaped name. + - query: SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" + FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" + ORDER BY "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" + - explain: "COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)" + - result: [ + {'re.viewer': 'Alice', 're.gion': 'north', 'ra$ting': 5}, + {'re.viewer': 'Bob', 're.gion': 'north', 'ra$ting': 3}, + {'re.viewer': 'Carol', 're.gion': 'south', 'ra$ting': 4}] + - + # An equality on the leading key column binds the scan, so the escaped names also have to line up between the + # query's predicate value and the candidate's placeholder. + - query: SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" + FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ + WHERE SQ."re.viewer" = 'Alice' + - explain: "COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, _.parent.re.gion AS re.gion, _.unnesting_0.ra$ting AS ra$ting)" + - result: [{'re.viewer': 'Alice', 're.gion': 'north', 'ra$ting': 5}] +--- +# An array under a chain of non-repeated structs: the constituent's nesting expression is `meta.tag_set.tags` rather +# than a single field, so the explode's collection value is a path of field accesses. +test_block: + name: deeply-nested-array + tests: + - + - query: SELECT SQ."label", "item"."kind", SQ."weight" + FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ + ORDER BY SQ."label", "item"."kind", SQ."weight" + - explain: "COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, _.unnesting_0.weight AS weight)" + - result: [ + {label: 'hammer', kind: 'tool', weight: 7}, + {label: 'kite', kind: 'toy', weight: 4}, + {label: 'nail', kind: 'tool', weight: 2}] + - + - query: SELECT "t"."label", "item"."kind", "t"."weight" + FROM "item", "item"."meta"."tag_set"."tags" AS "t" + ORDER BY "t"."label", "item"."kind", "t"."weight" + - explain: "COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, _.unnesting_0.weight AS weight)" + - result: [ + {label: 'hammer', kind: 'tool', weight: 7}, + {label: 'kite', kind: 'toy', weight: 4}, + {label: 'nail', kind: 'tool', weight: 2}] + - + - query: SELECT SQ."label", "item"."kind", SQ."weight" + FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ + WHERE SQ."label" = 'hammer' + - explain: "COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, _.unnesting_0.weight AS weight)" + - result: [{label: 'hammer', kind: 'tool', weight: 7}] +--- +# An array whose elements carry a struct of their own, with a key column reached through it: `a.name.last` is a path +# into the exploded element rather than one of its fields. +test_block: + name: deeply-nested-element + tests: + - + - query: SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" + FROM "book", "book"."authors" AS "a" + ORDER BY "a"."name"."last", "book"."title", "a"."score" + - explain: "COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score)" + - result: [ + {last: 'Austen', title: 'Emma', score: 8}, + {last: 'Herbert', title: 'Dune', score: 5}, + {last: 'Herbert', title: 'Dune', score: 9}] + - + # The subquery spelling of the same unnesting, flowing the element's struct and reaching into it outside. + - query: SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" + FROM "book", (SELECT "name", "score" FROM "book"."authors") AS SQ + ORDER BY SQ."name"."last", "book"."title", SQ."score" + - explain: "COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score)" + - result: [ + {last: 'Austen', title: 'Emma', score: 8}, + {last: 'Herbert', title: 'Dune', score: 5}, + {last: 'Herbert', title: 'Dune', score: 9}] + - + - query: SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" + FROM "book", "book"."authors" AS "a" + WHERE "a"."name"."last" = 'Herbert' + ORDER BY "a"."name"."last", "book"."title", "a"."score" + - explain: "COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score)" + - result: [ + {last: 'Herbert', title: 'Dune', score: 5}, + {last: 'Herbert', title: 'Dune', score: 9}] ... From 21a68cc1e43b1dfada86d6cbf3242a31554ff025 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Thu, 1 Oct 2026 15:47:14 +0100 Subject: [PATCH 03/11] more changes --- .../query/TransactionBoundQueryTest.java | 10 +-- ...unnested-record-type-indexes.metrics.binpb | Bin 36675 -> 43778 bytes .../unnested-record-type-indexes.metrics.yaml | 80 ++++++++++++++---- .../unnested-record-type-indexes.yamsql | 47 +++++++++- 4 files changed, 114 insertions(+), 23 deletions(-) diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/TransactionBoundQueryTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/TransactionBoundQueryTest.java index d430de7b520..b01909c2c4a 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/TransactionBoundQueryTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/TransactionBoundQueryTest.java @@ -202,7 +202,7 @@ void canUseRecordLayerIndex() throws SQLException, RelationalException { } @Test - void ignoreSyntheticIndexes() throws SQLException, RelationalException { + void canUseUnnestedSyntheticRecordType() throws SQLException, RelationalException { RecordMetaData metaDataOrig = getUpdatedMetaData(metaDataBuilder -> { }); RecordMetaData metaData = getUpdatedMetaData(metaDataBuilder -> { @@ -240,7 +240,6 @@ void ignoreSyntheticIndexes() throws SQLException, RelationalException { .isPresent() .get() .satisfies(table -> - // The table should contain only the index that Relational can process Assertions.assertThat(table.getIndexes()) .map(Metadata::getName) .contains("aIndex") @@ -249,8 +248,6 @@ void ignoreSyntheticIndexes() throws SQLException, RelationalException { ); try (RelationalStatement statement = connection.createStatement()) { - // This query could use the unnested index if we planned against it correctly. In particular, - // it could be executed as scan on the first two columns of the index String sqlQuery = "SELECT e.value " + "FROM t2, (SELECT key, value FROM t2.d) e " + "WHERE t2.c = 'foo' AND e.key = 'bar'"; @@ -258,8 +255,9 @@ void ignoreSyntheticIndexes() throws SQLException, RelationalException { RelationalResultSetAssert.assertThat(resultSet) .hasNextRow(); Assertions.assertThat(resultSet.getString("plan")) - .doesNotContain("unnestedIndex") - .contains("ISCAN(cIndex"); + .as("Planner should select the index on the unnested synthetic record type and bind both equalities") + .containsPattern("COVERING\\(unnestedIndex \\[EQUALS [^,]+, EQUALS [^]]+\\]") + .doesNotContain("ISCAN(cIndex"); RelationalResultSetAssert.assertThat(resultSet) .hasNoNextRow(); } diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index bfaeadafd06b2b06e29a132a4fabef21e0209f4a..5468a628190e29c4f467cd00d9ec04be6213ee31 100644 GIT binary patch delta 2287 zcmX>+kE!Vz(+21M`o98P#~HaK3W_pw6N@T!vr{W|GxLBbCA9*?5n9FQ8WG^*=;@~r z?CRs{9HI~$sHc>YSgE9=pj4KcSDLD%2chHh@_}L?+2Yj1l42zVw;+FCm>L}g4XAdQ zUZ^4=WK}?|Ir$|M=ZZ%WV}Z~HelAIl9wvcRCs#{L1Sve}Tgjs#X>fLmr-ifwgU~xh zXa6wQAWuJcjoh;Mg6#Orym+98CLb)Z;s84w=-^3FQo=y1tQ5RmBdwwhVs#Y2&YElx zrQ`(^S8xng08%;%@p=V`MX7ludN7S385kQRU7DAdT3nKummY7R2Q~tv0L;*oS~$7U z{^sQS!hGJyMi!cw;5QVc0JpJ|8AWxVG`N@txhq#aeIH1#fvcGr)ZS71oB*5y20+w4U87%4opHTnXo6~h5&`h>IRZbKK_$8X45%6Uje4)8OM31y>Wg0c@eIa_zLG9Y=tv8(-K+y zVwS$eN}8bj9vrCPA2e|#H*qEL0%2fDe2z(AYU62ni6n(t`@eH*NE=N4A84TfEQ#B3 zBw`{<;uNrlz=aT~VnHu%fmsbC2QHWJmeZOFH446t0SX%N7)A6CfyshQ*98sH3vXa4 z+dO%L69+5U3BaV6IoToCEXu?jPfFHNC`6M-b|FX(PhA4DPz`J$usVg8)?oj^N_JRr zKDjYHbMk+YE40o5){G{TKXN5ZzU!kl042SPOip4_YL2aqT3%9dfi*A)?Gzv)N -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -17,7 +17,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:110 + ref: unnested-record-type-indexes.yamsql:121 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -32,7 +32,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:122 + ref: unnested-record-type-indexes.yamsql:133 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)' @@ -47,7 +47,7 @@ unnamed-3: - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" ORDER BY "r"."reviewer", "restaurant"."region", "r"."rating" - ref: unnested-record-type-indexes.yamsql:135 + ref: unnested-record-type-indexes.yamsql:146 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -63,7 +63,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:148 + ref: unnested-record-type-indexes.yamsql:159 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -79,7 +79,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' - ref: unnested-record-type-indexes.yamsql:157 + ref: unnested-record-type-indexes.yamsql:168 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -95,7 +95,7 @@ unnamed-3: insert_reused_count: 3 - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" WHERE "r"."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:166 + ref: unnested-record-type-indexes.yamsql:177 explain: 'COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -112,7 +112,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" - ref: unnested-record-type-indexes.yamsql:181 + ref: unnested-record-type-indexes.yamsql:192 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -128,7 +128,7 @@ escaped-identifiers: - query: EXPLAIN SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" ORDER BY "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" - ref: unnested-record-type-indexes.yamsql:191 + ref: unnested-record-type-indexes.yamsql:202 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -144,7 +144,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ WHERE SQ."re.viewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:202 + ref: unnested-record-type-indexes.yamsql:213 explain: 'COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer @@ -161,7 +161,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ ORDER BY SQ."label", "item"."kind", SQ."weight" - ref: unnested-record-type-indexes.yamsql:214 + ref: unnested-record-type-indexes.yamsql:225 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -176,7 +176,7 @@ deeply-nested-array: insert_reused_count: 0 - query: EXPLAIN SELECT "t"."label", "item"."kind", "t"."weight" FROM "item", "item"."meta"."tag_set"."tags" AS "t" ORDER BY "t"."label", "item"."kind", "t"."weight" - ref: unnested-record-type-indexes.yamsql:223 + ref: unnested-record-type-indexes.yamsql:234 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -192,7 +192,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:232 + ref: unnested-record-type-indexes.yamsql:243 explain: 'COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind @@ -209,7 +209,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:244 + ref: unnested-record-type-indexes.yamsql:255 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -225,7 +225,7 @@ deeply-nested-element: - query: EXPLAIN SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" FROM "book", (SELECT "name", "score" FROM "book"."authors") AS SQ ORDER BY SQ."name"."last", "book"."title", SQ."score" - ref: unnested-record-type-indexes.yamsql:254 + ref: unnested-record-type-indexes.yamsql:265 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -241,7 +241,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:264 + ref: unnested-record-type-indexes.yamsql:275 explain: 'COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last @@ -254,3 +254,51 @@ deeply-nested-element: insert_time_ms: 1 insert_new_count: 31 insert_reused_count: 0 +primary-key-in-index-key: +- query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT + "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", + SQ."seats" + ref: unnested-record-type-indexes.yamsql:288 + explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) + | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats + AS seats)' + task_count: 268 + task_total_time_ms: 57 + transform_count: 82 + transform_time_ms: 25 + transform_yield_count: 25 + insert_time_ms: 1 + insert_new_count: 27 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT + "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' AND "venue"."venue_no" + = 1 + ref: unnested-record-type-indexes.yamsql:297 + explain: 'COVERING(mv_pk_in_key [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 + AS LONG)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], + unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS + day, _.parent.venue_no AS venue_no, _.unnesting_0.seats AS seats)' + task_count: 479 + task_total_time_ms: 55 + transform_count: 149 + transform_time_ms: 24 + transform_yield_count: 39 + insert_time_ms: 2 + insert_new_count: 55 + insert_reused_count: 2 +- query: EXPLAIN SELECT SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") + AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" + ref: unnested-record-type-indexes.yamsql:303 + explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], + parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) + | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no + AS venue_no) | MAP (_.seats AS seats)' + task_count: 334 + task_total_time_ms: 66 + transform_count: 98 + transform_time_ms: 25 + transform_yield_count: 27 + insert_time_ms: 1 + insert_new_count: 32 + insert_reused_count: 0 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index aca2b74f217..1e5ed8a8c32 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -35,7 +35,8 @@ options: # `item` holds its array under a chain of non-repeated structs, so the constituent's nesting expression is # a path of several fields rather than one; # `book` holds structs inside the elements of its array, so a key column is a path into the exploded -# element rather than a field of it. +# element rather than a field of it; +# `venue` names its own primary key in the index key, which the synthetic type's primary key also carries. schema_template: CREATE TYPE AS STRUCT "review" ("reviewer" STRING, "rating" BIGINT) @@ -71,6 +72,13 @@ schema_template: SELECT "a"."name"."last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", "a"."score" + + CREATE TYPE AS STRUCT "slot" ("day" STRING, "seats" BIGINT) + CREATE TABLE "venue" ("venue_no" BIGINT, "city" STRING, "slots" "slot" ARRAY, PRIMARY KEY ("venue_no")) + CREATE INDEX "mv_pk_in_key" AS + SELECT SQ."day", "venue"."venue_no", SQ."seats" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + ORDER BY SQ."day", "venue"."venue_no", SQ."seats" --- setup: steps: @@ -87,6 +95,9 @@ setup: - query: INSERT INTO "book" VALUES (1, 'Dune', [(('Frank', 'Herbert'), 9), (('Brian', 'Herbert'), 5)]), (2, 'Emma', [(('Jane', 'Austen'), 8)]) + - query: INSERT INTO "venue" + VALUES (1, 'SF', [('mon', 10), ('tue', 20)]), + (2, 'LA', [('wed', 5)]) --- test_block: tests: @@ -265,4 +276,38 @@ test_block: - result: [ {last: 'Herbert', title: 'Dune', score: 5}, {last: 'Herbert', title: 'Dune', score: 9}] +--- +# The parent table's primary key named in the index key, where the synthetic type's primary key already carries it. An +# index over an unnesting does not trim the redundant component, so the column is simply stored twice. +test_block: + name: primary-key-in-index-key + tests: + - + - query: SELECT SQ."day", "venue"."venue_no", SQ."seats" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + ORDER BY SQ."day", "venue"."venue_no", SQ."seats" + # `unnesting_0` sits at KEY:[5, 0] rather than KEY:[4, 0], so position 4 still holds the parent's primary key -- + # `venue_no` really is in the entry twice. The covering map reads it from KEY:[1], the index key position, as the + # decoder keeps the first occurrence it finds and so never descends into the nested tuple for it. + - explain: "COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats AS seats)" + - result: [ + {day: 'mon', venue_no: 1, seats: 10}, + {day: 'tue', venue_no: 1, seats: 20}, + {day: 'wed', venue_no: 2, seats: 5}] + - + - query: SELECT SQ."day", "venue"."venue_no", SQ."seats" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + WHERE SQ."day" = 'mon' AND "venue"."venue_no" = 1 + # Being carried twice costs the column nothing: the occurrence in the index key is an ordinary scalar position, so + # it still has a placeholder of its own and both predicates bind the scan. + - explain: "COVERING(mv_pk_in_key [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS LONG)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats AS seats)" + - result: [{day: 'mon', venue_no: 1, seats: 10}] + - + - query: SELECT SQ."seats" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + ORDER BY SQ."day", "venue"."venue_no", SQ."seats" + # The sort columns are not selected, so the covering scan flows them for the sort and a second MAP drops them + # again. `venue_no` is read from KEY:[1] here too, even though it is only wanted for the ordering. + - explain: "COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no AS venue_no) | MAP (_.seats AS seats)" + - result: [{seats: 10}, {seats: 20}, {seats: 5}] ... From 71b29eec096074ed28b9a91e319c64c7e698ca69 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Fri, 2 Oct 2026 12:20:11 +0100 Subject: [PATCH 04/11] Add range predicate tests for the index on the unnested record type Six queries covering an inequality on the leading key column, both sides of the same column bounding one range, and an equality prefix of one and two columns followed by a range on the column after it. Each is served by a covering scan of the unnesting index with no residual filter, which is what the bounds being pushed into the scan range looks like. --- ...unnested-record-type-indexes.metrics.binpb | Bin 43778 -> 58151 bytes .../unnested-record-type-indexes.metrics.yaml | 124 ++++++++++++++++-- .../unnested-record-type-indexes.yamsql | 52 ++++++++ 3 files changed, 164 insertions(+), 12 deletions(-) diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index 5468a628190e29c4f467cd00d9ec04be6213ee31..8dda50bbddc55dbac433fb8db6c571120c65818f 100644 GIT binary patch delta 1112 zcmZp=#l%J$7v{QgfnxmIl z;LOY^dJ>I{3U}wN712;Om@@4jqlLZ$qtJ%Q3ps`PqNCk|TpdGPgW^Lx9Q`Kq8%Qw5 zPF@gTIXQrFKjX8>jrKPtm*w(L-p435d1VmqWGQCB&0(tbjJz1ym||lm2Xr<~-r&SD zdA0bq$#p)hoA+wOGBN&`tgW<5+eSg%Ik6}|M_p($)Lq}d&XEGS>)M}gUZA`Ft=j`} z*IJOfxTB+eT!VwLIqES^M{%L*!Q~>M*Fs#kPy+4@DB#?H0q2;LnVhPw;OOU~5FDtd zRFqnlnOdG&q@)0JzDr_Rs=83OAS6_$+&u3Ba{b?r59EQapE=_JGdNVQ;s{k<1vNDV zgqQRUBw?Ya=H2oDIr00e z>mVn#cYTF8@g3BO_G&r`C<#%apeR2#za&+|A=$)4!7*6D$KTIg6P}=%mVlkU*j+HiYivz_gcTmU&!b{+r# delta 14 VcmZ2}jJfF=(*`@+&B>)lase`B2C@JE diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml index 6364c1d6102..8f1535058ee 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -108,11 +108,111 @@ unnamed-3: insert_time_ms: 5 insert_new_count: 39 insert_reused_count: 1 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + > 'Bob' + ref: unnested-record-type-indexes.yamsql:187 + explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING)]] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], + unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer + AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 397 + task_total_time_ms: 96 + transform_count: 129 + transform_time_ms: 42 + transform_yield_count: 35 + insert_time_ms: 4 + insert_new_count: 47 + insert_reused_count: 1 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + < 'Carol' + ref: unnested-record-type-indexes.yamsql:195 + explain: 'COVERING(mv_split [[LESS_THAN promote(@c32 AS STRING)]] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], + unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer + AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 397 + task_total_time_ms: 54 + transform_count: 129 + transform_time_ms: 27 + transform_yield_count: 35 + insert_time_ms: 3 + insert_new_count: 47 + insert_reused_count: 1 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + > 'Alice' AND SQ."reviewer" < 'Dave' + ref: unnested-record-type-indexes.yamsql:205 + explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING) && LESS_THAN + promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: + [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: + KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, + _.unnesting_0.rating AS rating)' + task_count: 397 + task_total_time_ms: 143 + transform_count: 129 + transform_time_ms: 66 + transform_yield_count: 35 + insert_time_ms: 7 + insert_new_count: 47 + insert_reused_count: 1 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + = 'Alice' AND "restaurant"."region" < 'south' + ref: unnested-record-type-indexes.yamsql:215 + explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), [LESS_THAN promote(@c38 + AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: + KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) + | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating + AS rating)' + task_count: 467 + task_total_time_ms: 147 + transform_count: 149 + transform_time_ms: 67 + transform_yield_count: 37 + insert_time_ms: 7 + insert_new_count: 53 + insert_reused_count: 3 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" > 4 + ref: unnested-record-type-indexes.yamsql:225 + explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 + AS STRING), [GREATER_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: + KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: + [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, + _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 467 + task_total_time_ms: 152 + transform_count: 149 + transform_time_ms: 68 + transform_yield_count: 37 + insert_time_ms: 8 + insert_new_count: 53 + insert_reused_count: 3 +- query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" + = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" < 5 + ref: unnested-record-type-indexes.yamsql:231 + explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 + AS STRING), [LESS_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: + KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: + [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, + _.parent.region AS region, _.unnesting_0.rating AS rating)' + task_count: 467 + task_total_time_ms: 150 + transform_count: 149 + transform_time_ms: 66 + transform_yield_count: 37 + insert_time_ms: 8 + insert_new_count: 53 + insert_reused_count: 3 escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" - ref: unnested-record-type-indexes.yamsql:192 + ref: unnested-record-type-indexes.yamsql:244 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -128,7 +228,7 @@ escaped-identifiers: - query: EXPLAIN SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" ORDER BY "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" - ref: unnested-record-type-indexes.yamsql:202 + ref: unnested-record-type-indexes.yamsql:254 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -144,7 +244,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ WHERE SQ."re.viewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:213 + ref: unnested-record-type-indexes.yamsql:265 explain: 'COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer @@ -161,7 +261,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ ORDER BY SQ."label", "item"."kind", SQ."weight" - ref: unnested-record-type-indexes.yamsql:225 + ref: unnested-record-type-indexes.yamsql:277 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -176,7 +276,7 @@ deeply-nested-array: insert_reused_count: 0 - query: EXPLAIN SELECT "t"."label", "item"."kind", "t"."weight" FROM "item", "item"."meta"."tag_set"."tags" AS "t" ORDER BY "t"."label", "item"."kind", "t"."weight" - ref: unnested-record-type-indexes.yamsql:234 + ref: unnested-record-type-indexes.yamsql:286 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -192,7 +292,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:243 + ref: unnested-record-type-indexes.yamsql:295 explain: 'COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind @@ -209,7 +309,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:255 + ref: unnested-record-type-indexes.yamsql:307 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -225,7 +325,7 @@ deeply-nested-element: - query: EXPLAIN SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" FROM "book", (SELECT "name", "score" FROM "book"."authors") AS SQ ORDER BY SQ."name"."last", "book"."title", SQ."score" - ref: unnested-record-type-indexes.yamsql:265 + ref: unnested-record-type-indexes.yamsql:317 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -241,7 +341,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:275 + ref: unnested-record-type-indexes.yamsql:327 explain: 'COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last @@ -258,7 +358,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:288 + ref: unnested-record-type-indexes.yamsql:344 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats @@ -274,7 +374,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' AND "venue"."venue_no" = 1 - ref: unnested-record-type-indexes.yamsql:297 + ref: unnested-record-type-indexes.yamsql:355 explain: 'COVERING(mv_pk_in_key [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS LONG)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS @@ -289,7 +389,7 @@ primary-key-in-index-key: insert_reused_count: 2 - query: EXPLAIN SELECT SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:303 + ref: unnested-record-type-indexes.yamsql:363 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index 1e5ed8a8c32..b4af9636352 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -178,6 +178,58 @@ test_block: - result: [ {reviewer: 'Alice', region: 'north', rating: 4}, {reviewer: 'Alice', region: 'north', rating: 5}] + - + # A range on the leading key column rather than an equality. The placeholder reserved for that column takes the + # inequality, so the scan is a one-sided range over the index instead of a scan of all of it with a filter. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" > 'Bob' + - explain: "COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Carol', region: 'south', rating: 4}, + {reviewer: 'Dave', region: 'north', rating: 2}] + - + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" < 'Carol' + - explain: "COVERING(mv_split [[LESS_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}, + {reviewer: 'Bob', region: 'north', rating: 3}] + - + # Both sides of the same column, which bound one range rather than two. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" > 'Alice' AND SQ."reviewer" < 'Dave' + - explain: "COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING) && LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Bob', region: 'north', rating: 3}, + {reviewer: 'Carol', region: 'south', rating: 4}] + - + # An equality on the leading column followed by a range on the next one. The equality fixes the prefix and the + # range bounds the column after it, so the two columns still make a single scan range. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" < 'south' + - explain: "COVERING(mv_split [EQUALS promote(@c32 AS STRING), [LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [ + {reviewer: 'Alice', region: 'north', rating: 4}, + {reviewer: 'Alice', region: 'north', rating: 5}] + - + # Equalities on both leading columns -- one from the unnesting, one from the parent row -- followed by a range on + # the third. + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" > 4 + - explain: "COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [GREATER_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [{reviewer: 'Alice', region: 'north', rating: 5}] + - + - query: SELECT SQ."reviewer", "restaurant"."region", SQ."rating" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" < 5 + - explain: "COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [LESS_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, _.unnesting_0.rating AS rating)" + - result: [{reviewer: 'Alice', region: 'north', rating: 4}] --- # Identifiers a protobuf field cannot be named after. Every name on the path to the unnesting -- the table, the array, # and the columns of its elements -- needs escaping, and the index only matches if the candidate and the query agree on From 1f8f04a931a0e86efab64bf4ba5bde5126f01393 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Tue, 6 Oct 2026 10:35:47 +0100 Subject: [PATCH 05/11] more tests --- .../ValueIndexScanMatchCandidate.java | 19 ++- ...unnested-record-type-indexes.metrics.binpb | Bin 58151 -> 91590 bytes .../unnested-record-type-indexes.metrics.yaml | 156 ++++++++++++++++++ .../unnested-record-type-indexes.yamsql | 89 ++++++++++ 4 files changed, 261 insertions(+), 3 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java index a7e3d90f5d5..acebca10dd4 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java @@ -21,6 +21,7 @@ package com.apple.foundationdb.record.query.plan.cascades; import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.logging.LogMessageKeys; import com.apple.foundationdb.record.metadata.Index; import com.apple.foundationdb.record.metadata.RecordType; @@ -239,7 +240,7 @@ public RecordQueryPlan toEquivalentPlan(@Nonnull final PartialMatch partialMatch primaryKey, IndexScanComparisons.byValue(toScanComparisons(comparisonRanges)), planContext.getPlannerConfiguration().getIndexFetchMethod(), - RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY, + resolveFetchIndexRecords(), reverseScanOrder, false, partialMatch.getMatchCandidate(), @@ -266,7 +267,7 @@ private Optional tryFetchCoveringIndexScan(@Nonnull final Parti primaryKey, scanParameters, planContext.getPlannerConfiguration().getIndexFetchMethod(), - RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY, + resolveFetchIndexRecords(), isReverse, false, partialMatch.getMatchCandidate(), @@ -279,7 +280,7 @@ private Optional tryFetchCoveringIndexScan(@Nonnull final Parti indexEntryToLogicalRecord.indexKeyValueToPartialRecord()); return Optional.of(new RecordQueryFetchFromPartialRecordPlan(Quantifier.physical(memoizer.memoizePlan(coveringIndexPlan)), - coveringIndexPlan::pushValueThroughFetch, baseRecordType, RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY)); + coveringIndexPlan::pushValueThroughFetch, baseRecordType, resolveFetchIndexRecords())); } @Nonnull @@ -298,6 +299,18 @@ public Optional pushValueThroughFetch(@Nonnull final Value toBePushedValu indexEntryToLogicalRecord.logicalValueValues())); } + @Nonnull + private RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords resolveFetchIndexRecords() { + final var anySynthetic = queriedRecordTypes.stream().anyMatch(RecordType::isSynthetic); + if (anySynthetic && !queriedRecordTypes.stream().allMatch(RecordType::isSynthetic)) { + throw new RecordCoreException("cannot mix regular and synthetic record types in index candidate") + .addLogInfo(LogMessageKeys.INDEX_NAME, index.getName()); + } + return anySynthetic + ? RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.SYNTHETIC_CONSTITUENTS + : RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY; + } + @Override public boolean isScopedToSingleType() { return queriedRecordTypes.size() == 1 || hasAndOrderedByRecordTypeKey(); diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index 8dda50bbddc55dbac433fb8db6c571120c65818f..05da28340d8504be634b2a463d0c959cd5384adf 100644 GIT binary patch delta 6383 zcmd^^e{2-T700`dP0V7<`M_{K+c4f3*h|jWyUU%qgxIwKgNTf6{DXoJ-7NNEww!nG zcGnm;F;)U;Q?L_~*)kV4~>K&65q5h$P#11-O*LLvQwq(v$!D60A+otfRe zJKvAQAxLf2fAHS!+u4~npZA^j9(ztaaN=~vftjCF-JL)WP;{;+wFv7<)Mj2P=@7Nm zC94Fjd9`&jRkgIPx^iI+XsD{Ln%fANwSv?xFfOK9)H)d_XjtrGTKG=*?#WvCp30`}nYYs{J1$?f+Pc7&W825FMec!3FHiwz1!dhtEo_)uSp%;QmoO_F;2E%} zvJSAEE5s{;q`C0Opt1qrzfQoxX{Ykwjk8m!pF!%}r95wPv#=N0*(on9zmg9-PW7ha zx8Z@`=fR5W8IV0KLa-zQE;(EbEA{~R#c&qv>)8xlXZ8+SK3bZVXuL8d-C_S-7R*Vz_@z#n0;x&86Ln`cG}bSyS%963_l!z6 zy>cS8C#HTrnZA?K6Ib9PM+zpd5*1Zr@UV=2=>!bVz-hl^p?c)G>HnOBJa8k;vh#1x zk4HG1-ncQ7Ep)#);0gfbfjb5bg-46C{CEs~k`8$GAsak!Fe8Tyi64*)Oho=M0LKoM zmPgS^uyF->Rl_ggzvvy8CTc+e-yjzb`%NqMoG;*te>EJ4dl%gp`7(AWe0VSy9zB#p zoLINLbh_8oE=hu_iBc=)c1e7jkT|jz&AbRRdMpq9d-UxB`0YbI#z-u@+`GyIl=2T= zKi*Ek_pZGFpL#Q6@cZwqr{IsTXUB6Ig1I-~xIh$M=vk^PJCH*Ur`)lF2@x%7LK_O` zHbLXjAGt#k6n!uCOFhO(bii>h^CZgUUDg>_Cg?%l|BLGdXjhNidSyHc^5ehWrU&^e z#`$vmy75MX+tr-aI&rSsbGZET5bRXd8RzpEiOB&gD zIL?g`jRA<<^k5aJT(AJJ&2qb>ajM2E#`ih511zYYSKCwv<}3p&Hmt1yRg3Bxmw}q5 z>S{AGnq@vHsLcWwl%-ZAu?S84Eq9QtLUPz&%tK*tp=hLC*n`bRSR&>(z#UUEDajz- zomGs?Ky5ww9(=iHQzl2YnRCOlz2d|MO%eDu6b+!#aH;@rPo7pdx3-!bBuUqVE$9XH z=CaI>Mn~R%O@t`gicK0!=Y>avpYA>5?d55ldy9=7$9WtKSTUOhzz=WJPZ{UI3Al64 zA*vML83aq7AyEe3xUqgF06tJs4ju)|jg#St8PZBMG&}6~N5jqUM~6jdIvNt&`Vws8 zj+Ia*0HxT{4~HMzj6Ty{emjR&tQ4!nk@MJzsN6$GZ#%IE-Uvc?B>2!{*^-@0(z ziXiOY{|^k|`F%A6!malULbbh(3u1;mE`26sBHZHyMRUtil4!7pw}wpJVc&$&yO?9L z7I|XsdWOD4d`i`Azen7PBe?0aN4pn4f4AfRj=J3E^5}!{)D6J8vcjo&7Z^oYD+(Rx z7;u6F(QE3C#3y0WG_{_Xf~4@nGz)zF!)#kKxcJ$&F>Jp3{g>vDQ-1gTqOh4uu7xIm zI-ka@h!7w?BPk|PHGJ1xBGz35v@Tsp51*Zkp)8JtGHOb6F=C83-3l$)v@VJFE_l-- z4rRlBG&%~e5z|NC>9w2M>IBubdIo3Q@nE&RM@>xQJaGRt31uC+3=(0C$Y8eL6Qgt< zL+MheViKj(HPKYM+=F)dYCLlvVdmz?GnZQL5Ri*1mnMqgN|bmZElkPgIh)SrotuZO zh|Qneyo}jA`0=BJ%}e)-O|?6@M4}V7jbE2cJkb#!-TiNZF40QoB!gxTb}4p>!+x0n zJ$n>D>q7t&^2it_=MxPB6F}m0q|T|xuSn2d9d2@p>1Rj8F(&vi&pbqzM-v<~k_AUh zppcay)c-dEOb>SaC(?t`S^o{` zfnKb5AA|g!pMLH9v~*TfwM%Dx!~8V&PkD5IJd8g?MSn>i7GhN8yBM`uRsMYnDz@w04S7UNTcdc@l(|l^rF1L_5Db>WpF@`Vk6 zj88OjesyId?p=o6xTt#?;JU4LUKRT7QiHOlVidw2P+!&9R9^#FWM=3nx{0q4q+JLz zT;hvtjDC{oCg;@cGMA^1l?+qUVnl9B4sVD3E>TrBHXVqXn@2yHCwT#3vjKkm2kUqf zSmSQ$Q$I?qQE~Z7aTJVL5+*kGV879 zQkK^kcR0>VZgQMQTLBmMzl1uq^WnPd0RG+5H6vw{A~wDl7`7m${_l00HA7}L7|Lkku|gl4B!<-?pQ7GEoF=( zOX#`Ai@}hrXy|tb1Mn05m&%}VBvn&*Q9=(_@j(??lkQ9x`kekr) | MAP (_.parent.city AS city, _.unnesting_0.day + AS day) + task_count: 240 + task_total_time_ms: 35 + transform_count: 78 + transform_time_ms: 15 + transform_yield_count: 23 + insert_time_ms: 0 + insert_new_count: 24 + insert_reused_count: 0 +- query: EXPLAIN SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", + "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" + = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" + ref: unnested-record-type-indexes.yamsql:389 + explain: ISCAN(mv_deep_element [EQUALS promote(@c35 AS STRING)]) | MAP (_.unnesting_0.name.first + AS first, _.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score + AS score) | MAP (_.first AS first, _.last AS last, _.title AS title) + task_count: 354 + task_total_time_ms: 27 + transform_count: 125 + transform_time_ms: 16 + transform_yield_count: 29 + insert_time_ms: 0 + insert_new_count: 32 + insert_reused_count: 0 +- query: EXPLAIN SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" + FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") + AS SQ WHERE SQ."label" = 'hammer' + ref: unnested-record-type-indexes.yamsql:396 + explain: ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner + AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight) + task_count: 369 + task_total_time_ms: 60 + transform_count: 125 + transform_time_ms: 27 + transform_yield_count: 33 + insert_time_ms: 2 + insert_new_count: 44 + insert_reused_count: 1 +aggregations: +- query: EXPLAIN SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" + FROM "restaurant"."reviews") AS SQ + ref: unnested-record-type-indexes.yamsql:407 + explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG + (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, + 0l) AS n) + task_count: 365 + task_total_time_ms: 35 + transform_count: 102 + transform_time_ms: 10 + transform_yield_count: 29 + insert_time_ms: 0 + insert_new_count: 39 + insert_reused_count: 0 +- query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", + "rating" FROM "restaurant"."reviews") AS SQ + ref: unnested-record-type-indexes.yamsql:411 + explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG + (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) + task_count: 365 + task_total_time_ms: 38 + transform_count: 102 + transform_time_ms: 12 + transform_yield_count: 29 + insert_time_ms: 1 + insert_new_count: 39 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" + ref: unnested-record-type-indexes.yamsql:417 + explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG + (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, + _._1._0 AS n) + task_count: 329 + task_total_time_ms: 30 + transform_count: 99 + transform_time_ms: 13 + transform_yield_count: 27 + insert_time_ms: 1 + insert_new_count: 32 + insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", + MAX(SQ."rating") AS "best" FROM "restaurant", (SELECT "reviewer", "rating" + FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" + ref: unnested-record-type-indexes.yamsql:422 + explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG + (sum_l(_._1.rating) AS _0, max_l(_._1.rating) AS _1) GROUP BY (_._1.reviewer + AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS total, _._1._1 AS best) + task_count: 329 + task_total_time_ms: 38 + transform_count: 99 + transform_time_ms: 14 + transform_yield_count: 27 + insert_time_ms: 0 + insert_new_count: 32 + insert_reused_count: 0 +- query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", + "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' + ref: unnested-record-type-indexes.yamsql:434 + explain: ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, + _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY + NULL | MAP (_._0._0 AS total) + task_count: 472 + task_total_time_ms: 59 + transform_count: 146 + transform_time_ms: 26 + transform_yield_count: 37 + insert_time_ms: 2 + insert_new_count: 55 + insert_reused_count: 1 +- query: EXPLAIN SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" + FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" + ref: unnested-record-type-indexes.yamsql:440 + explain: ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) + | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 + AS last, _._1._0 AS best) + task_count: 304 + task_total_time_ms: 36 + transform_count: 94 + transform_time_ms: 13 + transform_yield_count: 25 + insert_time_ms: 0 + insert_new_count: 27 + insert_reused_count: 0 +- query: EXPLAIN SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ GROUP BY + "venue"."venue_no" + ref: unnested-record-type-indexes.yamsql:446 + explain: SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 + AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no + AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total) + task_count: 283 + task_total_time_ms: 35 + transform_count: 81 + transform_time_ms: 12 + transform_yield_count: 25 + insert_time_ms: 0 + insert_new_count: 28 + insert_reused_count: 0 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index b4af9636352..b3d35550864 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -362,4 +362,93 @@ test_block: # again. `venue_no` is read from KEY:[1] here too, even though it is only wanted for the ordering. - explain: "COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no AS venue_no) | MAP (_.seats AS seats)" - result: [{seats: 10}, {seats: 20}, {seats: 5}] +--- +test_block: + name: index-scan-with-fetch + tests: + - + - query: SELECT "venue"."city", SQ."day", SQ."seats" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + WHERE SQ."day" = 'mon' + - explain: "ISCAN(mv_pk_in_key [EQUALS promote(@c32 AS STRING)]) | MAP (_.parent.city AS city, _.unnesting_0.day AS day, _.unnesting_0.seats AS seats)" + - result: [{city: 'SF', day: 'mon', seats: 10}] + - + - query: SELECT "venue"."city", SQ."day" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + ORDER BY SQ."day" + - explain: "ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day)" + - result: [{city: 'SF', day: 'mon'}, {city: 'SF', day: 'tue'}, {city: 'LA', day: 'wed'}] + - + - query: SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" + FROM "book", "book"."authors" AS "a" + WHERE "a"."name"."last" = 'Herbert' + ORDER BY "a"."name"."last", "book"."title", "a"."score" + - explain: "ISCAN(mv_deep_element [EQUALS promote(@c35 AS STRING)]) | MAP (_.unnesting_0.name.first AS first, _.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score) | MAP (_.first AS first, _.last AS last, _.title AS title)" + - result: [ + {first: 'Brian', last: 'Herbert', title: 'Dune'}, + {first: 'Frank', last: 'Herbert', title: 'Dune'}] + - + - query: SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" + FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ + WHERE SQ."label" = 'hammer' + - explain: "ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight)" + - result: [{owner: 'ann', label: 'hammer', weight: 7}] +--- +test_block: + name: aggregations + tests: + - + - query: SELECT COUNT(*) AS "n" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n)" + - result: [{n: 5}] + - + - query: SELECT SUM(SQ."rating") AS "total" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" + - result: [{total: 18}] + - + - query: SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + GROUP BY SQ."reviewer" + - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS n)" + - unorderedResult: [ + {reviewer: 'Alice', n: 2}, + {reviewer: 'Bob', n: 1}, + {reviewer: 'Carol', n: 1}, + {reviewer: 'Dave', n: 1}] + - + - query: SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", MAX(SQ."rating") AS "best" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + GROUP BY SQ."reviewer" + - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0, max_l(_._1.rating) AS _1) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS total, _._1._1 AS best)" + - unorderedResult: [ + {reviewer: 'Alice', total: 9, best: 5}, + {reviewer: 'Bob', total: 3, best: 3}, + {reviewer: 'Carol', total: 4, best: 4}, + {reviewer: 'Dave', total: 2, best: 2}] + - + # Grouping on a column of the parent row, which sits between the two unnested columns in the key. + - query: SELECT "restaurant"."region" AS "region", COUNT(*) AS "n" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + GROUP BY "restaurant"."region" + - error: "0AF00" + - + - query: SELECT SUM(SQ."rating") AS "total" + FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ + WHERE SQ."reviewer" = 'Alice' + - explain: "ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" + - result: [{total: 9}] + - + - query: SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" + FROM "book", "book"."authors" AS "a" + GROUP BY "a"."name"."last" + - explain: "ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best)" + - unorderedResult: [{last: 'Austen', best: 8}, {last: 'Herbert', best: 9}] + - + - query: SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" + FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ + GROUP BY "venue"."venue_no" + - explain: "SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total)" + - unorderedResult: [{venue_no: 1, total: 30}, {venue_no: 2, total: 5}] ... From 8dba6e2536e183648ce9f5fc2a67220e5cd7f63d Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Wed, 7 Oct 2026 12:18:54 +0100 Subject: [PATCH 06/11] nit --- .../cascades/ScanWithFetchMatchCandidate.java | 40 ++++-------------- ...unnested-record-type-indexes.metrics.binpb | Bin 91590 -> 94765 bytes .../unnested-record-type-indexes.metrics.yaml | 35 ++++++++++----- .../unnested-record-type-indexes.yamsql | 13 +++++- 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java index 15244c901d7..3dceecbb159 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ScanWithFetchMatchCandidate.java @@ -22,7 +22,6 @@ import com.apple.foundationdb.record.EvaluationContext; import com.apple.foundationdb.record.metadata.RecordType; -import com.apple.foundationdb.record.metadata.expressions.ListKeyExpression; import com.apple.foundationdb.record.query.plan.IndexKeyValueToPartialRecord; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; @@ -153,9 +152,9 @@ static Optional computeIn for (final var datum : entryData(keyValue, ImmutableIntArray.of(i))) { final var extractFromIndexEntryPairOptional = - datum.value().extractFromIndexEntryMaybe(baseObjectValue, EvaluationContext.empty(), + datum.getLeft().extractFromIndexEntryMaybe(baseObjectValue, EvaluationContext.empty(), AliasMap.emptyMap(), ImmutableSet.of(), - IndexKeyValueToPartialRecord.TupleSource.KEY, datum.ordinalPath()); + IndexKeyValueToPartialRecord.TupleSource.KEY, datum.getRight()); if (extractFromIndexEntryPairOptional.isPresent()) { final var extractFromIndexEntryPair = extractFromIndexEntryPairOptional.get(); if (!addCoveringField(builder, extractFromIndexEntryPair.getKey(), @@ -199,47 +198,24 @@ static Optional computeIn /** * Decomposes one column of the index entry into the data it holds that can be read individually. * - *

A column usually holds a single datum, at {@code tuple.get(ordinal)}. A {@link ListKeyExpression} is the - * exception: it places its child in a nested tuple rather than flattening it, so the data sit one level deeper, - * one element per column of that child. The {@link Value} registered for such a column is the - * {@link RecordConstructorValue} of those columns' values, which is what {@code KeyExpressionExpansionVisitor} - * builds for it, so the record mirrors the tuple and the recursion can follow the value at every level. - * * @param value the value registered for the column * @param ordinalPath the path to the column - * @return the data that column holds + * @return the data that column holds, each datum paired with the path at which it sits in the entry's tuple */ @Nonnull - private static List entryData(@Nonnull final Value value, - @Nonnull final ImmutableIntArray ordinalPath) { + private static List> entryData(@Nonnull final Value value, + @Nonnull final ImmutableIntArray ordinalPath) { if (!(value instanceof RecordConstructorValue)) { - return ImmutableList.of(new EntryDatum(value, ordinalPath)); + return ImmutableList.of(NonnullPair.of(value, ordinalPath)); } final var columns = ((RecordConstructorValue)value).getColumns(); - final var dataBuilder = ImmutableList.builder(); + final var dataBuilder = ImmutableList.>builder(); for (int i = 0; i < columns.size(); i++) { - dataBuilder.addAll(entryData(columns.get(i).getValue(), appendOrdinal(ordinalPath, i))); + dataBuilder.addAll(entryData(columns.get(i).getValue(), ImmutableIntArray.builder().addAll(ordinalPath).add(i).build())); } return dataBuilder.build(); } - @Nonnull - private static ImmutableIntArray appendOrdinal(@Nonnull final ImmutableIntArray ordinalPath, final int ordinal) { - return ImmutableIntArray.builder(ordinalPath.length() + 1) - .addAll(ordinalPath) - .add(ordinal) - .build(); - } - - /** - * A datum of an index entry that can be read on its own. - * - * @param value the value that datum holds - * @param ordinalPath the path at which it sits in the entry's tuple - */ - record EntryDatum(@Nonnull Value value, @Nonnull ImmutableIntArray ordinalPath) { - } - /** * Records what the given extraction covers, descending a node per field of the path it fills, which may run into * nested messages. Every name is present here, {@code addCoveringField} having already refused the extraction diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index 05da28340d8504be634b2a463d0c959cd5384adf..6d2febc3b361006a1fcd83e4024e33c91df1b343 100644 GIT binary patch delta 146 zcmV;D0B!%q%LT3E1%R{x#$^!Y7z(Qa3I}?6aB^>AWfGs0P(2-&5HA4+19l=Tm(d0R zW0(FS0T>^&7YZ2&$pR3WvAzlzV<6zS+yf{ZFtF|$I3Pd(5}udgF9ArGeIo%!6Lu^h zUoKxUE?+PpK~o@ZDH`<^m(f@O;{tMam&hamo&$C)AeV6o0dkkEO#wT%93}z62v49e AUjP6A delta 11 ScmZ4cg!R~I)`l&N$5H_!t_6(% diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml index d409c7a9d06..f1030d352d1 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -418,7 +418,7 @@ index-scan-with-fetch: insert_reused_count: 1 - query: EXPLAIN SELECT "venue"."city", SQ."day" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day" - ref: unnested-record-type-indexes.yamsql:382 + ref: unnested-record-type-indexes.yamsql:383 explain: ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day) task_count: 240 @@ -432,7 +432,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:389 + ref: unnested-record-type-indexes.yamsql:391 explain: ISCAN(mv_deep_element [EQUALS promote(@c35 AS STRING)]) | MAP (_.unnesting_0.name.first AS first, _.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score) | MAP (_.first AS first, _.last AS last, _.title AS title) @@ -447,7 +447,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:396 + ref: unnested-record-type-indexes.yamsql:401 explain: ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight) task_count: 369 @@ -461,7 +461,7 @@ index-scan-with-fetch: aggregations: - query: EXPLAIN SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:407 + ref: unnested-record-type-indexes.yamsql:413 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n) @@ -475,7 +475,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:411 + ref: unnested-record-type-indexes.yamsql:418 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) task_count: 365 @@ -488,7 +488,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:417 + ref: unnested-record-type-indexes.yamsql:425 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS n) @@ -503,7 +503,7 @@ aggregations: - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", MAX(SQ."rating") AS "best" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:422 + ref: unnested-record-type-indexes.yamsql:435 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0, max_l(_._1.rating) AS _1) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS total, _._1._1 AS best) @@ -517,7 +517,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:434 + ref: unnested-record-type-indexes.yamsql:452 explain: ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) @@ -531,7 +531,7 @@ aggregations: insert_reused_count: 1 - query: EXPLAIN SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" - ref: unnested-record-type-indexes.yamsql:440 + ref: unnested-record-type-indexes.yamsql:459 explain: ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best) @@ -546,7 +546,7 @@ aggregations: - query: EXPLAIN SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ GROUP BY "venue"."venue_no" - ref: unnested-record-type-indexes.yamsql:446 + ref: unnested-record-type-indexes.yamsql:466 explain: SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total) @@ -558,3 +558,18 @@ aggregations: insert_time_ms: 0 insert_new_count: 28 insert_reused_count: 0 +zzprobe: +- query: EXPLAIN SELECT SQ."reviewer" AS "rv", COUNT(*) AS "n" FROM "restaurant", + (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" + ref: unnested-record-type-indexes.yamsql:474 + explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG + (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS rv, + _._1._0 AS n) + task_count: 329 + task_total_time_ms: 21 + transform_count: 99 + transform_time_ms: 7 + transform_yield_count: 27 + insert_time_ms: 0 + insert_new_count: 32 + insert_reused_count: 0 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index b3d35550864..985df6bf207 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -363,22 +363,27 @@ test_block: - explain: "COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no AS venue_no) | MAP (_.seats AS seats)" - result: [{seats: 10}, {seats: 20}, {seats: 5}] --- +# Queries that select a column the index does not store. The scan can still be driven by the index, but the row has to +# be fetched to supply the missing column, so the plan is an `ISCAN` followed by a fetch rather than a covering scan. test_block: name: index-scan-with-fetch tests: - + # `city` is a column of the parent row that the index key does not mention. - query: SELECT "venue"."city", SQ."day", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' - explain: "ISCAN(mv_pk_in_key [EQUALS promote(@c32 AS STRING)]) | MAP (_.parent.city AS city, _.unnesting_0.day AS day, _.unnesting_0.seats AS seats)" - result: [{city: 'SF', day: 'mon', seats: 10}] - + # The same over the whole index, without a predicate to narrow the scan. - query: SELECT "venue"."city", SQ."day" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day" - explain: "ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day)" - result: [{city: 'SF', day: 'mon'}, {city: 'SF', day: 'tue'}, {city: 'LA', day: 'wed'}] - + # A sibling of an indexed column inside the exploded element: `last` is in the key, `first` is not. - query: SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' @@ -388,26 +393,33 @@ test_block: {first: 'Brian', last: 'Herbert', title: 'Dune'}, {first: 'Frank', last: 'Herbert', title: 'Dune'}] - + # A column of a struct on the way down to the array, which the constituent's nesting expression passes through + # without indexing. - query: SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - explain: "ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight)" - result: [{owner: 'ann', label: 'hammer', weight: 7}] --- +# Aggregates over an unnesting whose index is a plain (non-aggregate) index. The index supplies the rows to aggregate -- +# covering where every column it needs is in the key -- and the aggregation runs on top of the scan. test_block: name: aggregations tests: - + # no grouping - query: SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n)" - result: [{n: 5}] - + # no grouping - query: SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" - result: [{total: 18}] - + # Grouping on the leading key column, which the index already orders by. - query: SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" @@ -428,7 +440,6 @@ test_block: {reviewer: 'Carol', total: 4, best: 4}, {reviewer: 'Dave', total: 2, best: 2}] - - # Grouping on a column of the parent row, which sits between the two unnested columns in the key. - query: SELECT "restaurant"."region" AS "region", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY "restaurant"."region" From 4d26935f010c100c1ae8c49f84223a5497a38c47 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Wed, 7 Oct 2026 14:17:50 +0100 Subject: [PATCH 07/11] issue --- .../unnested-record-type-indexes.yamsql | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index 985df6bf207..5022185b57e 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -363,27 +363,23 @@ test_block: - explain: "COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no AS venue_no) | MAP (_.seats AS seats)" - result: [{seats: 10}, {seats: 20}, {seats: 5}] --- -# Queries that select a column the index does not store. The scan can still be driven by the index, but the row has to -# be fetched to supply the missing column, so the plan is an `ISCAN` followed by a fetch rather than a covering scan. +# Queries that select a column the index does not store. test_block: name: index-scan-with-fetch tests: - - # `city` is a column of the parent row that the index key does not mention. - query: SELECT "venue"."city", SQ."day", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' - explain: "ISCAN(mv_pk_in_key [EQUALS promote(@c32 AS STRING)]) | MAP (_.parent.city AS city, _.unnesting_0.day AS day, _.unnesting_0.seats AS seats)" - result: [{city: 'SF', day: 'mon', seats: 10}] - - # The same over the whole index, without a predicate to narrow the scan. - query: SELECT "venue"."city", SQ."day" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day" - explain: "ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day)" - result: [{city: 'SF', day: 'mon'}, {city: 'SF', day: 'tue'}, {city: 'LA', day: 'wed'}] - - # A sibling of an indexed column inside the exploded element: `last` is in the key, `first` is not. - query: SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' @@ -393,33 +389,33 @@ test_block: {first: 'Brian', last: 'Herbert', title: 'Dune'}, {first: 'Frank', last: 'Herbert', title: 'Dune'}] - - # A column of a struct on the way down to the array, which the constituent's nesting expression passes through - # without indexing. - query: SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - explain: "ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight)" - result: [{owner: 'ann', label: 'hammer', weight: 7}] --- -# Aggregates over an unnesting whose index is a plain (non-aggregate) index. The index supplies the rows to aggregate -- -# covering where every column it needs is in the key -- and the aggregation runs on top of the scan. +# Aggregates over an unnesting whose index is a plain (non-aggregate) index. test_block: name: aggregations tests: - # no grouping + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n)" - result: [{n: 5}] - # no grouping + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - explain: "ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" - result: [{total: 18}] - # Grouping on the leading key column, which the index already orders by. + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" @@ -430,6 +426,7 @@ test_block: {reviewer: 'Carol', n: 1}, {reviewer: 'Dave', n: 1}] - + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", MAX(SQ."rating") AS "best" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" @@ -445,12 +442,14 @@ test_block: GROUP BY "restaurant"."region" - error: "0AF00" - + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - explain: "ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" - result: [{total: 9}] - + # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" From b5b5d6b59d9d6813c77303edcaff608d57c3c582 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Wed, 7 Oct 2026 16:17:24 +0100 Subject: [PATCH 08/11] more tests --- ...unnested-record-type-indexes.metrics.binpb | Bin 94765 -> 117290 bytes .../unnested-record-type-indexes.metrics.yaml | 239 ++++++++++++++---- .../unnested-record-type-indexes.yamsql | 167 +++++++++++- 3 files changed, 342 insertions(+), 64 deletions(-) diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index 6d2febc3b361006a1fcd83e4024e33c91df1b343..6d1b964754b099bc5b72fbb4b9a0d26c34b30fc9 100644 GIT binary patch delta 7289 zcmdU!eNYtV9mo4{pm=~Rcf1@V)yIp5J?6MB#~}+Xl+&Ck2_kYPF`@Kw!lhgx_fA_| zm{U8Nq!2})#I3E#46&7tnZ$84$yD2=$=WRdF_xF2#_risoeNWBZa0vX?7yrnpe+}rXqOoAa*IOO)t{d=G z`vbkc?^b_1S{+>m@F#&IZBGpE(DnFssT=5};in7Lqp|69+TCFA=$xS$HN)rea{{V%q<}Oxxbg$`bZNEFwW7{h6 z0ox#u?H9P)fd#cpKy>}>d^9#Xl0z-g$xBPh=?GQz>3WjcqK!3M=ZQVwB|#m+m||6- zX%!WYb^|}^*v$!Vc|cwcT{l#;f0RizL0)DT3SC=_7;hJFpz-!nA=(4t`QO(={O5qx z&9Fu&^nuYic5?3!#(iK(`~#EzcxFI8GgTnd5KCaFP#8}|H3JNxv;D3F`2LJws~L>Y!6!AFBJAKTJX>rfd|ET_h_LLhJvh&D0u~Xq~b|_^!u&DWayfGhK;U9?kBIt7I!YWZn!s7nB>Du8LJ(-5J z@M(SK{0r|JKpDY*v2Y23htju0U1B>tI!9}3Y%10t&TMOpq-~98UoeEFnp|$7)U>90 zhs3F#tA$!>j6#{J(^6TIOdm7gjqtvX5%&J)PSfK6KELy{k+pKqUHJs?U&R|?m$VV+ z=+rVz*+$JhM!E}Din}09tErhN)za;)m3m820&=YB;;K-U6>o6^Q_$%<65VAe)7iuj zt8_`DVi^2Rz!$MXXJ@DLK~{O4s;)^go=XQHsd%E%FDurWHagoXe}U2O%M9hA)To0@ z8Iw!_69W#FzSBZAyvgj&$B~F+Pgp_+J}AByFEEWKB+))ph*|xGj+s3By9K5Z0ROV> zpZRPR_v+>YfUj@)g5fGyzUqD#amp-Rd~8mY+k;0z)SF1$i9O4VWPFYAUe|+-T{YcS zNa~`FhhPz|CcUsl9EPf?cy-iuSBFVAi>IlD-9jRCBr)TeT zel`F#a0);T!dNc|$A^y@^)FW!8pJwgvhw2DT+DU~IWwN3J; zyU)GB5`TK~Cp(Eh-MsxzI_giir212hu(DCZq0*TUZz#Esq+<^W5&x{5a~9arYO6ix z(?`LxI-+u_(~2Ah&In%kF!O)s1y2|1pU;fB;VCdTWl<|62XYm2#0}n^tZIfm^RXLz zM#ubZ?+LT%F#tz)oM%{;JN&~#fOlXw7!^;}q!kY~O?81nK@<2QYP!BKNs6`-DMHu( zG0&35j9o*XFh5V_somRbarF7|bNK8$6d&Ht=zo(LBRjQ%e!AG{DH%$DhA_p&tc?ni zw1kXQrWPixgYX3c)0u2$>l2SzO#1;m^ZJn@*3O+fd>-)kU{8qDdPQlPi^CczqACX1 zAuYfm&5fCQab}rQHw}%+zIgj>HmQEMNIjthX8ei;#b) zuy~MUGwbAOVGwF%3_v8ACvv!b)tGd|7C=j)YV zl&=@I_kvJ-`1b|+a7J1A=jRR<}f%q_s61NKV_Ki7hV?(#`+}D zrE4Mo#zG5j{Dt2u(wFC+15Es{XWz^>Eraj9GLgd?xqTmgk1uV>G6d<*Bwej;NZX*& zlt?v`4-V7vr48hZi~Ct6Wb)WZp?pt-O2$Di%BH2_Ee3gSn`G;3a;;>GaBW?}hL}h| zAWfGs0P(2-&5HA4+19l=Tm(d0R zW0(FS0T>^&7YZ2&$pR3WvAzlzV<6zS+yf{ZFtF|$I3Pd(5}udgF9ArGeIo%!6Lu^h zUoKxUE?+PpK~o@ZDH`<^m(f@O;{tMam&hamo&$C)AeV6o0dkkEO#wT%93}z62qB>` A4gdfE diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml index f1030d352d1..9599eb53cad 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -2,7 +2,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" - ref: unnested-record-type-indexes.yamsql:110 + ref: unnested-record-type-indexes.yamsql:154 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -17,7 +17,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:121 + ref: unnested-record-type-indexes.yamsql:165 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -32,7 +32,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:133 + ref: unnested-record-type-indexes.yamsql:177 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)' @@ -47,7 +47,7 @@ unnamed-3: - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" ORDER BY "r"."reviewer", "restaurant"."region", "r"."rating" - ref: unnested-record-type-indexes.yamsql:146 + ref: unnested-record-type-indexes.yamsql:190 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -63,7 +63,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:159 + ref: unnested-record-type-indexes.yamsql:203 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -79,7 +79,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' - ref: unnested-record-type-indexes.yamsql:168 + ref: unnested-record-type-indexes.yamsql:212 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -95,7 +95,7 @@ unnamed-3: insert_reused_count: 3 - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" WHERE "r"."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:177 + ref: unnested-record-type-indexes.yamsql:221 explain: 'COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -111,7 +111,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" > 'Bob' - ref: unnested-record-type-indexes.yamsql:187 + ref: unnested-record-type-indexes.yamsql:231 explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -127,7 +127,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" < 'Carol' - ref: unnested-record-type-indexes.yamsql:195 + ref: unnested-record-type-indexes.yamsql:239 explain: 'COVERING(mv_split [[LESS_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -143,7 +143,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" > 'Alice' AND SQ."reviewer" < 'Dave' - ref: unnested-record-type-indexes.yamsql:205 + ref: unnested-record-type-indexes.yamsql:249 explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING) && LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: @@ -160,7 +160,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" < 'south' - ref: unnested-record-type-indexes.yamsql:215 + ref: unnested-record-type-indexes.yamsql:259 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), [LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -177,7 +177,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" > 4 - ref: unnested-record-type-indexes.yamsql:225 + ref: unnested-record-type-indexes.yamsql:269 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [GREATER_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: @@ -194,7 +194,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" < 5 - ref: unnested-record-type-indexes.yamsql:231 + ref: unnested-record-type-indexes.yamsql:275 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [LESS_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: @@ -212,7 +212,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" - ref: unnested-record-type-indexes.yamsql:244 + ref: unnested-record-type-indexes.yamsql:288 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -228,7 +228,7 @@ escaped-identifiers: - query: EXPLAIN SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" ORDER BY "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" - ref: unnested-record-type-indexes.yamsql:254 + ref: unnested-record-type-indexes.yamsql:298 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -244,7 +244,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ WHERE SQ."re.viewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:265 + ref: unnested-record-type-indexes.yamsql:309 explain: 'COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer @@ -261,7 +261,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ ORDER BY SQ."label", "item"."kind", SQ."weight" - ref: unnested-record-type-indexes.yamsql:277 + ref: unnested-record-type-indexes.yamsql:321 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -276,7 +276,7 @@ deeply-nested-array: insert_reused_count: 0 - query: EXPLAIN SELECT "t"."label", "item"."kind", "t"."weight" FROM "item", "item"."meta"."tag_set"."tags" AS "t" ORDER BY "t"."label", "item"."kind", "t"."weight" - ref: unnested-record-type-indexes.yamsql:286 + ref: unnested-record-type-indexes.yamsql:330 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -292,7 +292,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:295 + ref: unnested-record-type-indexes.yamsql:339 explain: 'COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind @@ -309,7 +309,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:307 + ref: unnested-record-type-indexes.yamsql:351 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -325,7 +325,7 @@ deeply-nested-element: - query: EXPLAIN SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" FROM "book", (SELECT "name", "score" FROM "book"."authors") AS SQ ORDER BY SQ."name"."last", "book"."title", SQ."score" - ref: unnested-record-type-indexes.yamsql:317 + ref: unnested-record-type-indexes.yamsql:361 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -341,7 +341,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:327 + ref: unnested-record-type-indexes.yamsql:371 explain: 'COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last @@ -358,7 +358,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:344 + ref: unnested-record-type-indexes.yamsql:388 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats @@ -374,7 +374,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' AND "venue"."venue_no" = 1 - ref: unnested-record-type-indexes.yamsql:355 + ref: unnested-record-type-indexes.yamsql:399 explain: 'COVERING(mv_pk_in_key [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS LONG)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS @@ -389,7 +389,7 @@ primary-key-in-index-key: insert_reused_count: 2 - query: EXPLAIN SELECT SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:363 + ref: unnested-record-type-indexes.yamsql:407 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no @@ -405,7 +405,7 @@ primary-key-in-index-key: index-scan-with-fetch: - query: EXPLAIN SELECT "venue"."city", SQ."day", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' - ref: unnested-record-type-indexes.yamsql:376 + ref: unnested-record-type-indexes.yamsql:418 explain: ISCAN(mv_pk_in_key [EQUALS promote(@c32 AS STRING)]) | MAP (_.parent.city AS city, _.unnesting_0.day AS day, _.unnesting_0.seats AS seats) task_count: 369 @@ -418,7 +418,7 @@ index-scan-with-fetch: insert_reused_count: 1 - query: EXPLAIN SELECT "venue"."city", SQ."day" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day" - ref: unnested-record-type-indexes.yamsql:383 + ref: unnested-record-type-indexes.yamsql:424 explain: ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day) task_count: 240 @@ -432,7 +432,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:391 + ref: unnested-record-type-indexes.yamsql:431 explain: ISCAN(mv_deep_element [EQUALS promote(@c35 AS STRING)]) | MAP (_.unnesting_0.name.first AS first, _.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score) | MAP (_.first AS first, _.last AS last, _.title AS title) @@ -447,7 +447,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:401 + ref: unnested-record-type-indexes.yamsql:439 explain: ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight) task_count: 369 @@ -461,7 +461,7 @@ index-scan-with-fetch: aggregations: - query: EXPLAIN SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:413 + ref: unnested-record-type-indexes.yamsql:451 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n) @@ -475,7 +475,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:418 + ref: unnested-record-type-indexes.yamsql:458 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) task_count: 365 @@ -488,7 +488,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:425 + ref: unnested-record-type-indexes.yamsql:466 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS n) @@ -503,7 +503,7 @@ aggregations: - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", MAX(SQ."rating") AS "best" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:435 + ref: unnested-record-type-indexes.yamsql:477 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0, max_l(_._1.rating) AS _1) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS total, _._1._1 AS best) @@ -517,7 +517,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:452 + ref: unnested-record-type-indexes.yamsql:493 explain: ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) @@ -531,7 +531,7 @@ aggregations: insert_reused_count: 1 - query: EXPLAIN SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" - ref: unnested-record-type-indexes.yamsql:459 + ref: unnested-record-type-indexes.yamsql:500 explain: ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best) @@ -546,7 +546,7 @@ aggregations: - query: EXPLAIN SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ GROUP BY "venue"."venue_no" - ref: unnested-record-type-indexes.yamsql:466 + ref: unnested-record-type-indexes.yamsql:506 explain: SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total) @@ -558,18 +558,157 @@ aggregations: insert_time_ms: 0 insert_new_count: 28 insert_reused_count: 0 -zzprobe: -- query: EXPLAIN SELECT SQ."reviewer" AS "rv", COUNT(*) AS "n" FROM "restaurant", - (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:474 - explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG - (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS rv, - _._1._0 AS n) - task_count: 329 - task_total_time_ms: 21 - transform_count: 99 - transform_time_ms: 7 - transform_yield_count: 27 - insert_time_ms: 0 - insert_new_count: 32 +stored-table-index-vs-synthetic-type-index: +- query: EXPLAIN SELECT "kiosk"."kiosk_no", SQ."sku", SQ."qty" FROM "kiosk", (SELECT + "sku", "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' + ref: unnested-record-type-indexes.yamsql:538 + explain: ISCAN(kiosk_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { + EXPLODE q0.lines AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.sku AS sku, q1.qty + AS qty) } + task_count: 468 + task_total_time_ms: 78 + transform_count: 127 + transform_time_ms: 32 + transform_yield_count: 38 + insert_time_ms: 4 + insert_new_count: 62 + insert_reused_count: 3 +- query: EXPLAIN SELECT "kiosk"."kiosk_no", SQ."qty" FROM "kiosk", (SELECT "sku", + "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' AND SQ."sku" + = 'apple' + ref: unnested-record-type-indexes.yamsql:550 + explain: ISCAN(kiosk_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { + EXPLODE q0.lines | FILTER _.sku EQUALS promote(@c34 AS STRING) AS q1 RETURN + (q0.kiosk_no AS kiosk_no, q1.qty AS qty) } + task_count: 520 + task_total_time_ms: 69 + transform_count: 140 + transform_time_ms: 27 + transform_yield_count: 39 + insert_time_ms: 4 + insert_new_count: 66 + insert_reused_count: 3 +- query: EXPLAIN SELECT "shop"."shop_no", SQ."sku", SQ."qty" FROM "shop", (SELECT + "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' + ref: unnested-record-type-indexes.yamsql:558 + explain: ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE + q0.lines AS q1 RETURN (q0.shop_no AS shop_no, q1.sku AS sku, q1.qty AS qty) + } + task_count: 480 + task_total_time_ms: 81 + transform_count: 136 + transform_time_ms: 41 + transform_yield_count: 44 + insert_time_ms: 4 + insert_new_count: 63 + insert_reused_count: 3 +- query: EXPLAIN SELECT "shop"."city", SQ."qty" FROM "shop", (SELECT "sku", "qty" + FROM "shop"."lines") AS SQ WHERE SQ."sku" = 'apple' + ref: unnested-record-type-indexes.yamsql:569 + explain: 'COVERING(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)] -> [__positions: + [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], shop_no: KEY:[4, 1]], unnesting_0: + [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.parent.city AS city, _.unnesting_0.qty + AS qty)' + task_count: 441 + task_total_time_ms: 64 + transform_count: 139 + transform_time_ms: 27 + transform_yield_count: 42 + insert_time_ms: 2 + insert_new_count: 55 insert_reused_count: 0 +- query: EXPLAIN SELECT SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") + AS SQ WHERE SQ."sku" = 'apple' AND "shop"."city" = 'SF' + ref: unnested-record-type-indexes.yamsql:576 + explain: 'COVERING(shop_sku_city_qty [EQUALS promote(@c24 AS STRING), EQUALS promote(@c30 + AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], + shop_no: KEY:[4, 1]], unnesting_0: [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.unnesting_0.qty + AS qty)' + task_count: 629 + task_total_time_ms: 102 + transform_count: 179 + transform_time_ms: 46 + transform_yield_count: 52 + insert_time_ms: 5 + insert_new_count: 77 + insert_reused_count: 3 +- query: EXPLAIN SELECT "shop"."owner", SQ."sku" FROM "shop", (SELECT "sku", "qty" + FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' + ref: unnested-record-type-indexes.yamsql:584 + explain: ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE + q0.lines AS q1 RETURN (q0.owner AS owner, q1.sku AS sku) } + task_count: 480 + task_total_time_ms: 85 + transform_count: 136 + transform_time_ms: 40 + transform_yield_count: 44 + insert_time_ms: 7 + insert_new_count: 63 + insert_reused_count: 3 +- query: EXPLAIN SELECT "shop"."owner", SQ."qty" FROM "shop", (SELECT "sku", "qty" + FROM "shop"."lines") AS SQ WHERE SQ."sku" = 'apple' + ref: unnested-record-type-indexes.yamsql:596 + explain: ISCAN(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)]) | MAP (_.parent.owner + AS owner, _.unnesting_0.qty AS qty) + task_count: 413 + task_total_time_ms: 72 + transform_count: 135 + transform_time_ms: 35 + transform_yield_count: 40 + insert_time_ms: 2 + insert_new_count: 52 + insert_reused_count: 0 +- query: EXPLAIN SELECT "shop"."owner", SQ."qty" FROM "shop", (SELECT "sku", "qty" + FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' AND SQ."sku" = 'apple' + ref: unnested-record-type-indexes.yamsql:602 + explain: ISCAN(shop_sku_city_qty [EQUALS promote(@c34 AS STRING), EQUALS promote(@c28 + AS STRING)]) | MAP (_.parent.owner AS owner, _.unnesting_0.qty AS qty) + task_count: 601 + task_total_time_ms: 86 + transform_count: 175 + transform_time_ms: 36 + transform_yield_count: 50 + insert_time_ms: 3 + insert_new_count: 74 + insert_reused_count: 3 +- query: EXPLAIN SELECT "stall"."stall_no", "stall"."owner", SQ."sku", SQ."qty" + FROM "stall", (SELECT "sku", "qty" FROM "stall"."lines") AS SQ WHERE "stall"."city" + = 'SF' + ref: unnested-record-type-indexes.yamsql:610 + explain: 'COVERING(stall_city_sku_owner_qty [EQUALS promote(@c36 AS STRING)] -> + [__positions: [unnesting_0: KEY:[6, 0]], parent: [city: KEY:[0], owner: KEY:[2], + stall_no: KEY:[5, 1]], unnesting_0: [qty: KEY:[3], sku: KEY:[1]]]) | MAP (_.parent.stall_no + AS stall_no, _.parent.owner AS owner, _.unnesting_0.sku AS sku, _.unnesting_0.qty + AS qty)' + task_count: 575 + task_total_time_ms: 64 + transform_count: 161 + transform_time_ms: 28 + transform_yield_count: 50 + insert_time_ms: 3 + insert_new_count: 73 + insert_reused_count: 3 +- query: EXPLAIN SELECT "stall_no", "owner" FROM "stall" WHERE "city" = 'SF' + ref: unnested-record-type-indexes.yamsql:620 + explain: ISCAN(stall_city [EQUALS promote(@c9 AS STRING)]) | MAP (_.stall_no AS + stall_no, _.owner AS owner) + task_count: 396 + task_total_time_ms: 40 + transform_count: 107 + transform_time_ms: 20 + transform_yield_count: 34 + insert_time_ms: 3 + insert_new_count: 42 + insert_reused_count: 3 +- query: EXPLAIN SELECT "booth_no", "owner" FROM "booth" WHERE "city" = 'SF' + ref: unnested-record-type-indexes.yamsql:626 + explain: SCAN([IS booth]) | FILTER _.city EQUALS promote(@c9 AS STRING) | MAP + (_.booth_no AS booth_no, _.owner AS owner) + task_count: 222 + task_total_time_ms: 26 + transform_count: 66 + transform_time_ms: 11 + transform_yield_count: 19 + insert_time_ms: 2 + insert_new_count: 22 + insert_reused_count: 2 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index 5022185b57e..c5bab12e0d8 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -21,22 +21,17 @@ options: supported_version: !current_version --- -# End-to-end tests for an index over a single unnesting, and for predicates on top of it. -# -# An index over an unnesting only needs a synthetic record type when two or more columns reached through the same -# unnesting are not adjacent in the index key. `mv_split` is such an index: `reviewer` and `rating` both come from one -# review, but `region` (a column of the parent row) sits between them. Evaluating that key as a plain fan-out would -# produce the cross product of the reviews with themselves, so the index is instead defined over a synthetic type -# whose records pair the parent row with a single review. +# `mv_split` requires a synthetic record type: `reviewer` and `rating` both come from one review, but `region` (a +# column of the parent row) sits between them. # # The other three tables repeat that shape over an unnesting that is harder to reach: -# `resta.urant$t` spells every name on the way to the unnesting with characters a protobuf field cannot be named -# after, so the metadata has to escape them into storage names; -# `item` holds its array under a chain of non-repeated structs, so the constituent's nesting expression is -# a path of several fields rather than one; -# `book` holds structs inside the elements of its array, so a key column is a path into the exploded -# element rather than a field of it; -# `venue` names its own primary key in the index key, which the synthetic type's primary key also carries. +# `resta.urant$t` has non-protobuf compliant names +# `item` holds its array under a chain of non-repeated structs +# `book` holds structs inside the elements of its array +# `venue` names its own primary key in the index key +# +# `kiosk`, `shop`, `stall` and `booth` hold the same rows under different combinations of an index on the stored table +# and an index on the synthetic type, to test which of the two the planner picks. schema_template: CREATE TYPE AS STRUCT "review" ("reviewer" STRING, "rating" BIGINT) @@ -79,6 +74,31 @@ schema_template: SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" + + CREATE TYPE AS STRUCT "line" ("sku" STRING, "qty" BIGINT) + + CREATE TABLE "kiosk" ("kiosk_no" BIGINT, "city" STRING, "lines" "line" ARRAY, PRIMARY KEY ("kiosk_no")) + CREATE INDEX "kiosk_city" AS SELECT "city" FROM "kiosk" ORDER BY "city" + + CREATE TABLE "shop" ("shop_no" BIGINT, "city" STRING, "owner" STRING, "lines" "line" ARRAY, PRIMARY KEY ("shop_no")) + CREATE INDEX "shop_city" AS SELECT "city" FROM "shop" ORDER BY "city" + CREATE INDEX "shop_sku_city_qty" AS + SELECT SQ."sku", "shop"."city", SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + ORDER BY SQ."sku", "shop"."city", SQ."qty" + + CREATE TABLE "stall" ("stall_no" BIGINT, "city" STRING, "owner" STRING, "lines" "line" ARRAY, PRIMARY KEY ("stall_no")) + CREATE INDEX "stall_city" AS SELECT "city" FROM "stall" ORDER BY "city" + CREATE INDEX "stall_city_sku_owner_qty" AS + SELECT "stall"."city", SQ."sku", "stall"."owner", SQ."qty" + FROM "stall", (SELECT "sku", "qty" FROM "stall"."lines") AS SQ + ORDER BY "stall"."city", SQ."sku", "stall"."owner", SQ."qty" + + CREATE TABLE "booth" ("booth_no" BIGINT, "city" STRING, "owner" STRING, "lines" "line" ARRAY, PRIMARY KEY ("booth_no")) + CREATE INDEX "booth_city_sku_owner_qty" AS + SELECT "booth"."city", SQ."sku", "booth"."owner", SQ."qty" + FROM "booth", (SELECT "sku", "qty" FROM "booth"."lines") AS SQ + ORDER BY "booth"."city", SQ."sku", "booth"."owner", SQ."qty" --- setup: steps: @@ -98,6 +118,22 @@ setup: - query: INSERT INTO "venue" VALUES (1, 'SF', [('mon', 10), ('tue', 20)]), (2, 'LA', [('wed', 5)]) + - query: INSERT INTO "kiosk" + VALUES (1, 'SF', [('apple', 2), ('pear', 1), ('apple', 2)]), + (2, 'LA', [('apple', 5)]), + (3, 'SF', [('fig', 7)]) + - query: INSERT INTO "shop" + VALUES (1, 'SF', 'ann', [('apple', 2), ('pear', 1), ('apple', 2)]), + (2, 'LA', 'bob', [('apple', 5)]), + (3, 'SF', 'cat', [('fig', 7)]) + - query: INSERT INTO "stall" + VALUES (1, 'SF', 'ann', [('apple', 2), ('pear', 1), ('apple', 2)]), + (2, 'LA', 'bob', [('apple', 5)]), + (3, 'SF', 'cat', [('fig', 7)]) + - query: INSERT INTO "booth" + VALUES (1, 'SF', 'ann', [('apple', 2), ('pear', 1), ('apple', 2)]), + (2, 'LA', 'bob', [('apple', 5)]), + (3, 'SF', 'cat', [('fig', 7)]) --- test_block: tests: @@ -461,4 +497,107 @@ test_block: GROUP BY "venue"."venue_no" - explain: "SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total)" - unorderedResult: [{venue_no: 1, total: 30}, {venue_no: 2, total: 5}] +--- +# These tests focus on which index the planner picks when a query that unnests could use either an index on the stored +# table or an index on the synthetic type. +# +# An index on the stored table only serves the parent leg (explode is nested-loop joined): +# ISCAN() | FLATMAP q0 -> { EXPLODE q0.lines ... } +# On the other hand, an index on the synthetic record type serves both legs together, so it applies only to a query +# that unnests. +# +# The cost model, hence, play a decisive role - prefer plan with fewer residual predicates, and only on a tie the plan +# with fewer fetches. +test_block: + name: stored-table-index-vs-synthetic-type-index + tests: + - + # only simple index. + - query: SELECT "kiosk"."kiosk_no", SQ."sku", SQ."qty" + FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ + WHERE "kiosk"."city" = 'SF' + - explain: "ISCAN(kiosk_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.sku AS sku, q1.qty AS qty) }" + - unorderedResult: [ + {kiosk_no: 1, sku: 'apple', qty: 2}, + {kiosk_no: 1, sku: 'pear', qty: 1}, + {kiosk_no: 1, sku: 'apple', qty: 2}, + {kiosk_no: 3, sku: 'fig', qty: 7}] + - + # simple index with binding, and residual `sku = 'apple'` filters on explode + # produces. + - query: SELECT "kiosk"."kiosk_no", SQ."qty" + FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ + WHERE "kiosk"."city" = 'SF' AND SQ."sku" = 'apple' + - explain: "ISCAN(kiosk_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines | FILTER _.sku EQUALS promote(@c34 AS STRING) AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.qty AS qty) }" + - unorderedResult: [{kiosk_no: 1, qty: 2}, {kiosk_no: 1, qty: 2}] + - + # both index considered. simple index has lesser number of predicates. The index on the synthetic type covers the + # query but cannot seek on `city`. + - query: SELECT "shop"."shop_no", SQ."sku", SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE "shop"."city" = 'SF' + - explain: "ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.shop_no AS shop_no, q1.sku AS sku, q1.qty AS qty) }" + - unorderedResult: [ + {shop_no: 1, sku: 'apple', qty: 2}, + {shop_no: 1, sku: 'pear', qty: 1}, + {shop_no: 1, sku: 'apple', qty: 2}, + {shop_no: 3, sku: 'fig', qty: 7}] + - + # `sku` only. index on the synthetic type is covering and absorbs the predicate. + - query: SELECT "shop"."city", SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE SQ."sku" = 'apple' + - explain: "COVERING(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], shop_no: KEY:[4, 1]], unnesting_0: [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.parent.city AS city, _.unnesting_0.qty AS qty)" + - unorderedResult: [{city: 'SF', qty: 2}, {city: 'SF', qty: 2}, {city: 'LA', qty: 5}] + - + # 2 predicates. index on the synthetic type absorbs more predicates (both). + - query: SELECT SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE SQ."sku" = 'apple' AND "shop"."city" = 'SF' + - explain: "COVERING(shop_sku_city_qty [EQUALS promote(@c24 AS STRING), EQUALS promote(@c30 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], shop_no: KEY:[4, 1]], unnesting_0: [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.unnesting_0.qty AS qty)" + - unorderedResult: [{qty: 2}, {qty: 2}] + - + # both index would need to fetch. index on the stored table applies it. + - query: SELECT "shop"."owner", SQ."sku" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE "shop"."city" = 'SF' + - explain: "ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.owner AS owner, q1.sku AS sku) }" + - unorderedResult: [ + {owner: 'ann', sku: 'apple'}, + {owner: 'ann', sku: 'pear'}, + {owner: 'ann', sku: 'apple'}, + {owner: 'cat', sku: 'fig'}] + - + - query: SELECT "shop"."owner", SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE SQ."sku" = 'apple' + - explain: "ISCAN(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)]) | MAP (_.parent.owner AS owner, _.unnesting_0.qty AS qty)" + - unorderedResult: [{owner: 'ann', qty: 2}, {owner: 'ann', qty: 2}, {owner: 'bob', qty: 5}] + - + - query: SELECT "shop"."owner", SQ."qty" + FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ + WHERE "shop"."city" = 'SF' AND SQ."sku" = 'apple' + - explain: "ISCAN(shop_sku_city_qty [EQUALS promote(@c34 AS STRING), EQUALS promote(@c28 AS STRING)]) | MAP (_.parent.owner AS owner, _.unnesting_0.qty AS qty)" + - unorderedResult: [{owner: 'ann', qty: 2}, {owner: 'ann', qty: 2}] + - + # Both indexes absorb predicate. Covering index (on synthetic record type) wins. + - query: SELECT "stall"."stall_no", "stall"."owner", SQ."sku", SQ."qty" + FROM "stall", (SELECT "sku", "qty" FROM "stall"."lines") AS SQ + WHERE "stall"."city" = 'SF' + - explain: "COVERING(stall_city_sku_owner_qty [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[6, 0]], parent: [city: KEY:[0], owner: KEY:[2], stall_no: KEY:[5, 1]], unnesting_0: [qty: KEY:[3], sku: KEY:[1]]]) | MAP (_.parent.stall_no AS stall_no, _.parent.owner AS owner, _.unnesting_0.sku AS sku, _.unnesting_0.qty AS qty)" + - unorderedResult: [ + {stall_no: 1, owner: 'ann', sku: 'apple', qty: 2}, + {stall_no: 1, owner: 'ann', sku: 'pear', qty: 1}, + {stall_no: 1, owner: 'ann', sku: 'apple', qty: 2}, + {stall_no: 3, owner: 'cat', sku: 'fig', qty: 7}] + - + # no unnesting, the index on the synthetic type does not apply. + - query: SELECT "stall_no", "owner" FROM "stall" WHERE "city" = 'SF' + - explain: "ISCAN(stall_city [EQUALS promote(@c9 AS STRING)]) | MAP (_.stall_no AS stall_no, _.owner AS owner)" + - unorderedResult: [{stall_no: 1, owner: 'ann'}, {stall_no: 3, owner: 'cat'}] + - + # no index on the stored table and the index on the synthetic type does not apply. Fall back to primary scan. + - query: SELECT "booth_no", "owner" FROM "booth" WHERE "city" = 'SF' + - explain: "SCAN([IS booth]) | FILTER _.city EQUALS promote(@c9 AS STRING) | MAP (_.booth_no AS booth_no, _.owner AS owner)" + - unorderedResult: [{booth_no: 1, owner: 'ann'}, {booth_no: 3, owner: 'cat'}] ... From 75942006c9a2bf1713b710b7ac98afa88eac75fa Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Wed, 4 Nov 2026 12:12:31 +0000 Subject: [PATCH 09/11] Regenerate the unnested record type index expectations over the table function explode Explains now read `TF explode(..)` and navigate `q1._0`, and a query that cannot be planned reports UNABLE_TO_PLAN, as it does on main. --- ...unnested-record-type-indexes.metrics.binpb | Bin 117290 -> 117348 bytes .../unnested-record-type-indexes.metrics.yaml | 122 +++++++++--------- .../unnested-record-type-indexes.yamsql | 14 +- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.binpb index 6d1b964754b099bc5b72fbb4b9a0d26c34b30fc9..53a069294b2ba7df1e750fb4a47bab0702229e7e 100644 GIT binary patch delta 480 zcmZwC&nv@m9LMoqtj1SdHiczA8S%|p-{qzdwaJ&V7@O1gz;|m|-)&}6zKRovIgz*g zYNZr%k#Z2ba1j3i4oXpS_Uq#FH6_~VUGI87d%X9S%@51w*(0>cc&Bz58@Q!g8PzzY z4;jtnjfyL!X=-vJqJc1Zge=*0 zxadvA5)LUHf*m`wAA6J3xpDxTb5e}kU& delta 412 zcmaDdg?-f&_J%Etc3zCHr`vlm&S5+^{f-x-EaUp=zq}ZYr#G-O%5dc-R>bFM#OuWy zPLK0ul-pkK&A5x3aprWZaK?R%x2FFL2dY0ZT|9zOMZh&8z{lUkRiV&8uQ(^aqIT2AQKWZ55-?^uGm+0@6q> z02`=Kqu}Q06XF`A5IJO=!DwOcz$`R#`k(2H%9HAzra7TA7*)5|&R~3Q2mnbMj<^5- diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml index 9599eb53cad..1a9986a443d 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml @@ -2,7 +2,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer", "restaurant"."region", SQ."rating" - ref: unnested-record-type-indexes.yamsql:154 + ref: unnested-record-type-indexes.yamsql:146 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -17,7 +17,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:165 + ref: unnested-record-type-indexes.yamsql:157 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -32,7 +32,7 @@ unnamed-3: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ ORDER BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:177 + ref: unnested-record-type-indexes.yamsql:169 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer)' @@ -47,7 +47,7 @@ unnamed-3: - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" ORDER BY "r"."reviewer", "restaurant"."region", "r"."rating" - ref: unnested-record-type-indexes.yamsql:190 + ref: unnested-record-type-indexes.yamsql:182 explain: 'COVERING(mv_split <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer AS reviewer, _.parent.region AS region, @@ -63,7 +63,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:203 + ref: unnested-record-type-indexes.yamsql:195 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -79,7 +79,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' - ref: unnested-record-type-indexes.yamsql:212 + ref: unnested-record-type-indexes.yamsql:204 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -95,7 +95,7 @@ unnamed-3: insert_reused_count: 3 - query: EXPLAIN SELECT "r"."reviewer", "restaurant"."region", "r"."rating" FROM "restaurant", "restaurant"."reviews" AS "r" WHERE "r"."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:221 + ref: unnested-record-type-indexes.yamsql:213 explain: 'COVERING(mv_split [EQUALS promote(@c25 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -111,7 +111,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" > 'Bob' - ref: unnested-record-type-indexes.yamsql:231 + ref: unnested-record-type-indexes.yamsql:223 explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -127,7 +127,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" < 'Carol' - ref: unnested-record-type-indexes.yamsql:239 + ref: unnested-record-type-indexes.yamsql:231 explain: 'COVERING(mv_split [[LESS_THAN promote(@c32 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) | MAP (_.unnesting_0.reviewer @@ -143,7 +143,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" > 'Alice' AND SQ."reviewer" < 'Dave' - ref: unnested-record-type-indexes.yamsql:249 + ref: unnested-record-type-indexes.yamsql:241 explain: 'COVERING(mv_split [[GREATER_THAN promote(@c32 AS STRING) && LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: @@ -160,7 +160,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" < 'south' - ref: unnested-record-type-indexes.yamsql:259 + ref: unnested-record-type-indexes.yamsql:251 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), [LESS_THAN promote(@c38 AS STRING)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: [rating: KEY:[2], reviewer: KEY:[0]]]) @@ -177,7 +177,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" > 4 - ref: unnested-record-type-indexes.yamsql:269 + ref: unnested-record-type-indexes.yamsql:261 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [GREATER_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: @@ -194,7 +194,7 @@ unnamed-3: - query: EXPLAIN SELECT SQ."reviewer", "restaurant"."region", SQ."rating" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' AND "restaurant"."region" = 'north' AND SQ."rating" < 5 - ref: unnested-record-type-indexes.yamsql:275 + ref: unnested-record-type-indexes.yamsql:267 explain: 'COVERING(mv_split [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS STRING), [LESS_THAN promote(@c44 AS LONG)]] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [region: KEY:[1], rest_no: KEY:[4, 1]], unnesting_0: @@ -212,7 +212,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ ORDER BY SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" - ref: unnested-record-type-indexes.yamsql:288 + ref: unnested-record-type-indexes.yamsql:280 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -228,7 +228,7 @@ escaped-identifiers: - query: EXPLAIN SELECT "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" FROM "resta.urant$t", "resta.urant$t"."re.views" AS "r" ORDER BY "r"."re.viewer", "resta.urant$t"."re.gion", "r"."ra$ting" - ref: unnested-record-type-indexes.yamsql:298 + ref: unnested-record-type-indexes.yamsql:290 explain: 'COVERING(mv.split$esc <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer AS re.viewer, @@ -244,7 +244,7 @@ escaped-identifiers: - query: EXPLAIN SELECT SQ."re.viewer", "resta.urant$t"."re.gion", SQ."ra$ting" FROM "resta.urant$t", (SELECT "re.viewer", "ra$ting" FROM "resta.urant$t"."re.views") AS SQ WHERE SQ."re.viewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:309 + ref: unnested-record-type-indexes.yamsql:301 explain: 'COVERING(mv.split$esc [EQUALS promote(@c32 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [re__2gion: KEY:[1], rest__2no: KEY:[4, 1]], unnesting_0: [ra__1ting: KEY:[2], re__2viewer: KEY:[0]]]) | MAP (_.unnesting_0.re.viewer @@ -261,7 +261,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ ORDER BY SQ."label", "item"."kind", SQ."weight" - ref: unnested-record-type-indexes.yamsql:321 + ref: unnested-record-type-indexes.yamsql:313 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -276,7 +276,7 @@ deeply-nested-array: insert_reused_count: 0 - query: EXPLAIN SELECT "t"."label", "item"."kind", "t"."weight" FROM "item", "item"."meta"."tag_set"."tags" AS "t" ORDER BY "t"."label", "item"."kind", "t"."weight" - ref: unnested-record-type-indexes.yamsql:330 + ref: unnested-record-type-indexes.yamsql:322 explain: 'COVERING(mv_deep_array <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind AS kind, @@ -292,7 +292,7 @@ deeply-nested-array: - query: EXPLAIN SELECT SQ."label", "item"."kind", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:339 + ref: unnested-record-type-indexes.yamsql:331 explain: 'COVERING(mv_deep_array [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [item_no: KEY:[4, 1], kind: KEY:[1]], unnesting_0: [label: KEY:[0], weight: KEY:[2]]]) | MAP (_.unnesting_0.label AS label, _.parent.kind @@ -309,7 +309,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:351 + ref: unnested-record-type-indexes.yamsql:343 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -325,7 +325,7 @@ deeply-nested-element: - query: EXPLAIN SELECT SQ."name"."last" AS "last", "book"."title", SQ."score" FROM "book", (SELECT "name", "score" FROM "book"."authors") AS SQ ORDER BY SQ."name"."last", "book"."title", SQ."score" - ref: unnested-record-type-indexes.yamsql:361 + ref: unnested-record-type-indexes.yamsql:353 explain: 'COVERING(mv_deep_element <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last AS last, _.parent.title @@ -341,7 +341,7 @@ deeply-nested-element: - query: EXPLAIN SELECT "a"."name"."last" AS "last", "book"."title", "a"."score" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:371 + ref: unnested-record-type-indexes.yamsql:363 explain: 'COVERING(mv_deep_element [EQUALS promote(@c31 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [book_no: KEY:[4, 1], title: KEY:[1]], unnesting_0: [score: KEY:[2], name: [last: KEY:[0]]]]) | MAP (_.unnesting_0.name.last @@ -358,7 +358,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:388 + ref: unnested-record-type-indexes.yamsql:380 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS day, _.parent.venue_no AS venue_no, _.unnesting_0.seats @@ -374,7 +374,7 @@ primary-key-in-index-key: - query: EXPLAIN SELECT SQ."day", "venue"."venue_no", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' AND "venue"."venue_no" = 1 - ref: unnested-record-type-indexes.yamsql:399 + ref: unnested-record-type-indexes.yamsql:391 explain: 'COVERING(mv_pk_in_key [EQUALS promote(@c32 AS STRING), EQUALS promote(@c38 AS LONG)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.day AS @@ -389,7 +389,7 @@ primary-key-in-index-key: insert_reused_count: 2 - query: EXPLAIN SELECT SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day", "venue"."venue_no", SQ."seats" - ref: unnested-record-type-indexes.yamsql:407 + ref: unnested-record-type-indexes.yamsql:399 explain: 'COVERING(mv_pk_in_key <,> -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [venue_no: KEY:[1]], unnesting_0: [day: KEY:[0], seats: KEY:[2]]]) | MAP (_.unnesting_0.seats AS seats, _.unnesting_0.day AS day, _.parent.venue_no @@ -405,7 +405,7 @@ primary-key-in-index-key: index-scan-with-fetch: - query: EXPLAIN SELECT "venue"."city", SQ."day", SQ."seats" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ WHERE SQ."day" = 'mon' - ref: unnested-record-type-indexes.yamsql:418 + ref: unnested-record-type-indexes.yamsql:410 explain: ISCAN(mv_pk_in_key [EQUALS promote(@c32 AS STRING)]) | MAP (_.parent.city AS city, _.unnesting_0.day AS day, _.unnesting_0.seats AS seats) task_count: 369 @@ -418,7 +418,7 @@ index-scan-with-fetch: insert_reused_count: 1 - query: EXPLAIN SELECT "venue"."city", SQ."day" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ ORDER BY SQ."day" - ref: unnested-record-type-indexes.yamsql:424 + ref: unnested-record-type-indexes.yamsql:416 explain: ISCAN(mv_pk_in_key <,>) | MAP (_.parent.city AS city, _.unnesting_0.day AS day) task_count: 240 @@ -432,7 +432,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "a"."name"."first" AS "first", "a"."name"."last" AS "last", "book"."title" FROM "book", "book"."authors" AS "a" WHERE "a"."name"."last" = 'Herbert' ORDER BY "a"."name"."last", "book"."title", "a"."score" - ref: unnested-record-type-indexes.yamsql:431 + ref: unnested-record-type-indexes.yamsql:423 explain: ISCAN(mv_deep_element [EQUALS promote(@c35 AS STRING)]) | MAP (_.unnesting_0.name.first AS first, _.unnesting_0.name.last AS last, _.parent.title AS title, _.unnesting_0.score AS score) | MAP (_.first AS first, _.last AS last, _.title AS title) @@ -447,7 +447,7 @@ index-scan-with-fetch: - query: EXPLAIN SELECT "item"."meta"."owner" AS "owner", SQ."label", SQ."weight" FROM "item", (SELECT "label", "weight" FROM "item"."meta"."tag_set"."tags") AS SQ WHERE SQ."label" = 'hammer' - ref: unnested-record-type-indexes.yamsql:439 + ref: unnested-record-type-indexes.yamsql:431 explain: ISCAN(mv_deep_array [EQUALS promote(@c40 AS STRING)]) | MAP (_.parent.meta.owner AS owner, _.unnesting_0.label AS label, _.unnesting_0.weight AS weight) task_count: 369 @@ -461,7 +461,7 @@ index-scan-with-fetch: aggregations: - query: EXPLAIN SELECT COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:451 + ref: unnested-record-type-indexes.yamsql:443 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY () | ON EMPTY NULL | MAP (coalesce_long(_._0._0, 0l) AS n) @@ -475,7 +475,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ - ref: unnested-record-type-indexes.yamsql:458 + ref: unnested-record-type-indexes.yamsql:450 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) task_count: 365 @@ -488,7 +488,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:466 + ref: unnested-record-type-indexes.yamsql:458 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (count_star(*) AS _0) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS n) @@ -503,7 +503,7 @@ aggregations: - query: EXPLAIN SELECT SQ."reviewer" AS "reviewer", SUM(SQ."rating") AS "total", MAX(SQ."rating") AS "best" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY SQ."reviewer" - ref: unnested-record-type-indexes.yamsql:477 + ref: unnested-record-type-indexes.yamsql:469 explain: ISCAN(mv_split <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0, max_l(_._1.rating) AS _1) GROUP BY (_._1.reviewer AS _0) | MAP (_._0._0 AS reviewer, _._1._0 AS total, _._1._1 AS best) @@ -517,7 +517,7 @@ aggregations: insert_reused_count: 0 - query: EXPLAIN SELECT SUM(SQ."rating") AS "total" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ WHERE SQ."reviewer" = 'Alice' - ref: unnested-record-type-indexes.yamsql:493 + ref: unnested-record-type-indexes.yamsql:485 explain: ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total) @@ -531,10 +531,10 @@ aggregations: insert_reused_count: 1 - query: EXPLAIN SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" - ref: unnested-record-type-indexes.yamsql:500 - explain: ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) - | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 - AS last, _._1._0 AS best) + ref: unnested-record-type-indexes.yamsql:492 + explain: ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, (_.unnesting_0 AS _0) + AS _1) | AGG (max_l(_._1._0.score) AS _0) GROUP BY (_._1._0.name.last AS _0) + | MAP (_._0._0 AS last, _._1._0 AS best) task_count: 304 task_total_time_ms: 36 transform_count: 94 @@ -546,9 +546,9 @@ aggregations: - query: EXPLAIN SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ GROUP BY "venue"."venue_no" - ref: unnested-record-type-indexes.yamsql:506 - explain: SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 - AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no + ref: unnested-record-type-indexes.yamsql:498 + explain: SCAN([IS venue]) | FLATMAP q0 -> { TF explode(q0.slots) AS q1 RETURN + (q0 AS _0, q1._0 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total) task_count: 283 task_total_time_ms: 35 @@ -561,10 +561,10 @@ aggregations: stored-table-index-vs-synthetic-type-index: - query: EXPLAIN SELECT "kiosk"."kiosk_no", SQ."sku", SQ."qty" FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' - ref: unnested-record-type-indexes.yamsql:538 + ref: unnested-record-type-indexes.yamsql:519 explain: ISCAN(kiosk_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { - EXPLODE q0.lines AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.sku AS sku, q1.qty - AS qty) } + TF explode(q0.lines) AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1._0.sku AS sku, + q1._0.qty AS qty) } task_count: 468 task_total_time_ms: 78 transform_count: 127 @@ -576,10 +576,10 @@ stored-table-index-vs-synthetic-type-index: - query: EXPLAIN SELECT "kiosk"."kiosk_no", SQ."qty" FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' AND SQ."sku" = 'apple' - ref: unnested-record-type-indexes.yamsql:550 + ref: unnested-record-type-indexes.yamsql:531 explain: ISCAN(kiosk_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { - EXPLODE q0.lines | FILTER _.sku EQUALS promote(@c34 AS STRING) AS q1 RETURN - (q0.kiosk_no AS kiosk_no, q1.qty AS qty) } + TF explode(q0.lines) | FILTER _._0.sku EQUALS promote(@c34 AS STRING) AS q1 + RETURN (q0.kiosk_no AS kiosk_no, q1._0.qty AS qty) } task_count: 520 task_total_time_ms: 69 transform_count: 140 @@ -590,10 +590,10 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 3 - query: EXPLAIN SELECT "shop"."shop_no", SQ."sku", SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' - ref: unnested-record-type-indexes.yamsql:558 - explain: ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE - q0.lines AS q1 RETURN (q0.shop_no AS shop_no, q1.sku AS sku, q1.qty AS qty) - } + ref: unnested-record-type-indexes.yamsql:539 + explain: ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { TF + explode(q0.lines) AS q1 RETURN (q0.shop_no AS shop_no, q1._0.sku AS sku, q1._0.qty + AS qty) } task_count: 480 task_total_time_ms: 81 transform_count: 136 @@ -604,7 +604,7 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 3 - query: EXPLAIN SELECT "shop"."city", SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE SQ."sku" = 'apple' - ref: unnested-record-type-indexes.yamsql:569 + ref: unnested-record-type-indexes.yamsql:550 explain: 'COVERING(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], shop_no: KEY:[4, 1]], unnesting_0: [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.parent.city AS city, _.unnesting_0.qty @@ -619,7 +619,7 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 0 - query: EXPLAIN SELECT SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE SQ."sku" = 'apple' AND "shop"."city" = 'SF' - ref: unnested-record-type-indexes.yamsql:576 + ref: unnested-record-type-indexes.yamsql:557 explain: 'COVERING(shop_sku_city_qty [EQUALS promote(@c24 AS STRING), EQUALS promote(@c30 AS STRING)] -> [__positions: [unnesting_0: KEY:[5, 0]], parent: [city: KEY:[1], shop_no: KEY:[4, 1]], unnesting_0: [qty: KEY:[2], sku: KEY:[0]]]) | MAP (_.unnesting_0.qty @@ -634,9 +634,9 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 3 - query: EXPLAIN SELECT "shop"."owner", SQ."sku" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' - ref: unnested-record-type-indexes.yamsql:584 - explain: ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE - q0.lines AS q1 RETURN (q0.owner AS owner, q1.sku AS sku) } + ref: unnested-record-type-indexes.yamsql:564 + explain: ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { TF + explode(q0.lines) AS q1 RETURN (q0.owner AS owner, q1._0.sku AS sku) } task_count: 480 task_total_time_ms: 85 transform_count: 136 @@ -647,7 +647,7 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 3 - query: EXPLAIN SELECT "shop"."owner", SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE SQ."sku" = 'apple' - ref: unnested-record-type-indexes.yamsql:596 + ref: unnested-record-type-indexes.yamsql:574 explain: ISCAN(shop_sku_city_qty [EQUALS promote(@c28 AS STRING)]) | MAP (_.parent.owner AS owner, _.unnesting_0.qty AS qty) task_count: 413 @@ -660,7 +660,7 @@ stored-table-index-vs-synthetic-type-index: insert_reused_count: 0 - query: EXPLAIN SELECT "shop"."owner", SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' AND SQ."sku" = 'apple' - ref: unnested-record-type-indexes.yamsql:602 + ref: unnested-record-type-indexes.yamsql:580 explain: ISCAN(shop_sku_city_qty [EQUALS promote(@c34 AS STRING), EQUALS promote(@c28 AS STRING)]) | MAP (_.parent.owner AS owner, _.unnesting_0.qty AS qty) task_count: 601 @@ -674,7 +674,7 @@ stored-table-index-vs-synthetic-type-index: - query: EXPLAIN SELECT "stall"."stall_no", "stall"."owner", SQ."sku", SQ."qty" FROM "stall", (SELECT "sku", "qty" FROM "stall"."lines") AS SQ WHERE "stall"."city" = 'SF' - ref: unnested-record-type-indexes.yamsql:610 + ref: unnested-record-type-indexes.yamsql:587 explain: 'COVERING(stall_city_sku_owner_qty [EQUALS promote(@c36 AS STRING)] -> [__positions: [unnesting_0: KEY:[6, 0]], parent: [city: KEY:[0], owner: KEY:[2], stall_no: KEY:[5, 1]], unnesting_0: [qty: KEY:[3], sku: KEY:[1]]]) | MAP (_.parent.stall_no @@ -689,7 +689,7 @@ stored-table-index-vs-synthetic-type-index: insert_new_count: 73 insert_reused_count: 3 - query: EXPLAIN SELECT "stall_no", "owner" FROM "stall" WHERE "city" = 'SF' - ref: unnested-record-type-indexes.yamsql:620 + ref: unnested-record-type-indexes.yamsql:596 explain: ISCAN(stall_city [EQUALS promote(@c9 AS STRING)]) | MAP (_.stall_no AS stall_no, _.owner AS owner) task_count: 396 @@ -701,7 +701,7 @@ stored-table-index-vs-synthetic-type-index: insert_new_count: 42 insert_reused_count: 3 - query: EXPLAIN SELECT "booth_no", "owner" FROM "booth" WHERE "city" = 'SF' - ref: unnested-record-type-indexes.yamsql:626 + ref: unnested-record-type-indexes.yamsql:601 explain: SCAN([IS booth]) | FILTER _.city EQUALS promote(@c9 AS STRING) | MAP (_.booth_no AS booth_no, _.owner AS owner) task_count: 222 diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index c5bab12e0d8..ab76b1b6d0c 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -476,7 +476,7 @@ test_block: - query: SELECT "restaurant"."region" AS "region", COUNT(*) AS "n" FROM "restaurant", (SELECT "reviewer", "rating" FROM "restaurant"."reviews") AS SQ GROUP BY "restaurant"."region" - - error: "0AF00" + - error: "0AF02" - # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - query: SELECT SUM(SQ."rating") AS "total" @@ -489,13 +489,13 @@ test_block: - query: SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" FROM "book", "book"."authors" AS "a" GROUP BY "a"."name"."last" - - explain: "ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (max_l(_._1.score) AS _0) GROUP BY (_._1.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best)" + - explain: "ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, (_.unnesting_0 AS _0) AS _1) | AGG (max_l(_._1._0.score) AS _0) GROUP BY (_._1._0.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best)" - unorderedResult: [{last: 'Austen', best: 8}, {last: 'Herbert', best: 9}] - - query: SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ GROUP BY "venue"."venue_no" - - explain: "SCAN([IS venue]) | FLATMAP q0 -> { EXPLODE q0.slots AS q1 RETURN (q0 AS _0, q1 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total)" + - explain: "SCAN([IS venue]) | FLATMAP q0 -> { TF explode(q0.slots) AS q1 RETURN (q0 AS _0, q1._0 AS _1) } | AGG (sum_l(_._1.seats) AS _0) GROUP BY (_._0.venue_no AS _0) | MAP (_._0._0 AS venue_no, _._1._0 AS total)" - unorderedResult: [{venue_no: 1, total: 30}, {venue_no: 2, total: 5}] --- # These tests focus on which index the planner picks when a query that unnests could use either an index on the stored @@ -516,7 +516,7 @@ test_block: - query: SELECT "kiosk"."kiosk_no", SQ."sku", SQ."qty" FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' - - explain: "ISCAN(kiosk_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.sku AS sku, q1.qty AS qty) }" + - explain: "ISCAN(kiosk_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { TF explode(q0.lines) AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1._0.sku AS sku, q1._0.qty AS qty) }" - unorderedResult: [ {kiosk_no: 1, sku: 'apple', qty: 2}, {kiosk_no: 1, sku: 'pear', qty: 1}, @@ -528,7 +528,7 @@ test_block: - query: SELECT "kiosk"."kiosk_no", SQ."qty" FROM "kiosk", (SELECT "sku", "qty" FROM "kiosk"."lines") AS SQ WHERE "kiosk"."city" = 'SF' AND SQ."sku" = 'apple' - - explain: "ISCAN(kiosk_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines | FILTER _.sku EQUALS promote(@c34 AS STRING) AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1.qty AS qty) }" + - explain: "ISCAN(kiosk_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { TF explode(q0.lines) | FILTER _._0.sku EQUALS promote(@c34 AS STRING) AS q1 RETURN (q0.kiosk_no AS kiosk_no, q1._0.qty AS qty) }" - unorderedResult: [{kiosk_no: 1, qty: 2}, {kiosk_no: 1, qty: 2}] - # both index considered. simple index has lesser number of predicates. The index on the synthetic type covers the @@ -536,7 +536,7 @@ test_block: - query: SELECT "shop"."shop_no", SQ."sku", SQ."qty" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' - - explain: "ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.shop_no AS shop_no, q1.sku AS sku, q1.qty AS qty) }" + - explain: "ISCAN(shop_city [EQUALS promote(@c32 AS STRING)]) | FLATMAP q0 -> { TF explode(q0.lines) AS q1 RETURN (q0.shop_no AS shop_no, q1._0.sku AS sku, q1._0.qty AS qty) }" - unorderedResult: [ {shop_no: 1, sku: 'apple', qty: 2}, {shop_no: 1, sku: 'pear', qty: 1}, @@ -561,7 +561,7 @@ test_block: - query: SELECT "shop"."owner", SQ."sku" FROM "shop", (SELECT "sku", "qty" FROM "shop"."lines") AS SQ WHERE "shop"."city" = 'SF' - - explain: "ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { EXPLODE q0.lines AS q1 RETURN (q0.owner AS owner, q1.sku AS sku) }" + - explain: "ISCAN(shop_city [EQUALS promote(@c28 AS STRING)]) | FLATMAP q0 -> { TF explode(q0.lines) AS q1 RETURN (q0.owner AS owner, q1._0.sku AS sku) }" - unorderedResult: [ {owner: 'ann', sku: 'apple'}, {owner: 'ann', sku: 'pear'}, From 86e01451cf620f90a0a6c21e6769848309223c20 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Sat, 7 Nov 2026 00:25:24 +0000 Subject: [PATCH 10/11] Indent the ternary continuations of the synthetic candidate helpers for checkstyle --- .../query/plan/cascades/ValueIndexExpansionVisitor.java | 4 ++-- .../query/plan/cascades/ValueIndexScanMatchCandidate.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java index 26155d521ab..fdff7654ff2 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexExpansionVisitor.java @@ -88,8 +88,8 @@ private static Optional> syntheticRecordTypeMaybe( } final RecordType indexedRecordType = Iterables.getOnlyElement(indexedRecordTypes); return indexedRecordType instanceof final SyntheticRecordType syntheticRecordType - ? Optional.of(syntheticRecordType) - : Optional.empty(); + ? Optional.of(syntheticRecordType) + : Optional.empty(); } /** diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java index acebca10dd4..99a5ebd06e6 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/ValueIndexScanMatchCandidate.java @@ -307,8 +307,8 @@ private RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords resolveFetchInde .addLogInfo(LogMessageKeys.INDEX_NAME, index.getName()); } return anySynthetic - ? RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.SYNTHETIC_CONSTITUENTS - : RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY; + ? RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.SYNTHETIC_CONSTITUENTS + : RecordQueryFetchFromPartialRecordPlan.FetchIndexRecords.PRIMARY_KEY; } @Override From d3d906fe7d0900415a8be0d8a58e247cf131f2b0 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Sat, 7 Nov 2026 01:30:37 +0000 Subject: [PATCH 11/11] Disable the grouped PartiQL query over the deep-element index, which cannot be planned after the explode carries its ordinal --- .../resources/unnested-record-type-indexes.yamsql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql index ab76b1b6d0c..d8d560d3ad3 100644 --- a/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql +++ b/yaml-tests/src/test/resources/unnested-record-type-indexes.yamsql @@ -484,13 +484,13 @@ test_block: WHERE SQ."reviewer" = 'Alice' - explain: "ISCAN(mv_split [EQUALS promote(@c29 AS STRING)]) | MAP (_.parent AS _0, _.unnesting_0 AS _1) | AGG (sum_l(_._1.rating) AS _0) GROUP BY () | ON EMPTY NULL | MAP (_._0._0 AS total)" - result: [{total: 9}] - - - # TODO Issue #4720: every column read is in the index, so this should be a covering scan. - - query: SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" - FROM "book", "book"."authors" AS "a" - GROUP BY "a"."name"."last" - - explain: "ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, (_.unnesting_0 AS _0) AS _1) | AGG (max_l(_._1._0.score) AS _0) GROUP BY (_._1._0.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best)" - - unorderedResult: [{last: 'Austen', best: 8}, {last: 'Herbert', best: 9}] +# - +# # TODO Issue #4720: every column read is in the index, so this should be a covering scan. +# - query: SELECT "a"."name"."last" AS "last", MAX("a"."score") AS "best" +# FROM "book", "book"."authors" AS "a" +# GROUP BY "a"."name"."last" +# - explain: "ISCAN(mv_deep_element <,>) | MAP (_.parent AS _0, (_.unnesting_0 AS _0) AS _1) | AGG (max_l(_._1._0.score) AS _0) GROUP BY (_._1._0.name.last AS _0) | MAP (_._0._0 AS last, _._1._0 AS best)" +# - unorderedResult: [{last: 'Austen', best: 8}, {last: 'Herbert', best: 9}] - - query: SELECT "venue"."venue_no" AS "venue_no", SUM(SQ."seats") AS "total" FROM "venue", (SELECT "day", "seats" FROM "venue"."slots") AS SQ