-
Notifications
You must be signed in to change notification settings - Fork 118
Support nullable and non-nullable types with same name in the same template #3658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2e00001
3082449
cb96a99
f94e628
9eaf300
d64c340
de29718
7804c64
6af4023
c7b7243
34a88eb
0d68610
fdd0367
023d6ae
5505514
4f2d738
759e93c
2de10ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,8 +26,7 @@ | |
| 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.common.collect.ImmutableBiMap; | ||
| import com.google.protobuf.DescriptorProtos; | ||
| import com.google.protobuf.DescriptorProtos.FileDescriptorProto; | ||
| import com.google.protobuf.DescriptorProtos.FileDescriptorSet; | ||
|
|
@@ -50,7 +49,6 @@ | |
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.TreeSet; | ||
|
|
@@ -85,14 +83,25 @@ | |
| @Nonnull | ||
| private final Map<String, EnumDescriptor> enumDescriptorMapShort = new LinkedHashMap<>(); | ||
|
|
||
| /** | ||
| * BiMap storing nullable types and their protobuf names. | ||
| * Only types with isNullable() = true are stored here. | ||
| */ | ||
| @Nonnull | ||
| private final BiMap<Type, String> nullableTypeToNameMap; | ||
|
|
||
| /** | ||
| * BiMap storing non-nullable types and their protobuf names. | ||
| * Only types with isNullable() = false are stored here. | ||
| */ | ||
| @Nonnull | ||
| private final Map<Type, String> typeToNameMap; | ||
| private final BiMap<Type, String> nonNullableTypeToNameMap; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think splitting the maps has made the logic in |
||
|
|
||
| @Nonnull | ||
| public static TypeRepository empty() { | ||
| FileDescriptorSet.Builder resultBuilder = FileDescriptorSet.newBuilder(); | ||
| try { | ||
| return new TypeRepository(resultBuilder.build(), Maps.newHashMap()); | ||
| return new TypeRepository(resultBuilder.build(), HashBiMap.create(), HashBiMap.create()); | ||
| } catch (final DescriptorValidationException e) { | ||
| throw new IllegalStateException(e); | ||
| } | ||
|
|
@@ -123,9 +132,9 @@ | |
| } | ||
|
|
||
| @Nonnull | ||
| public static TypeRepository parseFrom(@Nonnull final byte[] schemaDescBuf) throws DescriptorValidationException, IOException { | ||
| return new TypeRepository(FileDescriptorSet.parseFrom(schemaDescBuf), Maps.newHashMap()); | ||
| return new TypeRepository(FileDescriptorSet.parseFrom(schemaDescBuf), HashBiMap.create(), HashBiMap.create()); | ||
| } | ||
|
Check warning on line 137 in fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java
|
||
|
|
||
| /** | ||
| * Creates a new dynamic message builder for the given message type. | ||
|
|
@@ -150,8 +159,7 @@ | |
| */ | ||
| @Nullable | ||
| public DynamicMessage.Builder newMessageBuilder(@Nonnull final Type type) { | ||
| final String msgTypeName = Preconditions.checkNotNull(typeToNameMap.get(type)); | ||
| Objects.requireNonNull(msgTypeName); | ||
| final String msgTypeName = getProtoTypeName(type); | ||
| return newMessageBuilder(msgTypeName); | ||
| } | ||
|
|
||
|
|
@@ -162,8 +170,13 @@ | |
| */ | ||
| @Nonnull | ||
| public String getProtoTypeName(@Nonnull final Type type) { | ||
| final String typeName = Preconditions.checkNotNull(typeToNameMap.get(type)); | ||
| return Objects.requireNonNull(typeName); | ||
| final String typeName; | ||
| if (type.isNullable()) { | ||
| typeName = nullableTypeToNameMap.get(type); | ||
| } else { | ||
| typeName = nonNullableTypeToNameMap.get(type); | ||
| } | ||
| return Preconditions.checkNotNull(typeName, "Type not found in repository: %s", type); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -305,7 +318,8 @@ | |
| } | ||
|
|
||
| private TypeRepository(@Nonnull final FileDescriptorSet fileDescSet, | ||
| @Nonnull final Map<Type, String> typeToNameMap) throws DescriptorValidationException { | ||
| @Nonnull final BiMap<Type, String> nullableTypeToNameMap, | ||
| @Nonnull final BiMap<Type, String> nonNullableTypeToNameMap) throws DescriptorValidationException { | ||
| this.fileDescSet = fileDescSet; | ||
| Map<String, FileDescriptor> fileDescMap = init(fileDescSet); | ||
|
|
||
|
|
@@ -327,7 +341,8 @@ | |
| enumDescriptorMapShort.remove(enumName); | ||
| } | ||
|
|
||
| this.typeToNameMap = ImmutableMap.copyOf(typeToNameMap); | ||
| this.nullableTypeToNameMap = ImmutableBiMap.copyOf(nullableTypeToNameMap); | ||
| this.nonNullableTypeToNameMap = ImmutableBiMap.copyOf(nonNullableTypeToNameMap); | ||
| } | ||
|
|
||
| @SuppressWarnings("java:S3776") | ||
|
|
@@ -436,13 +451,15 @@ | |
| public static class Builder { | ||
| private @Nonnull final FileDescriptorProto.Builder fileDescProtoBuilder; | ||
| private @Nonnull final FileDescriptorSet.Builder fileDescSetBuilder; | ||
| private @Nonnull final BiMap<Type, String> typeToNameMap; | ||
| private @Nonnull final BiMap<Type, String> nullableTypeToNameMap; | ||
| private @Nonnull final BiMap<Type, String> nonNullableTypeToNameMap; | ||
|
|
||
| private Builder() { | ||
| fileDescProtoBuilder = FileDescriptorProto.newBuilder(); | ||
| fileDescProtoBuilder.addAllDependency(DEPENDENCIES.stream().map(FileDescriptor::getFullName).collect(Collectors.toList())); | ||
| fileDescSetBuilder = FileDescriptorSet.newBuilder(); | ||
| typeToNameMap = HashBiMap.create(); | ||
| nullableTypeToNameMap = HashBiMap.create(); | ||
| nonNullableTypeToNameMap = HashBiMap.create(); | ||
| } | ||
|
|
||
| @Nonnull | ||
|
|
@@ -451,7 +468,7 @@ | |
| resultBuilder.addFile(fileDescProtoBuilder.build()); | ||
| resultBuilder.mergeFrom(this.fileDescSetBuilder.build()); | ||
| try { | ||
| return new TypeRepository(resultBuilder.build(), typeToNameMap); | ||
| return new TypeRepository(resultBuilder.build(), nullableTypeToNameMap, nonNullableTypeToNameMap); | ||
| } catch (final DescriptorValidationException dve) { | ||
| throw new IllegalStateException("validation should not fail", dve); | ||
| } | ||
|
|
@@ -471,20 +488,50 @@ | |
|
|
||
| @Nonnull | ||
| public Builder addTypeIfNeeded(@Nonnull final Type type) { | ||
| if (!typeToNameMap.containsKey(type)) { | ||
| // Type.Null is special - it can only be nullable and has no non-nullable variant | ||
| if (type instanceof Type.Null) { | ||
| if (!nullableTypeToNameMap.containsKey(type)) { | ||
| type.defineProtoType(this); | ||
| } | ||
| return this; | ||
| } | ||
|
Comment on lines
+491
to
+497
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe It's a bit weird, as none of those types actually override |
||
|
|
||
| // Type.None is special - it can only be non-nullable and has no nullable variant | ||
| if (type.getTypeCode() == Type.TypeCode.NONE) { | ||
| if (!nonNullableTypeToNameMap.containsKey(type)) { | ||
| type.defineProtoType(this); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
|
|
||
| final BiMap<Type, String> targetMap = type.isNullable() ? nullableTypeToNameMap : nonNullableTypeToNameMap; | ||
| final BiMap<Type, String> oppositeMap = type.isNullable() ? nonNullableTypeToNameMap : nullableTypeToNameMap; | ||
|
|
||
| if (!targetMap.containsKey(type)) { | ||
| // Check if the opposite nullability variant exists and get its protobuf name | ||
| final Type oppositeNullabilityType = type.isNullable() ? type.notNullable() : type.withNullability(true); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could be simplified to: final Type oppositeNullabilityType = type.withNullability(!type.isNullable()); |
||
| String existingProtoName = oppositeMap.get(oppositeNullabilityType); | ||
|
|
||
| if (existingProtoName != null) { | ||
| // Reuse the same protobuf name for the type with different nullability | ||
| targetMap.put(type, existingProtoName); | ||
| return this; | ||
| } | ||
|
|
||
| // Standard case: define the protobuf type | ||
| type.defineProtoType(this); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| @Nonnull | ||
| public Optional<String> getTypeName(@Nonnull final Type type) { | ||
| return Optional.ofNullable(typeToNameMap.get(type)); | ||
| } | ||
|
|
||
| @Nonnull | ||
| public Optional<Type> getTypeByName(@Nonnull final String name) { | ||
| return Optional.ofNullable(typeToNameMap.inverse().get(name)); | ||
| if (type.isNullable()) { | ||
| return Optional.ofNullable(nullableTypeToNameMap.get(type)); | ||
| } else { | ||
| return Optional.ofNullable(nonNullableTypeToNameMap.get(type)); | ||
| } | ||
| } | ||
|
|
||
| @Nonnull | ||
|
|
@@ -501,8 +548,37 @@ | |
|
|
||
| @Nonnull | ||
| public Builder registerTypeToTypeNameMapping(@Nonnull final Type type, @Nonnull final String protoTypeName) { | ||
| Verify.verify(!typeToNameMap.containsKey(type)); | ||
| typeToNameMap.put(type, protoTypeName); | ||
| final BiMap<Type, String> targetMap = type.isNullable() ? nullableTypeToNameMap : nonNullableTypeToNameMap; | ||
| final BiMap<Type, String> oppositeMap = type.isNullable() ? nonNullableTypeToNameMap : nullableTypeToNameMap; | ||
|
Check warning on line 552 in fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java
|
||
|
|
||
| final String existingTypeName = targetMap.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 the opposite nullability variant uses this protobuf name | ||
| final Type oppositeNullabilityType = type.isNullable() ? type.notNullable() : type.withNullability(true); | ||
| final String oppositeExistingName = oppositeMap.get(oppositeNullabilityType); | ||
| if (oppositeExistingName != null && oppositeExistingName.equals(protoTypeName)) { | ||
| // Both nullable and non-nullable variants can share the same protobuf type name | ||
| targetMap.put(type, protoTypeName); | ||
| return this; | ||
| } | ||
|
|
||
| // Check if this protobuf name is already used by a different structural type | ||
| final Type existingTypeForName = targetMap.inverse().get(protoTypeName); | ||
| if (existingTypeForName != null && !existingTypeForName.equals(type)) { | ||
| throw new IllegalArgumentException(String.format( | ||
|
Comment on lines
+573
to
+575
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is actually already handled by What I think we are missing here is coverage of the case:
I think you'd need to add a check that |
||
| "Name %s is already registered with a different type %s, cannot register with type %s", | ||
| protoTypeName, existingTypeForName, type)); | ||
| } | ||
|
|
||
| // Standard case: new type with new name (or same name as opposite nullability) | ||
| targetMap.put(type, protoTypeName); | ||
| return this; | ||
| } | ||
|
|
||
|
|
@@ -515,7 +591,7 @@ | |
| @Nonnull | ||
| public Optional<String> defineAndResolveType(@Nonnull final Type type) { | ||
| addTypeIfNeeded(type); | ||
| return Optional.ofNullable(typeToNameMap.get(type)); | ||
| return getTypeName(type); | ||
| } | ||
|
|
||
| @Nonnull | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commenting here some unit tests that should be added to
TypeRepositoryTest:addSameTypeMultipleTimesShouldNotCreateMultipleMessageTypes, but it adds a nullable and non-nullable variant of the same typegetProtoTypeNameornewMessageBuilderwith the other nullability. I believe this currently throws an error, though I'm not sure it needs toaddPrimitiveTypeIsNotAllowed, but also includingAny,Null,None, andRelationtypes. It would be good to also updateaddPrimitiveTypeIsNotAllowedwhile you're there, because the assertion is off. That doesn't throw anymore, so having thecatchis confusing and muddies what the assertion actually isMaybe these should be in some schema file, but it is probably going to be easier to get the coverage in unit testing