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..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 @@ -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,15 @@ 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 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<>(); } @Nonnull @@ -471,11 +471,38 @@ public Builder setPackage(@Nonnull final String name) { @Nonnull public Builder addTypeIfNeeded(@Nonnull final Type type) { if (!typeToNameMap.containsKey(type)) { + // 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) { + typeToNameMap.put(type, existingProtoName); + return this; + } + } + + // Standard case: define the protobuf type type.defineProtoType(this); } return this; } + /** + * 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(); + if (differsOnlyInNullability(type, existingType)) { + return existingType; + } + } + return null; + } + @Nonnull public Optional getTypeName(@Nonnull final Type type) { return Optional.ofNullable(typeToNameMap.get(type)); @@ -483,7 +510,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 +527,56 @@ 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) { + 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); + 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 + } + + // 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.notNullable() : type1; + final Type nonNullable2 = type2.isNullable() ? type2.notNullable() : type2; + + return nonNullable1.equals(nonNullable2); + } + @Nonnull public Builder addAllTypes(@Nonnull final Collection types) { types.forEach(this::addTypeIfNeeded); 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..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 @@ -1187,7 +1187,7 @@ void testNamingStructsDifferentTypesThrows() throws Exception { 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. + Assertions.assertTrue(message.contains("cannot register different types with same name")); // we could improve this error message. } } } diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 737ece61b6..807d7aac47 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -226,6 +226,11 @@ void arrays(YamlTest.Runner runner) throws Exception { runner.runYamsql("arrays.yamsql"); } + @TestTemplate + public void structTypeVariants(YamlTest.Runner runner) throws Exception { + runner.runYamsql("struct-type-nullability-variants.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-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.yamsql b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql new file mode 100644 index 0000000000..316e30ea20 --- /dev/null +++ b/yaml-tests/src/test/resources/struct-type-nullability-variants.yamsql @@ -0,0 +1,123 @@ +# +# struct-type-nullability-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) +--- +options: + supported_version: !current_version +--- +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' + - 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 + - + - 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}]}] +...