From d6539b553bc5612f86dc383a1d420522865f9815 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Tue, 3 Nov 2026 12:41:22 +0000 Subject: [PATCH 1/3] Describe the planner type of a RecordType, including a synthetic one --- .../foundationdb/record/RecordMetaData.java | 38 ++++-- .../plan/cascades/IndexExpansionInfo.java | 2 +- .../record/RecordMetaDataPlannerTypeTest.java | 117 ++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/RecordMetaDataPlannerTypeTest.java diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java index 44620b938a4..0609502cd2d 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java @@ -773,10 +773,20 @@ public List getTempFunctions() { } } - @Nonnull public Type.Record getPlannerType(@Nonnull String recordTypeName) { - final RecordType recordType = getRecordType(recordTypeName); + return getPlannerTypeForRecordType(getRecordType(recordTypeName)); + } + + /** + * The type the planner sees for the records of a record type. Unlike {@link #getPlannerType(String)}, this also + * describes a synthetic record type, whose name {@link #getRecordType(String)} does not resolve. + * + * @param recordType the record type to describe + * @return the planner's type for the records of {@code recordType} + */ + @Nonnull + public Type.Record getPlannerTypeForRecordType(@Nonnull RecordType recordType) { Type.Record plannerType = Type.Record.fromDescriptor(recordType.getDescriptor()); if (storeRecordVersions) { plannerType = plannerType.addPseudoFields(); @@ -786,13 +796,27 @@ public Type.Record getPlannerType(@Nonnull String recordTypeName) { @Nonnull public Type.Record getPlannerType(@Nonnull Collection recordTypeNames) { - if (recordTypeNames.size() == 1) { - final String recordTypeName = Iterables.getOnlyElement(recordTypeNames); - return getPlannerType(recordTypeName); + return getPlannerTypeForRecordTypes(recordTypeNames.stream() + .map(this::getRecordType) + .collect(Collectors.toList())); + } + + /** + * The type the planner sees for records of any of several record types: the type of the one record type, or the + * union of the fields of all of them. Unlike {@link #getPlannerType(Collection)}, this also describes synthetic + * record types, whose names {@link #getRecordType(String)} does not resolve. + * + * @param recordTypes the record types to describe + * @return the planner's type for records of any of {@code recordTypes} + */ + @Nonnull + public Type.Record getPlannerTypeForRecordTypes(@Nonnull Collection recordTypes) { + if (recordTypes.size() == 1) { + return getPlannerTypeForRecordType(Iterables.getOnlyElement(recordTypes)); } // todo: should be removed https://github.com/FoundationDB/fdb-record-layer/issues/1884 - LinkedHashMap fieldsByName = recordTypeNames.stream() - .map(this::getPlannerType) + LinkedHashMap fieldsByName = recordTypes.stream() + .map(this::getPlannerTypeForRecordType) .flatMap(type -> type.getFields().stream()) .collect(Collectors.groupingBy(Type.Record.Field::getFieldName, LinkedHashMap::new, diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/IndexExpansionInfo.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/IndexExpansionInfo.java index 86d9b64f543..74c149c69a7 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/IndexExpansionInfo.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/IndexExpansionInfo.java @@ -137,7 +137,7 @@ public static IndexExpansionInfo createInfo(@Nonnull RecordMetaData metaData, .map(RecordType::getName) .collect(ImmutableSet.toImmutableSet()); @Nonnull - final Type.Record baseType = metaData.getPlannerType(indexedRecordTypeNames); + final Type.Record baseType = metaData.getPlannerTypeForRecordTypes(indexedRecordTypes); @Nullable final KeyExpression commonPrimaryKeyForTypes = RecordMetaData.commonPrimaryKey(indexedRecordTypes); diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/RecordMetaDataPlannerTypeTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/RecordMetaDataPlannerTypeTest.java new file mode 100644 index 00000000000..bc18a6c6151 --- /dev/null +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/RecordMetaDataPlannerTypeTest.java @@ -0,0 +1,117 @@ +/* + * RecordMetaDataPlannerTypeTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2026 Apple Inc. and the FoundationDB project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.record; + +import com.apple.foundationdb.record.metadata.JoinedRecordTypeBuilder; +import com.apple.foundationdb.record.metadata.MetaDataException; +import com.apple.foundationdb.record.metadata.SyntheticRecordType; +import com.apple.foundationdb.record.query.plan.cascades.typing.PseudoField; +import com.apple.foundationdb.record.query.plan.cascades.typing.Type; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static com.apple.foundationdb.record.metadata.Key.Expressions.field; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests of the planner types {@link RecordMetaData} describes record types with, by name and by {@link + * com.apple.foundationdb.record.metadata.RecordType}. + */ +class RecordMetaDataPlannerTypeTest { + @Nonnull + private static final String OUTER = "OuterRecord"; + @Nonnull + private static final String OTHER = "OtherRecord"; + @Nonnull + private static final String JOINED = "Joined"; + + @Test + void getPlannerTypeForRecordTypeDescribesASyntheticType() { + final RecordMetaData metaData = metaData(metaDataBuilder -> { + final JoinedRecordTypeBuilder typeBuilder = metaDataBuilder.addJoinedRecordType(JOINED); + typeBuilder.addConstituent("outer", OUTER); + typeBuilder.addConstituent("other", OTHER); + typeBuilder.addJoin("outer", field("other_id"), "other", field("other_id")); + }); + final SyntheticRecordType type = metaData.getSyntheticRecordType(JOINED); + + // A synthetic type's name cannot be resolved against the stored types, so only the type-taking overload can + // describe it. + assertThatThrownBy(() -> metaData.getPlannerType(JOINED)).isInstanceOf(MetaDataException.class); + final Type.Record plannerType = metaData.getPlannerTypeForRecordType(type); + assertThat(plannerType).isEqualTo(Type.Record.fromDescriptor(type.getDescriptor())); + assertThat(fieldNames(plannerType)).containsExactly("outer", "other"); + assertThat(metaData.getPlannerTypeForRecordTypes(List.of(type))).isEqualTo(plannerType); + } + + @Test + void getPlannerTypeForRecordTypeMatchesTheNameTakingOverloads() { + final RecordMetaData metaData = metaData(metaDataBuilder -> metaDataBuilder.setStoreRecordVersions(true)); + + assertThat(metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER))) + .isEqualTo(metaData.getPlannerType(OUTER)); + assertThat(metaData.getPlannerTypeForRecordTypes(List.of(metaData.getRecordType(OUTER)))) + .isEqualTo(metaData.getPlannerType(List.of(OUTER))); + assertThat(metaData.getPlannerTypeForRecordTypes( + List.of(metaData.getRecordType(OUTER), metaData.getRecordType(OTHER)))) + .isEqualTo(metaData.getPlannerType(List.of(OUTER, 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(); + assertThat(metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER)).getFieldNameFieldMap()) + .containsKey(versionField); + assertThat(metaData.getPlannerTypeForRecordTypes( + List.of(metaData.getRecordType(OUTER), metaData.getRecordType(OTHER))).getFieldNameFieldMap()) + .containsKey(versionField); + } + + @Test + void getPlannerTypeForRecordTypesUnionsTheirFields() { + final RecordMetaData metaData = metaData(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(fieldNames(unionType)).containsExactly("rec_id", "other_id", "map", "other_value"); + + // A single type is described exactly as the type-taking overload would describe it on its own. + assertThat(metaData.getPlannerTypeForRecordTypes(List.of(metaData.getRecordType(OUTER)))) + .isEqualTo(metaData.getPlannerTypeForRecordType(metaData.getRecordType(OUTER))); + } + + @Nonnull + private static RecordMetaData metaData(@Nonnull final Consumer hook) { + final RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder() + .setRecords(TestRecordsNestedMapProto.getDescriptor()); + hook.accept(metaDataBuilder); + return metaDataBuilder.build(); + } + + @Nonnull + private static List fieldNames(@Nonnull final Type.Record type) { + return type.getFields().stream().map(Type.Record.Field::getFieldName).collect(Collectors.toList()); + } +} From 7c543ea5844cf06603d3c6444f79a8d58c655c39 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Tue, 3 Nov 2026 13:49:53 +0000 Subject: [PATCH 2/3] trim comments --- .../apple/foundationdb/record/RecordMetaData.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java index 0609502cd2d..9da5a2de0f6 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java @@ -773,14 +773,21 @@ public List getTempFunctions() { } } + /** + * The type the planner sees for the records of the record type with the given name. The name is resolved with + * {@link #getRecordType(String)}, which only resolves stored record types. + * + * @param recordTypeName the name of a stored record type + * @return the planner's type for the records of that record type + * @throws MetaDataException if there is no stored record type with that name + */ @Nonnull public Type.Record getPlannerType(@Nonnull String recordTypeName) { return getPlannerTypeForRecordType(getRecordType(recordTypeName)); } /** - * The type the planner sees for the records of a record type. Unlike {@link #getPlannerType(String)}, this also - * describes a synthetic record type, whose name {@link #getRecordType(String)} does not resolve. + * The type the planner sees for the records of a record type. * * @param recordType the record type to describe * @return the planner's type for the records of {@code recordType} @@ -803,8 +810,7 @@ public Type.Record getPlannerType(@Nonnull Collection recordTypeNames) { /** * The type the planner sees for records of any of several record types: the type of the one record type, or the - * union of the fields of all of them. Unlike {@link #getPlannerType(Collection)}, this also describes synthetic - * record types, whose names {@link #getRecordType(String)} does not resolve. + * union of the fields of all of them. * * @param recordTypes the record types to describe * @return the planner's type for records of any of {@code recordTypes} From 29f906b58243a1e1c36797b73c05b925d5f12da2 Mon Sep 17 00:00:00 2001 From: Pranjal Gupta Date: Tue, 3 Nov 2026 14:45:42 +0000 Subject: [PATCH 3/3] Document getPlannerType(Collection), and resolve a single name without building a collection --- .../apple/foundationdb/record/RecordMetaData.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java index 9da5a2de0f6..c4b777c450e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java @@ -801,11 +801,22 @@ public Type.Record getPlannerTypeForRecordType(@Nonnull RecordType recordType) { return plannerType; } + /** + * The type the planner sees for records of any of the record types with the given names. The names are resolved + * with {@link #getRecordType(String)}, which only resolves stored record types. + * + * @param recordTypeNames the names of stored record types + * @return the planner's type for records of any of those record types + * @throws MetaDataException if there is no stored record type with one of those names + */ @Nonnull public Type.Record getPlannerType(@Nonnull Collection recordTypeNames) { + if (recordTypeNames.size() == 1) { + return getPlannerType(Iterables.getOnlyElement(recordTypeNames)); + } return getPlannerTypeForRecordTypes(recordTypeNames.stream() .map(this::getRecordType) - .collect(Collectors.toList())); + .toList()); } /**