From 2e00001d0bd11800c6b3f9a71f54e70a32672a78 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 25 Sep 2025 16:43:07 -0700 Subject: [PATCH 01/14] save --- .../metadata/RecordLayerSchemaTemplate.java | 274 ++++++++++++++++-- 1 file changed, 255 insertions(+), 19 deletions(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 7d1ee36c6c..3e7378d198 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -49,6 +49,7 @@ import com.google.protobuf.Descriptors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; @@ -366,6 +367,103 @@ public void accept(@Nonnull final Visitor visitor) { visitor.finishVisit(this); } + /** + * Manages nullable and non-nullable variants of a struct type with the same name. + * This allows a struct type to exist in both nullable (for table columns) and + * non-nullable (for array elements) forms within the same schema template. + */ + private static final class TypeVariants { + private DataType.Named nullableVariant; + private DataType.Named nonNullableVariant; + + /** + * Adds or updates a type variant based on its nullability. + * + * @param type The type to add/update + */ + public void addVariant(@Nonnull DataType.Named type) { + // Cast to DataType to access isNullable() method + // All named types (StructType, EnumType, etc.) extend DataType and implement Named + DataType dataType = (DataType) type; + if (dataType.isNullable()) { + this.nullableVariant = type; + } else { + this.nonNullableVariant = type; + } + } + + /** + * Gets the appropriate variant based on required nullability. + * + * @param nullable Whether nullable variant is required + * @return The appropriate variant, or null if not available + */ + @Nullable + public DataType.Named getVariant(boolean nullable) { + return nullable ? nullableVariant : nonNullableVariant; + } + + /** + * Gets the nullable variant if available. + */ + @Nullable + public DataType.Named getNullableVariant() { + return nullableVariant; + } + + /** + * Gets the non-nullable variant if available. + */ + @Nullable + public DataType.Named getNonNullableVariant() { + return nonNullableVariant; + } + + /** + * Checks if both variants exist. + */ + public boolean hasBothVariants() { + return nullableVariant != null && nonNullableVariant != null; + } + + /** + * Checks if any variant exists. + */ + public boolean hasAnyVariant() { + return nullableVariant != null || nonNullableVariant != null; + } + + /** + * Gets the primary variant (nullable if available, otherwise non-nullable). + * This maintains backward compatibility by preferring nullable variants. + */ + @Nonnull + public DataType.Named getPrimaryVariant() { + if (nullableVariant != null) { + return nullableVariant; + } + if (nonNullableVariant != null) { + return nonNullableVariant; + } + throw new RelationalException("No variants available", ErrorCode.INTERNAL_ERROR).toUncheckedWrappedException(); + } + + /** + * Gets all non-null variants. + */ + @Nonnull + public Collection getAllVariants() { + final var variants = new ArrayList(); + if (nullableVariant != null) { + variants.add(nullableVariant); + } + if (nonNullableVariant != null) { + variants.add(nonNullableVariant); + } + return variants; + } + } + public static final class Builder { private static final String TABLE_ALREADY_EXISTS = "table '%s' already exists"; private static final String TYPE_WITH_NAME_ALREADY_EXISTS = "type with name '%s' already exists"; @@ -384,7 +482,7 @@ public static final class Builder { private final Map tables; @Nonnull - private final Map auxiliaryTypes; // for quick lookup + private final Map auxiliaryTypes; // for quick lookup @Nonnull private final Map invokedRoutines; @@ -490,7 +588,8 @@ public Builder addInvokedRoutines(@Nonnull final Collection findType(@Nonnull final String name) { } if (auxiliaryTypes.containsKey(name)) { - return Optional.of((DataType) auxiliaryTypes.get(name)); + // Return the primary variant (nullable if available, otherwise non-nullable) + // This maintains backward compatibility + return Optional.of((DataType) auxiliaryTypes.get(name).getPrimaryVariant()); } return Optional.empty(); } + /** + * Finds a type variant with specific nullability. This method is used to support + * struct types that exist in both nullable and non-nullable forms. + * + * @param name The name of the type to find + * @param nullable Whether to find the nullable or non-nullable variant + * @return Optional containing the specific variant if found + */ + @Nonnull + public Optional findTypeVariant(@Nonnull final String name, boolean nullable) { + if (tables.containsKey(name)) { + DataType tableType = tables.get(name).getDatatype(); + // Only return table type if nullability matches + if (tableType.isNullable() == nullable) { + return Optional.of(tableType); + } + return Optional.empty(); + } + + if (auxiliaryTypes.containsKey(name)) { + TypeVariants variants = auxiliaryTypes.get(name); + DataType.Named variant = variants.getVariant(nullable); + return variant != null ? Optional.of((DataType) variant) : Optional.empty(); + } + + return Optional.empty(); + } + + /** + * Convenience method to add both nullable and non-nullable variants of a struct type. + * This is useful when you know a struct type will be used both as table columns + * (nullable) and as array elements (non-nullable). + * + * @param structType The base struct type (nullability will be ignored, both variants created) + * @return {@code this} {@link Builder}. + */ + @Nonnull + public Builder addStructTypeWithBothVariants(@Nonnull DataType.StructType structType) { + // Create nullable variant + DataType.StructType nullableVariant = (DataType.StructType) structType.withNullable(true); + addAuxiliaryType(nullableVariant); + + // Create non-nullable variant + DataType.StructType nonNullableVariant = (DataType.StructType) structType.withNullable(false); + addAuxiliaryType(nonNullableVariant); + + return this; + } + + /** + * Gets the appropriate struct type variant for use in array contexts (non-nullable). + * + * @param typeName The name of the struct type + * @return Optional containing the non-nullable variant if available + */ + @Nonnull + public Optional getStructTypeForArrayContext(@Nonnull final String typeName) { + return auxiliaryTypes.containsKey(typeName) ? + Optional.ofNullable((DataType.StructType) auxiliaryTypes.get(typeName).getNonNullableVariant()) : + Optional.empty(); + } + + /** + * Gets the appropriate struct type variant for use in table column contexts (nullable). + * + * @param typeName The name of the struct type + * @return Optional containing the nullable variant if available + */ + @Nonnull + public Optional getStructTypeForColumnContext(@Nonnull final String typeName) { + return auxiliaryTypes.containsKey(typeName) ? + Optional.ofNullable((DataType.StructType) auxiliaryTypes.get(typeName).getNullableVariant()) : + Optional.empty(); + } + @Nonnull public RecordLayerSchemaTemplate build() { Assert.thatUnchecked(!tables.isEmpty(), ErrorCode.INVALID_SCHEMA_TEMPLATE, "schema template contains no tables"); @@ -563,9 +753,14 @@ public RecordLayerSchemaTemplate build() { } if (!needsResolution) { - for (final var auxiliaryType : auxiliaryTypes.values()) { - if (!((DataType) auxiliaryType).isResolved()) { - needsResolution = true; + for (final var typeVariants : auxiliaryTypes.values()) { + for (final var variant : typeVariants.getAllVariants()) { + if (!((DataType) variant).isResolved()) { + needsResolution = true; + break; + } + } + if (needsResolution) { break; } } @@ -584,14 +779,39 @@ public RecordLayerSchemaTemplate build() { } } + /** + * Creates both nullable and non-nullable variants for a struct type if they don't exist. + * + * @param typeName The name of the struct type that needs both variants + */ + private void createBothVariantsIfNeeded(@Nonnull final String typeName) { + TypeVariants variants = auxiliaryTypes.get(typeName); + if (variants == null || variants.hasBothVariants()) { + return; // Nothing to do + } + + // We have only one variant but need both - create the missing variant + DataType.Named existingVariant = variants.getPrimaryVariant(); + DataType existingAsDataType = (DataType) existingVariant; + + if (existingVariant instanceof DataType.StructType && existingAsDataType.isResolved()) { + // Create the opposite nullability variant + DataType.Named newVariant = (DataType.Named) existingAsDataType.withNullable(!existingAsDataType.isNullable()); + variants.addVariant(newVariant); + } + } + private void resolveTypes() { // collect all named types from tables + auxiliary types. final var mapBuilder = ImmutableMap.builder(); for (final var table : tables.values()) { mapBuilder.put(table.getName(), table.getDatatype()); } - for (final var auxiliaryType : auxiliaryTypes.entrySet()) { - mapBuilder.put(auxiliaryType.getKey(), (DataType) auxiliaryType.getValue()); + for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { + // For each type name, add the primary variant to the map for backward compatibility + // This ensures existing type resolution logic works + TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); + mapBuilder.put(auxiliaryTypeEntry.getKey(), (DataType) typeVariants.getPrimaryVariant()); } final var namedTypes = mapBuilder.build(); @@ -600,8 +820,12 @@ private void resolveTypes() { for (final var table : tables.values()) { depsBuilder.put(table.getDatatype(), getDependencies(table.getDatatype(), namedTypes)); } - for (final var auxiliaryType : auxiliaryTypes.entrySet()) { - depsBuilder.put((DataType) auxiliaryType.getValue(), getDependencies((DataType) auxiliaryType.getValue(), namedTypes)); + for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { + TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); + // Add dependencies for all variants of this type + for (final var variant : typeVariants.getAllVariants()) { + depsBuilder.put((DataType) variant, getDependencies((DataType) variant, namedTypes)); + } } final var deps = depsBuilder.build(); @@ -640,14 +864,26 @@ private void resolveTypes() { tables.clear(); tables.putAll(resolvedTables.build()); - final var resolvedAuxiliaryTypes = ImmutableMap.builder(); - for (final var auxiliaryType : auxiliaryTypes.entrySet()) { - final var dataType = (DataType) auxiliaryType.getValue(); - if (!dataType.isResolved()) { - resolvedAuxiliaryTypes.put(auxiliaryType.getKey(), (DataType.Named) ((DataType) resolvedTypes.get(auxiliaryType.getKey())).withNullable(dataType.isNullable())); - } else { - resolvedAuxiliaryTypes.put(auxiliaryType.getKey(), auxiliaryType.getValue()); + final var resolvedAuxiliaryTypes = ImmutableMap.builder(); + for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { + final String typeName = auxiliaryTypeEntry.getKey(); + final TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); + + TypeVariants resolvedVariants = new TypeVariants(); + + // Resolve each variant + for (final var variant : typeVariants.getAllVariants()) { + DataType.Named resolvedVariant; + DataType variantAsDataType = (DataType) variant; + if (!variantAsDataType.isResolved()) { + resolvedVariant = (DataType.Named) ((DataType) resolvedTypes.get(typeName)).withNullable(variantAsDataType.isNullable()); + } else { + resolvedVariant = variant; + } + resolvedVariants.addVariant(resolvedVariant); } + + resolvedAuxiliaryTypes.put(typeName, resolvedVariants); } auxiliaryTypes.clear(); From 3082449aed4c01b05cefb5a6aa89d79c2b9f4015 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Mon, 29 Sep 2025 10:38:34 -0700 Subject: [PATCH 02/14] save --- .../metadata/RecordLayerSchemaTemplate.java | 4 ++-- yaml-tests/src/test/java/YamlIntegrationTests.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 3e7378d198..00fa0fabc4 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -703,11 +703,11 @@ public Optional findTypeVariant(@Nonnull final String name, boolean nu @Nonnull public Builder addStructTypeWithBothVariants(@Nonnull DataType.StructType structType) { // Create nullable variant - DataType.StructType nullableVariant = (DataType.StructType) structType.withNullable(true); + DataType.StructType nullableVariant = structType.withNullable(true); addAuxiliaryType(nullableVariant); // Create non-nullable variant - DataType.StructType nonNullableVariant = (DataType.StructType) structType.withNullable(false); + DataType.StructType nonNullableVariant = structType.withNullable(false); addAuxiliaryType(nonNullableVariant); return this; diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 737ece61b6..e14285ca6e 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -226,6 +226,16 @@ void arrays(YamlTest.Runner runner) throws Exception { runner.runYamsql("arrays.yamsql"); } + @TestTemplate + public void structTypeVariants(YamlTest.Runner runner) throws Exception { + runner.runYamsql("struct-type-variants.yamsql"); + } + + @TestTemplate + public void structNullableVariantsBasic(YamlTest.Runner runner) throws Exception { + runner.runYamsql("struct-nullable-variants-basic.yamsql"); + } + @TestTemplate public void insertEnum(YamlTest.Runner runner) throws Exception { runner.runYamsql("insert-enum.yamsql"); From cb96a99a6ca9a752f4f57725e9005af4b9b997b7 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Tue, 30 Sep 2025 15:52:28 -0700 Subject: [PATCH 03/14] save --- .../plan/cascades/typing/TypeRepository.java | 86 +++++++++++++++++-- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index c8aa4d2d2e..9b392eda70 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -23,8 +23,6 @@ import com.apple.foundationdb.record.TupleFieldsProto; import com.google.common.base.Preconditions; import com.google.common.base.Verify; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.protobuf.DescriptorProtos; @@ -435,13 +433,17 @@ private void addEnumType(@Nonnull final EnumDescriptor enumType, public static class Builder { private @Nonnull final FileDescriptorProto.Builder fileDescProtoBuilder; private @Nonnull final FileDescriptorSet.Builder fileDescSetBuilder; - private @Nonnull final BiMap typeToNameMap; + private @Nonnull final Map typeToNameMap; + private @Nonnull final Map nameToCanonicalTypeMap; + private @Nonnull final Set typesWithBothNullabilityVariants; private Builder() { fileDescProtoBuilder = FileDescriptorProto.newBuilder(); fileDescProtoBuilder.addAllDependency(DEPENDENCIES.stream().map(FileDescriptor::getFullName).collect(Collectors.toList())); fileDescSetBuilder = FileDescriptorSet.newBuilder(); - typeToNameMap = HashBiMap.create(); + typeToNameMap = new HashMap<>(); + nameToCanonicalTypeMap = new HashMap<>(); + typesWithBothNullabilityVariants = new HashSet<>(); } @Nonnull @@ -471,11 +473,40 @@ public Builder setPackage(@Nonnull final String name) { @Nonnull public Builder addTypeIfNeeded(@Nonnull final Type type) { if (!typeToNameMap.containsKey(type)) { - type.defineProtoType(this); + // Check if we already have a nullability variant of this type defined + final String expectedProtoName = generateProtoTypeName(type); + final Type existingCanonicalType = nameToCanonicalTypeMap.get(expectedProtoName); + + if (existingCanonicalType != null && differsOnlyInNullability(type, existingCanonicalType)) { + // Don't call defineProtoType again, just register the mapping + typesWithBothNullabilityVariants.add(expectedProtoName); + typeToNameMap.put(type, expectedProtoName); + } else { + // Standard case: define the protobuf type + type.defineProtoType(this); + } } return this; } + /** + * Generates the expected protobuf type name for a given type. + * This is used to predict what name a type would get before calling defineProtoType. + */ + private String generateProtoTypeName(@Nonnull final Type type) { + // For most types, we can predict the protobuf name based on structure + // This is a simplified version - the actual logic might be more complex + if (type instanceof Type.Record) { + final Type.Record recordType = (Type.Record) type; + final String recordName = recordType.getName(); + if (recordName != null) { + return recordName.toUpperCase(); + } + } + // Add more type-specific logic as needed + return type.toString().toUpperCase().replaceAll("[^A-Z0-9_]", "_"); + } + @Nonnull public Optional getTypeName(@Nonnull final Type type) { return Optional.ofNullable(typeToNameMap.get(type)); @@ -483,7 +514,7 @@ public Optional getTypeName(@Nonnull final Type type) { @Nonnull public Optional getTypeByName(@Nonnull final String name) { - return Optional.ofNullable(typeToNameMap.inverse().get(name)); + return Optional.ofNullable(nameToCanonicalTypeMap.get(name)); } @Nonnull @@ -500,11 +531,52 @@ public Builder addEnumType(@Nonnull final DescriptorProtos.EnumDescriptorProto e @Nonnull public Builder registerTypeToTypeNameMapping(@Nonnull final Type type, @Nonnull final String protoTypeName) { - Verify.verify(!typeToNameMap.containsKey(type)); + final String existingTypeName = typeToNameMap.get(type); + if (existingTypeName != null) { + // Type already registered, verify same protobuf name + Verify.verify(existingTypeName.equals(protoTypeName), + "Type %s is already registered with name %s, cannot register with different name %s", + type, existingTypeName, protoTypeName); + return this; + } + + // Check if a type with same structure but different nullability is already registered + final Type existingTypeForName = nameToCanonicalTypeMap.get(protoTypeName); + if (existingTypeForName != null && differsOnlyInNullability(type, existingTypeForName)) { + // Allow both nullable and non-nullable variants to map to the same protobuf type + // Don't update nameToCanonicalTypeMap - keep the first registered type as canonical + typeToNameMap.put(type, protoTypeName); + return this; + } + + // Standard case: new type with new name typeToNameMap.put(type, protoTypeName); + nameToCanonicalTypeMap.put(protoTypeName, type); return this; } + /** + * Checks if two types differ only in their nullability setting. + * This is used to allow both nullable and non-nullable variants of the same structural type + * to map to the same protobuf type name. + */ + private boolean differsOnlyInNullability(@Nonnull final Type type1, @Nonnull final Type type2) { + if (type1.equals(type2)) { + return false; // Same type, doesn't differ + } + + // Check if they have different nullability + if (type1.isNullable() == type2.isNullable()) { + return false; // Same nullability, so they differ in structure + } + + // Create non-nullable versions to compare structure + final Type nonNullable1 = type1.isNullable() ? type1.withNullability(false) : type1; + final Type nonNullable2 = type2.isNullable() ? type2.withNullability(false) : type2; + + return nonNullable1.equals(nonNullable2); + } + @Nonnull public Builder addAllTypes(@Nonnull final Collection types) { types.forEach(this::addTypeIfNeeded); From f94e628c5a6438a7f68c748ad3609326b3023c5a Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Wed, 1 Oct 2025 17:05:12 -0700 Subject: [PATCH 04/14] test work --- .../plan/cascades/typing/TypeRepository.java | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index 9b392eda70..22a4047a95 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -473,38 +473,36 @@ public Builder setPackage(@Nonnull final String name) { @Nonnull public Builder addTypeIfNeeded(@Nonnull final Type type) { if (!typeToNameMap.containsKey(type)) { - // Check if we already have a nullability variant of this type defined - final String expectedProtoName = generateProtoTypeName(type); - final Type existingCanonicalType = nameToCanonicalTypeMap.get(expectedProtoName); - - if (existingCanonicalType != null && differsOnlyInNullability(type, existingCanonicalType)) { - // Don't call defineProtoType again, just register the mapping - typesWithBothNullabilityVariants.add(expectedProtoName); - typeToNameMap.put(type, expectedProtoName); - } else { - // Standard case: define the protobuf type - type.defineProtoType(this); + // Check if we have a structurally identical type with different nullability already registered + Type canonicalType = findCanonicalTypeForStructure(type); + if (canonicalType != null) { + // Use the same protobuf name as the canonical type + String existingProtoName = typeToNameMap.get(canonicalType); + if (existingProtoName != null) { + typesWithBothNullabilityVariants.add(existingProtoName); + typeToNameMap.put(type, existingProtoName); + return this; + } } + + // Standard case: define the protobuf type + type.defineProtoType(this); } return this; } /** - * Generates the expected protobuf type name for a given type. - * This is used to predict what name a type would get before calling defineProtoType. + * Finds a type that has the same structure as the given type but different nullability. + * Returns null if no such type exists. */ - private String generateProtoTypeName(@Nonnull final Type type) { - // For most types, we can predict the protobuf name based on structure - // This is a simplified version - the actual logic might be more complex - if (type instanceof Type.Record) { - final Type.Record recordType = (Type.Record) type; - final String recordName = recordType.getName(); - if (recordName != null) { - return recordName.toUpperCase(); + private Type findCanonicalTypeForStructure(@Nonnull final Type type) { + for (Map.Entry entry : typeToNameMap.entrySet()) { + Type existingType = entry.getKey(); + if (differsOnlyInNullability(type, existingType)) { + return existingType; } } - // Add more type-specific logic as needed - return type.toString().toUpperCase().replaceAll("[^A-Z0-9_]", "_"); + return null; } @Nonnull From 9eaf30085e5f4efb618a6de9c2e046be89aaf6f6 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Wed, 1 Oct 2025 20:59:11 -0700 Subject: [PATCH 05/14] add test --- .../src/test/java/YamlIntegrationTests.java | 5 - .../resources/struct-type-variants.yamsql | 116 ++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 yaml-tests/src/test/resources/struct-type-variants.yamsql diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index e14285ca6e..7fcf6fe50c 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -231,11 +231,6 @@ public void structTypeVariants(YamlTest.Runner runner) throws Exception { runner.runYamsql("struct-type-variants.yamsql"); } - @TestTemplate - public void structNullableVariantsBasic(YamlTest.Runner runner) throws Exception { - runner.runYamsql("struct-nullable-variants-basic.yamsql"); - } - @TestTemplate public void insertEnum(YamlTest.Runner runner) throws Exception { runner.runYamsql("insert-enum.yamsql"); diff --git a/yaml-tests/src/test/resources/struct-type-variants.yamsql b/yaml-tests/src/test/resources/struct-type-variants.yamsql new file mode 100644 index 0000000000..5025e8360c --- /dev/null +++ b/yaml-tests/src/test/resources/struct-type-variants.yamsql @@ -0,0 +1,116 @@ +# +# struct-type-variants.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2024 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Tests for struct types with same name but different nullability +# This validates that a struct type like "Address" can exist in both: +# - Nullable form (for table columns) +# - Non-nullable form (for array elements) + +--- +schema_template: + create type as struct Address(street string, city string, zipcode integer) + + create table users( + id integer, + name string, + home_address Address, + primary key(id) + ) + + create table locations( + id integer, + name string, + addresses Address array, + primary key(id) + ) + + create table businesses( + id integer, + name string, + headquarters Address, + branch_offices Address array, + primary key(id) + ) + +--- +setup: + steps: + # Insert data into users table with nullable addresses + - query: | + INSERT INTO users VALUES + (1, 'John Doe', ('123 Main St', 'Anytown', 12345)), + (2, 'Jane Smith', ('456 Oak Ave', 'Somewhere', 67890)), + (3, 'Bob Johnson', null) # Null address should be allowed + + # Insert data into locations table with address arrays + - query: | + INSERT INTO locations VALUES + (1, 'Shopping Center', [ + ('100 Mall Dr', 'Big City', 11111), + ('200 Plaza Way', 'Big City', 11112) + ]), + (2, 'Office Complex', [ + ('300 Business Blvd', 'Metro', 22222), + ('400 Corporate Ct', 'Metro', 22223), + ('500 Executive Dr', 'Metro', 22224) + ]) + + # Insert data into businesses table (mixed usage) + - query: | + INSERT INTO businesses VALUES + (1, 'Tech Corp', + ('999 Innovation Way', 'Silicon Valley', 94000), + [('101 Branch St', 'New York', 10001), ('202 Office Rd', 'Boston', 02101)] + ), + (2, 'Global Inc', + null, # Headquarters can be null + [('303 Regional Ave', 'Chicago', 60601)] + ) + +--- +test_block: + name: struct-type-variants-tests + tests: + # Test 1: Query nullable Address columns by checking a field + - + - query: SELECT id, name, home_address FROM users WHERE home_address.city = 'Anytown' + - result: [{1, 'John Doe', {street: '123 Main St', city: 'Anytown', zipcode: 12345}}] + + # Test 2: Query nullable Address columns by checking another field + - + - query: SELECT id, name, home_address FROM users WHERE home_address.city = 'Somewhere' + - result: [{2, 'Jane Smith', {street: '456 Oak Ave', city: 'Somewhere', zipcode: 67890}}] + + # Test 3: Query Address arrays (non-nullable elements) + - + - query: SELECT id, name, addresses FROM locations WHERE id = 1 + - result: [{1, 'Shopping Center', [{street: '100 Mall Dr', city: 'Big City', zipcode: 11111}, {street: '200 Plaza Way', city: 'Big City', zipcode: 11112}]}] + + # Test 4: Query locations by name + - + - query: SELECT id, name FROM locations WHERE name = 'Office Complex' + - result: [{2, 'Office Complex'}] + + # Test 5: Mixed usage - query by headquarters city + - + - query: SELECT id, name, headquarters, branch_offices + FROM businesses + WHERE headquarters.city = 'Silicon Valley' + - result: [{1, 'Tech Corp', {street: '999 Innovation Way', city: 'Silicon Valley', zipcode: 94000}, [{street: '101 Branch St', city: 'New York', zipcode: 10001}, {street: '202 Office Rd', city: 'Boston', zipcode: 2101}]}] +... From d64c34089135a03dff97b2fa9b7967a1a6c4b8d7 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 2 Oct 2025 16:28:31 -0700 Subject: [PATCH 06/14] remove unnecessary methods --- .../metadata/RecordLayerSchemaTemplate.java | 144 +----------------- .../resources/struct-type-variants.yamsql | 4 +- 2 files changed, 4 insertions(+), 144 deletions(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 00fa0fabc4..26db109191 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -392,47 +392,6 @@ public void addVariant(@Nonnull DataType.Named type) { } } - /** - * Gets the appropriate variant based on required nullability. - * - * @param nullable Whether nullable variant is required - * @return The appropriate variant, or null if not available - */ - @Nullable - public DataType.Named getVariant(boolean nullable) { - return nullable ? nullableVariant : nonNullableVariant; - } - - /** - * Gets the nullable variant if available. - */ - @Nullable - public DataType.Named getNullableVariant() { - return nullableVariant; - } - - /** - * Gets the non-nullable variant if available. - */ - @Nullable - public DataType.Named getNonNullableVariant() { - return nonNullableVariant; - } - - /** - * Checks if both variants exist. - */ - public boolean hasBothVariants() { - return nullableVariant != null && nonNullableVariant != null; - } - - /** - * Checks if any variant exists. - */ - public boolean hasAnyVariant() { - return nullableVariant != null || nonNullableVariant != null; - } - /** * Gets the primary variant (nullable if available, otherwise non-nullable). * This maintains backward compatibility by preferring nullable variants. @@ -600,11 +559,7 @@ public Builder addAuxiliaryType(@Nonnull DataType.Named auxiliaryType) { // For struct types, allow both nullable and non-nullable variants if (auxiliaryType instanceof DataType.StructType) { - TypeVariants variants = auxiliaryTypes.get(auxiliaryType.getName()); - if (variants == null) { - variants = new TypeVariants(); - auxiliaryTypes.put(auxiliaryType.getName(), variants); - } + TypeVariants variants = auxiliaryTypes.computeIfAbsent(auxiliaryType.getName(), k -> new TypeVariants()); variants.addVariant(auxiliaryType); } else { // For non-struct types (enums, etc.), maintain existing behavior @@ -664,81 +619,6 @@ public Optional findType(@Nonnull final String name) { return Optional.empty(); } - /** - * Finds a type variant with specific nullability. This method is used to support - * struct types that exist in both nullable and non-nullable forms. - * - * @param name The name of the type to find - * @param nullable Whether to find the nullable or non-nullable variant - * @return Optional containing the specific variant if found - */ - @Nonnull - public Optional findTypeVariant(@Nonnull final String name, boolean nullable) { - if (tables.containsKey(name)) { - DataType tableType = tables.get(name).getDatatype(); - // Only return table type if nullability matches - if (tableType.isNullable() == nullable) { - return Optional.of(tableType); - } - return Optional.empty(); - } - - if (auxiliaryTypes.containsKey(name)) { - TypeVariants variants = auxiliaryTypes.get(name); - DataType.Named variant = variants.getVariant(nullable); - return variant != null ? Optional.of((DataType) variant) : Optional.empty(); - } - - return Optional.empty(); - } - - /** - * Convenience method to add both nullable and non-nullable variants of a struct type. - * This is useful when you know a struct type will be used both as table columns - * (nullable) and as array elements (non-nullable). - * - * @param structType The base struct type (nullability will be ignored, both variants created) - * @return {@code this} {@link Builder}. - */ - @Nonnull - public Builder addStructTypeWithBothVariants(@Nonnull DataType.StructType structType) { - // Create nullable variant - DataType.StructType nullableVariant = structType.withNullable(true); - addAuxiliaryType(nullableVariant); - - // Create non-nullable variant - DataType.StructType nonNullableVariant = structType.withNullable(false); - addAuxiliaryType(nonNullableVariant); - - return this; - } - - /** - * Gets the appropriate struct type variant for use in array contexts (non-nullable). - * - * @param typeName The name of the struct type - * @return Optional containing the non-nullable variant if available - */ - @Nonnull - public Optional getStructTypeForArrayContext(@Nonnull final String typeName) { - return auxiliaryTypes.containsKey(typeName) ? - Optional.ofNullable((DataType.StructType) auxiliaryTypes.get(typeName).getNonNullableVariant()) : - Optional.empty(); - } - - /** - * Gets the appropriate struct type variant for use in table column contexts (nullable). - * - * @param typeName The name of the struct type - * @return Optional containing the nullable variant if available - */ - @Nonnull - public Optional getStructTypeForColumnContext(@Nonnull final String typeName) { - return auxiliaryTypes.containsKey(typeName) ? - Optional.ofNullable((DataType.StructType) auxiliaryTypes.get(typeName).getNullableVariant()) : - Optional.empty(); - } - @Nonnull public RecordLayerSchemaTemplate build() { Assert.thatUnchecked(!tables.isEmpty(), ErrorCode.INVALID_SCHEMA_TEMPLATE, "schema template contains no tables"); @@ -779,28 +659,6 @@ public RecordLayerSchemaTemplate build() { } } - /** - * Creates both nullable and non-nullable variants for a struct type if they don't exist. - * - * @param typeName The name of the struct type that needs both variants - */ - private void createBothVariantsIfNeeded(@Nonnull final String typeName) { - TypeVariants variants = auxiliaryTypes.get(typeName); - if (variants == null || variants.hasBothVariants()) { - return; // Nothing to do - } - - // We have only one variant but need both - create the missing variant - DataType.Named existingVariant = variants.getPrimaryVariant(); - DataType existingAsDataType = (DataType) existingVariant; - - if (existingVariant instanceof DataType.StructType && existingAsDataType.isResolved()) { - // Create the opposite nullability variant - DataType.Named newVariant = (DataType.Named) existingAsDataType.withNullable(!existingAsDataType.isNullable()); - variants.addVariant(newVariant); - } - } - private void resolveTypes() { // collect all named types from tables + auxiliary types. final var mapBuilder = ImmutableMap.builder(); diff --git a/yaml-tests/src/test/resources/struct-type-variants.yamsql b/yaml-tests/src/test/resources/struct-type-variants.yamsql index 5025e8360c..9651a72e4a 100644 --- a/yaml-tests/src/test/resources/struct-type-variants.yamsql +++ b/yaml-tests/src/test/resources/struct-type-variants.yamsql @@ -21,7 +21,9 @@ # This validates that a struct type like "Address" can exist in both: # - Nullable form (for table columns) # - Non-nullable form (for array elements) - +--- +options: + supported_version: !current_version --- schema_template: create type as struct Address(street string, city string, zipcode integer) From de29718b94e10a787bfe00a17ae505ea690c5808 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 2 Oct 2025 20:29:46 -0700 Subject: [PATCH 07/14] save --- .../recordlayer/metadata/RecordLayerSchemaTemplate.java | 1 - 1 file changed, 1 deletion(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 26db109191..7179440535 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -49,7 +49,6 @@ import com.google.protobuf.Descriptors; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; From 7804c64fbf068b0d15401d001e4261be43bf829c Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 2 Oct 2025 23:35:50 -0700 Subject: [PATCH 08/14] fix tests and style --- .../query/plan/cascades/typing/TypeRepository.java | 13 ++++++++----- .../recordlayer/query/StandardQueryTests.java | 12 ------------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index 22a4047a95..3ccd929b45 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -532,9 +532,7 @@ public Builder registerTypeToTypeNameMapping(@Nonnull final Type type, @Nonnull final String existingTypeName = typeToNameMap.get(type); if (existingTypeName != null) { // Type already registered, verify same protobuf name - Verify.verify(existingTypeName.equals(protoTypeName), - "Type %s is already registered with name %s, cannot register with different name %s", - type, existingTypeName, protoTypeName); + Verify.verify(existingTypeName.equals(protoTypeName), "Type %s is already registered with name %s, cannot register with different name %s", type, existingTypeName, protoTypeName); return this; } @@ -563,14 +561,19 @@ private boolean differsOnlyInNullability(@Nonnull final Type type1, @Nonnull fin return false; // Same type, doesn't differ } + // Handle Type.Null specially - it can only be nullable, so it can't have a non-nullable variant + if (type1 instanceof Type.Null || type2 instanceof Type.Null) { + return false; // Type.Null can't have non-nullable variants + } + // Check if they have different nullability if (type1.isNullable() == type2.isNullable()) { return false; // Same nullability, so they differ in structure } // Create non-nullable versions to compare structure - final Type nonNullable1 = type1.isNullable() ? type1.withNullability(false) : type1; - final Type nonNullable2 = type2.isNullable() ? type2.withNullability(false) : type2; + final Type nonNullable1 = type1.isNullable() ? type1.notNullable() : type1; + final Type nonNullable2 = type2.isNullable() ? type2.notNullable() : type2; return nonNullable1.equals(nonNullable2); } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java index b85953e5ac..decd22c640 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java @@ -1180,18 +1180,6 @@ void testNamingStructsSameType() throws Exception { } } - @Test - void testNamingStructsDifferentTypesThrows() throws Exception { - final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))"; - try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { - try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { - statement.executeUpdate("insert into t1 values (42, 100, 500, 101)"); - final var message = Assertions.assertThrows(SQLException.class, () -> statement.execute("select struct asd (a, 42, struct def (b, c), struct def(b, c, a)) as X from t1")).getMessage(); - Assertions.assertTrue(message.contains("value already present: DEF")); // we could improve this error message. - } - } - } - @Test void testNamingStructsSameTypeDifferentNestingLevels() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))"; From 6af4023d5f41175c3a2ff0a39cbb00661082214c Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 14:19:36 -0700 Subject: [PATCH 09/14] save --- .../plan/cascades/typing/TypeRepository.java | 6 +- .../metadata/RecordLayerSchemaTemplate.java | 131 +++--------------- 2 files changed, 21 insertions(+), 116 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index 3ccd929b45..3e42a9bfe3 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -435,7 +435,6 @@ public static class Builder { private @Nonnull final FileDescriptorSet.Builder fileDescSetBuilder; private @Nonnull final Map typeToNameMap; private @Nonnull final Map nameToCanonicalTypeMap; - private @Nonnull final Set typesWithBothNullabilityVariants; private Builder() { fileDescProtoBuilder = FileDescriptorProto.newBuilder(); @@ -443,7 +442,6 @@ private Builder() { fileDescSetBuilder = FileDescriptorSet.newBuilder(); typeToNameMap = new HashMap<>(); nameToCanonicalTypeMap = new HashMap<>(); - typesWithBothNullabilityVariants = new HashSet<>(); } @Nonnull @@ -479,7 +477,6 @@ public Builder addTypeIfNeeded(@Nonnull final Type type) { // Use the same protobuf name as the canonical type String existingProtoName = typeToNameMap.get(canonicalType); if (existingProtoName != null) { - typesWithBothNullabilityVariants.add(existingProtoName); typeToNameMap.put(type, existingProtoName); return this; } @@ -492,9 +489,10 @@ public Builder addTypeIfNeeded(@Nonnull final Type type) { } /** - * Finds a type that has the same structure as the given type but different nullability. + * Finds a type in typeToNameMap that has the same structure as the given type but different nullability. * Returns null if no such type exists. */ + @Nullable private Type findCanonicalTypeForStructure(@Nonnull final Type type) { for (Map.Entry entry : typeToNameMap.entrySet()) { Type existingType = entry.getKey(); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 7179440535..7d1ee36c6c 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -366,62 +366,6 @@ public void accept(@Nonnull final Visitor visitor) { visitor.finishVisit(this); } - /** - * Manages nullable and non-nullable variants of a struct type with the same name. - * This allows a struct type to exist in both nullable (for table columns) and - * non-nullable (for array elements) forms within the same schema template. - */ - private static final class TypeVariants { - private DataType.Named nullableVariant; - private DataType.Named nonNullableVariant; - - /** - * Adds or updates a type variant based on its nullability. - * - * @param type The type to add/update - */ - public void addVariant(@Nonnull DataType.Named type) { - // Cast to DataType to access isNullable() method - // All named types (StructType, EnumType, etc.) extend DataType and implement Named - DataType dataType = (DataType) type; - if (dataType.isNullable()) { - this.nullableVariant = type; - } else { - this.nonNullableVariant = type; - } - } - - /** - * Gets the primary variant (nullable if available, otherwise non-nullable). - * This maintains backward compatibility by preferring nullable variants. - */ - @Nonnull - public DataType.Named getPrimaryVariant() { - if (nullableVariant != null) { - return nullableVariant; - } - if (nonNullableVariant != null) { - return nonNullableVariant; - } - throw new RelationalException("No variants available", ErrorCode.INTERNAL_ERROR).toUncheckedWrappedException(); - } - - /** - * Gets all non-null variants. - */ - @Nonnull - public Collection getAllVariants() { - final var variants = new ArrayList(); - if (nullableVariant != null) { - variants.add(nullableVariant); - } - if (nonNullableVariant != null) { - variants.add(nonNullableVariant); - } - return variants; - } - } - public static final class Builder { private static final String TABLE_ALREADY_EXISTS = "table '%s' already exists"; private static final String TYPE_WITH_NAME_ALREADY_EXISTS = "type with name '%s' already exists"; @@ -440,7 +384,7 @@ public static final class Builder { private final Map tables; @Nonnull - private final Map auxiliaryTypes; // for quick lookup + private final Map auxiliaryTypes; // for quick lookup @Nonnull private final Map invokedRoutines; @@ -546,8 +490,7 @@ public Builder addInvokedRoutines(@Nonnull final Collection new TypeVariants()); - variants.addVariant(auxiliaryType); - } else { - // For non-struct types (enums, etc.), maintain existing behavior - Assert.thatUnchecked(!auxiliaryTypes.containsKey(auxiliaryType.getName()), ErrorCode.INVALID_SCHEMA_TEMPLATE, TYPE_WITH_NAME_ALREADY_EXISTS, auxiliaryType.getName()); - TypeVariants variants = new TypeVariants(); - variants.addVariant(auxiliaryType); - auxiliaryTypes.put(auxiliaryType.getName(), variants); - } + Assert.thatUnchecked(!auxiliaryTypes.containsKey(auxiliaryType.getName()), ErrorCode.INVALID_SCHEMA_TEMPLATE, TYPE_WITH_NAME_ALREADY_EXISTS, auxiliaryType.getName()); + auxiliaryTypes.put(auxiliaryType.getName(), auxiliaryType); return this; } @@ -610,9 +543,7 @@ public Optional findType(@Nonnull final String name) { } if (auxiliaryTypes.containsKey(name)) { - // Return the primary variant (nullable if available, otherwise non-nullable) - // This maintains backward compatibility - return Optional.of((DataType) auxiliaryTypes.get(name).getPrimaryVariant()); + return Optional.of((DataType) auxiliaryTypes.get(name)); } return Optional.empty(); @@ -632,14 +563,9 @@ public RecordLayerSchemaTemplate build() { } if (!needsResolution) { - for (final var typeVariants : auxiliaryTypes.values()) { - for (final var variant : typeVariants.getAllVariants()) { - if (!((DataType) variant).isResolved()) { - needsResolution = true; - break; - } - } - if (needsResolution) { + for (final var auxiliaryType : auxiliaryTypes.values()) { + if (!((DataType) auxiliaryType).isResolved()) { + needsResolution = true; break; } } @@ -664,11 +590,8 @@ private void resolveTypes() { for (final var table : tables.values()) { mapBuilder.put(table.getName(), table.getDatatype()); } - for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { - // For each type name, add the primary variant to the map for backward compatibility - // This ensures existing type resolution logic works - TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); - mapBuilder.put(auxiliaryTypeEntry.getKey(), (DataType) typeVariants.getPrimaryVariant()); + for (final var auxiliaryType : auxiliaryTypes.entrySet()) { + mapBuilder.put(auxiliaryType.getKey(), (DataType) auxiliaryType.getValue()); } final var namedTypes = mapBuilder.build(); @@ -677,12 +600,8 @@ private void resolveTypes() { for (final var table : tables.values()) { depsBuilder.put(table.getDatatype(), getDependencies(table.getDatatype(), namedTypes)); } - for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { - TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); - // Add dependencies for all variants of this type - for (final var variant : typeVariants.getAllVariants()) { - depsBuilder.put((DataType) variant, getDependencies((DataType) variant, namedTypes)); - } + for (final var auxiliaryType : auxiliaryTypes.entrySet()) { + depsBuilder.put((DataType) auxiliaryType.getValue(), getDependencies((DataType) auxiliaryType.getValue(), namedTypes)); } final var deps = depsBuilder.build(); @@ -721,26 +640,14 @@ private void resolveTypes() { tables.clear(); tables.putAll(resolvedTables.build()); - final var resolvedAuxiliaryTypes = ImmutableMap.builder(); - for (final var auxiliaryTypeEntry : auxiliaryTypes.entrySet()) { - final String typeName = auxiliaryTypeEntry.getKey(); - final TypeVariants typeVariants = auxiliaryTypeEntry.getValue(); - - TypeVariants resolvedVariants = new TypeVariants(); - - // Resolve each variant - for (final var variant : typeVariants.getAllVariants()) { - DataType.Named resolvedVariant; - DataType variantAsDataType = (DataType) variant; - if (!variantAsDataType.isResolved()) { - resolvedVariant = (DataType.Named) ((DataType) resolvedTypes.get(typeName)).withNullable(variantAsDataType.isNullable()); - } else { - resolvedVariant = variant; - } - resolvedVariants.addVariant(resolvedVariant); + final var resolvedAuxiliaryTypes = ImmutableMap.builder(); + for (final var auxiliaryType : auxiliaryTypes.entrySet()) { + final var dataType = (DataType) auxiliaryType.getValue(); + if (!dataType.isResolved()) { + resolvedAuxiliaryTypes.put(auxiliaryType.getKey(), (DataType.Named) ((DataType) resolvedTypes.get(auxiliaryType.getKey())).withNullable(dataType.isNullable())); + } else { + resolvedAuxiliaryTypes.put(auxiliaryType.getKey(), auxiliaryType.getValue()); } - - resolvedAuxiliaryTypes.put(typeName, resolvedVariants); } auxiliaryTypes.clear(); From c7b72430054124ff37ef1949481f701e90303717 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 14:40:51 -0700 Subject: [PATCH 10/14] add yaml files --- .../struct-type-nullability-variants.binpb | 0 .../src/test/java/YamlIntegrationTests.java | 2 +- ...ct-type-nullability-variants.metrics.binpb | 81 +++++++++++++++++++ ...uct-type-nullability-variants.metrics.yaml | 60 ++++++++++++++ .../struct-type-nullability-variants.yaml | 0 ...> struct-type-nullability-variants.yamsql} | 10 ++- 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb create mode 100644 yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.binpb create mode 100644 yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.yaml create mode 100644 yaml-tests/src/test/resources/struct-type-nullability-variants.yaml rename yaml-tests/src/test/resources/{struct-type-variants.yamsql => struct-type-nullability-variants.yamsql} (82%) diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 7fcf6fe50c..807d7aac47 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -228,7 +228,7 @@ void arrays(YamlTest.Runner runner) throws Exception { @TestTemplate public void structTypeVariants(YamlTest.Runner runner) throws Exception { - runner.runYamsql("struct-type-variants.yamsql"); + runner.runYamsql("struct-type-nullability-variants.yamsql"); } @TestTemplate diff --git a/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.binpb b/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.binpb new file mode 100644 index 0000000000..1077f4ad5e --- /dev/null +++ b/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.binpb @@ -0,0 +1,81 @@ + +r +struct-type-variants-testsTEXPLAIN SELECT id, name, home_address FROM users WHERE home_address.city = 'Anytown' +W9 >(08@SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q27.ID AS ID, q27.NAME AS NAME, q27.HOME_ADDRESS AS HOME_ADDRESS)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 2 [ label=<
Predicate Filter
WHERE q2.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 3 [ label=<
Type Filter
WHERE record IS [USERS]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 4 [ label=<
Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 5 [ label=<
Primary Storage
record types: [USERS, LOCATIONS, BUSINESSES]
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 3 -> 2 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q20> label="q20" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q27> label="q27" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +t +struct-type-variants-testsVEXPLAIN SELECT id, name, home_address FROM users WHERE home_address.city = 'Somewhere' +W9 >(08@SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q27.ID AS ID, q27.NAME AS NAME, q27.HOME_ADDRESS AS HOME_ADDRESS)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 2 [ label=<
Predicate Filter
WHERE q2.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 3 [ label=<
Type Filter
WHERE record IS [USERS]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HOME_ADDRESS)" ]; + 4 [ label=<
Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 5 [ label=<
Primary Storage
record types: [USERS, LOCATIONS, BUSINESSES]
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 3 -> 2 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q20> label="q20" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q27> label="q27" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +\ +struct-type-variants-tests>EXPLAIN SELECT id, name, addresses FROM locations WHERE id = 1 +9 麢(0?8@SCAN(<,>) | TFILTER LOCATIONS | FILTER _.ID EQUALS promote(@c12 AS INT) | MAP (_.ID AS ID, _.NAME AS NAME, _.ADDRESSES AS ADDRESSES)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q27.ID AS ID, q27.NAME AS NAME, q27.ADDRESSES AS ADDRESSES)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS ADDRESSES)" ]; + 2 [ label=<
Predicate Filter
WHERE q2.ID EQUALS promote(@c12 AS INT)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS ADDRESSES)" ]; + 3 [ label=<
Type Filter
WHERE record IS [LOCATIONS]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS ADDRESSES)" ]; + 4 [ label=<
Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 5 [ label=<
Primary Storage
record types: [USERS, LOCATIONS, BUSINESSES]
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 3 -> 2 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q20> label="q20" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q27> label="q27" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} +b +struct-type-variants-testsDEXPLAIN SELECT id, name FROM locations WHERE name = 'Office Complex' +ǺTB >(08@oSCAN(<,>) | TFILTER LOCATIONS | FILTER _.NAME EQUALS promote(@c10 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q27.ID AS ID, q27.NAME AS NAME)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME)" ]; + 2 [ label=<
Predicate Filter
WHERE q2.NAME EQUALS promote(@c10 AS STRING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS ADDRESSES)" ]; + 3 [ label=<
Type Filter
WHERE record IS [LOCATIONS]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS ADDRESSES)" ]; + 4 [ label=<
Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 5 [ label=<
Primary Storage
record types: [USERS, LOCATIONS, BUSINESSES]
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 3 -> 2 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q20> label="q20" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q27> label="q27" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} + +struct-type-variants-testspEXPLAIN SELECT id, name, headquarters, branch_offices FROM businesses WHERE headquarters.city = 'Silicon Valley' +ĝW9 >(0Ÿ8@SCAN(<,>) | TFILTER BUSINESSES | FILTER _.HEADQUARTERS.CITY EQUALS promote(@c16 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HEADQUARTERS AS HEADQUARTERS, _.BRANCH_OFFICES AS BRANCH_OFFICES)digraph G { + fontname=courier; + rankdir=BT; + splines=polyline; + 1 [ label=<
Value Computation
MAP (q27.ID AS ID, q27.NAME AS NAME, q27.HEADQUARTERS AS HEADQUARTERS, q27.BRANCH_OFFICES AS BRANCH_OFFICES)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HEADQUARTERS, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS BRANCH_OFFICES)" ]; + 2 [ label=<
Predicate Filter
WHERE q2.HEADQUARTERS.CITY EQUALS promote(@c16 AS STRING)
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HEADQUARTERS, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS BRANCH_OFFICES)" ]; + 3 [ label=<
Type Filter
WHERE record IS [BUSINESSES]
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(INT AS ID, STRING AS NAME, STRING AS STREET, STRING AS CITY, INT AS ZIPCODE AS HEADQUARTERS, ARRAY(STRING AS STREET, STRING AS CITY, INT AS ZIPCODE) AS BRANCH_OFFICES)" ]; + 4 [ label=<
Scan
range: <-∞, ∞>
> color="black" shape="plain" style="solid" fillcolor="black" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 5 [ label=<
Primary Storage
record types: [USERS, LOCATIONS, BUSINESSES]
> color="black" shape="plain" style="filled" fillcolor="lightblue" fontname="courier" fontsize="8" tooltip="RELATION(RECORD)" ]; + 3 -> 2 [ label=< q2> label="q2" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 4 -> 3 [ label=< q20> label="q20" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 5 -> 4 [ color="gray20" style="solid" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; + 2 -> 1 [ label=< q27> label="q27" color="gray20" style="bold" fontname="courier" fontsize="8" arrowhead="normal" arrowtail="none" dir="both" ]; +} \ No newline at end of file diff --git a/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.yaml b/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.yaml new file mode 100644 index 0000000000..873713d3ab --- /dev/null +++ b/yaml-tests/src/test/resources/struct-type-nullability-variants.metrics.yaml @@ -0,0 +1,60 @@ +struct-type-variants-tests: +- query: EXPLAIN SELECT id, name, home_address FROM users WHERE home_address.city + = 'Anytown' + explain: SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 + AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS) + task_count: 234 + task_total_time_ms: 183 + transform_count: 57 + transform_time_ms: 131 + transform_yield_count: 18 + insert_time_ms: 9 + insert_new_count: 24 + insert_reused_count: 1 +- query: EXPLAIN SELECT id, name, home_address FROM users WHERE home_address.city + = 'Somewhere' + explain: SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 + AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS) + task_count: 234 + task_total_time_ms: 183 + transform_count: 57 + transform_time_ms: 131 + transform_yield_count: 18 + insert_time_ms: 9 + insert_new_count: 24 + insert_reused_count: 1 +- query: EXPLAIN SELECT id, name, addresses FROM locations WHERE id = 1 + explain: SCAN(<,>) | TFILTER LOCATIONS | FILTER _.ID EQUALS promote(@c12 AS INT) + | MAP (_.ID AS ID, _.NAME AS NAME, _.ADDRESSES AS ADDRESSES) + task_count: 234 + task_total_time_ms: 16 + transform_count: 57 + transform_time_ms: 6 + transform_yield_count: 18 + insert_time_ms: 1 + insert_new_count: 24 + insert_reused_count: 1 +- query: EXPLAIN SELECT id, name FROM locations WHERE name = 'Office Complex' + explain: SCAN(<,>) | TFILTER LOCATIONS | FILTER _.NAME EQUALS promote(@c10 AS + STRING) | MAP (_.ID AS ID, _.NAME AS NAME) + task_count: 246 + task_total_time_ms: 176 + transform_count: 66 + transform_time_ms: 131 + transform_yield_count: 17 + insert_time_ms: 12 + insert_new_count: 25 + insert_reused_count: 2 +- query: EXPLAIN SELECT id, name, headquarters, branch_offices FROM businesses WHERE + headquarters.city = 'Silicon Valley' + explain: SCAN(<,>) | TFILTER BUSINESSES | FILTER _.HEADQUARTERS.CITY EQUALS promote(@c16 + AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HEADQUARTERS AS HEADQUARTERS, + _.BRANCH_OFFICES AS BRANCH_OFFICES) + task_count: 234 + task_total_time_ms: 182 + transform_count: 57 + transform_time_ms: 131 + transform_yield_count: 18 + insert_time_ms: 9 + insert_new_count: 24 + insert_reused_count: 1 diff --git a/yaml-tests/src/test/resources/struct-type-nullability-variants.yaml b/yaml-tests/src/test/resources/struct-type-nullability-variants.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/yaml-tests/src/test/resources/struct-type-variants.yamsql b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql similarity index 82% rename from yaml-tests/src/test/resources/struct-type-variants.yamsql rename to yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql index 9651a72e4a..e8897d0b5a 100644 --- a/yaml-tests/src/test/resources/struct-type-variants.yamsql +++ b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql @@ -1,5 +1,5 @@ # -# struct-type-variants.yamsql +# struct-type-nullability-variants.yamsql # # This source file is part of the FoundationDB open source project # @@ -22,9 +22,6 @@ # - Nullable form (for table columns) # - Non-nullable form (for array elements) --- -options: - supported_version: !current_version ---- schema_template: create type as struct Address(street string, city string, zipcode integer) @@ -92,21 +89,25 @@ test_block: # Test 1: Query nullable Address columns by checking a field - - query: SELECT id, name, home_address FROM users WHERE home_address.city = 'Anytown' + - explain: "SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS)" - result: [{1, 'John Doe', {street: '123 Main St', city: 'Anytown', zipcode: 12345}}] # Test 2: Query nullable Address columns by checking another field - - query: SELECT id, name, home_address FROM users WHERE home_address.city = 'Somewhere' + - explain: "SCAN(<,>) | TFILTER USERS | FILTER _.HOME_ADDRESS.CITY EQUALS promote(@c14 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HOME_ADDRESS AS HOME_ADDRESS)" - result: [{2, 'Jane Smith', {street: '456 Oak Ave', city: 'Somewhere', zipcode: 67890}}] # Test 3: Query Address arrays (non-nullable elements) - - query: SELECT id, name, addresses FROM locations WHERE id = 1 + - explain: "SCAN(<,>) | TFILTER LOCATIONS | FILTER _.ID EQUALS promote(@c12 AS INT) | MAP (_.ID AS ID, _.NAME AS NAME, _.ADDRESSES AS ADDRESSES)" - result: [{1, 'Shopping Center', [{street: '100 Mall Dr', city: 'Big City', zipcode: 11111}, {street: '200 Plaza Way', city: 'Big City', zipcode: 11112}]}] # Test 4: Query locations by name - - query: SELECT id, name FROM locations WHERE name = 'Office Complex' + - explain: "SCAN(<,>) | TFILTER LOCATIONS | FILTER _.NAME EQUALS promote(@c10 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME)" - result: [{2, 'Office Complex'}] # Test 5: Mixed usage - query by headquarters city @@ -114,5 +115,6 @@ test_block: - query: SELECT id, name, headquarters, branch_offices FROM businesses WHERE headquarters.city = 'Silicon Valley' + - explain: "SCAN(<,>) | TFILTER BUSINESSES | FILTER _.HEADQUARTERS.CITY EQUALS promote(@c16 AS STRING) | MAP (_.ID AS ID, _.NAME AS NAME, _.HEADQUARTERS AS HEADQUARTERS, _.BRANCH_OFFICES AS BRANCH_OFFICES)" - result: [{1, 'Tech Corp', {street: '999 Innovation Way', city: 'Silicon Valley', zipcode: 94000}, [{street: '101 Branch St', city: 'New York', zipcode: 10001}, {street: '202 Office Rd', city: 'Boston', zipcode: 2101}]}] ... From 34a88eb2b8b090fe191aa47cf22df21e2f6c7859 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 14:42:21 -0700 Subject: [PATCH 11/14] remove an empty file --- .../relational/yamltests/struct-type-nullability-variants.binpb | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb diff --git a/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb b/yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/struct-type-nullability-variants.binpb deleted file mode 100644 index e69de29bb2..0000000000 From 0d6861090f5989b0126ad5116476e79ba7eebd10 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 14:43:09 -0700 Subject: [PATCH 12/14] remove --- .../src/test/resources/struct-type-nullability-variants.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 yaml-tests/src/test/resources/struct-type-nullability-variants.yaml diff --git a/yaml-tests/src/test/resources/struct-type-nullability-variants.yaml b/yaml-tests/src/test/resources/struct-type-nullability-variants.yaml deleted file mode 100644 index e69de29bb2..0000000000 From fdd03676315707e3e8ba5423dcadece4de20f5d0 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 14:56:36 -0700 Subject: [PATCH 13/14] fix test --- .../query/plan/cascades/typing/TypeRepository.java | 3 ++- .../recordlayer/query/StandardQueryTests.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index 3e42a9bfe3..aae1e1c23b 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -536,7 +536,8 @@ public Builder registerTypeToTypeNameMapping(@Nonnull final Type type, @Nonnull // Check if a type with same structure but different nullability is already registered final Type existingTypeForName = nameToCanonicalTypeMap.get(protoTypeName); - if (existingTypeForName != null && differsOnlyInNullability(type, existingTypeForName)) { + if (existingTypeForName != null) { + Verify.verify(differsOnlyInNullability(type, existingTypeForName), "Name %s is already registered with a different type, cannot register different types with same name", existingTypeName); // Allow both nullable and non-nullable variants to map to the same protobuf type // Don't update nameToCanonicalTypeMap - keep the first registered type as canonical typeToNameMap.put(type, protoTypeName); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java index decd22c640..527cb81988 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/StandardQueryTests.java @@ -1180,6 +1180,18 @@ void testNamingStructsSameType() throws Exception { } } + @Test + void testNamingStructsDifferentTypesThrows() throws Exception { + final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))"; + try (var ddl = Ddl.builder().database(URI.create("/TEST/QT")).relationalExtension(relationalExtension).schemaTemplate(schemaTemplate).build()) { + try (var statement = ddl.setSchemaAndGetConnection().createStatement()) { + statement.executeUpdate("insert into t1 values (42, 100, 500, 101)"); + final var message = Assertions.assertThrows(SQLException.class, () -> statement.execute("select struct asd (a, 42, struct def (b, c), struct def(b, c, a)) as X from t1")).getMessage(); + Assertions.assertTrue(message.contains("cannot register different types with same name")); // we could improve this error message. + } + } + } + @Test void testNamingStructsSameTypeDifferentNestingLevels() throws Exception { final String schemaTemplate = "CREATE TABLE T1(pk bigint, a bigint, b bigint, c bigint, PRIMARY KEY(pk))"; From 023d6aee6d04eaff6fe0444636d89c3a6c50a005 Mon Sep 17 00:00:00 2001 From: Pengpeng Lu Date: Thu, 16 Oct 2025 15:03:23 -0700 Subject: [PATCH 14/14] fix mixed mode test --- .../src/test/resources/struct-type-nullability-variants.yamsql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql index e8897d0b5a..316e30ea20 100644 --- a/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql +++ b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql @@ -22,6 +22,9 @@ # - Nullable form (for table columns) # - Non-nullable form (for array elements) --- +options: + supported_version: !current_version +--- schema_template: create type as struct Address(street string, city string, zipcode integer)