feat(Spanner): Add support for Protobuf Enums - #15699
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for Spanner Enum types by adding a ProtobufEnumName property, a constructor, and helper methods to SpannerDbType, as well as supporting conversion of protobuf values to CLR enum types. The review feedback suggests correcting the string representation of the enum type from PROTO to ENUM, handling nullable enum types correctly during conversion, and fixing a typo in a parameter name.
|
Looking good, I think writing the integration tests should help clear up how spanner is actually handling the enums. You've got the logic to handle enums coming from Spanner almost there, but we will also need to support the other direction of enum types flowing to spanner from the client. |
|
@robertvoinescu-work answered most of our comments but there's some discussion still worth having. I'll be going over with Amanda the issues I'm seeing with FQNs later today and should be able to respond again with more afterwards. |
ba4527f to
ab7bd7c
Compare
7a1eca7 to
783c20e
Compare
| /// <summary> | ||
| /// Convenience methods for working with Protobuf enums | ||
| /// </summary> | ||
| public static class ProtobufEnumExtensions |
There was a problem hiding this comment.
I lean towards limiting our changes to the public surface as much as possible so I'll suggest marking this internal for our own use cases/tests. Open to discussion - also Amanda might have some input here, too.
There was a problem hiding this comment.
The hope for this was to be a convenience for users writing DDL when knowing the FQN of a Protobuf enum might be more obscure knowledge (even more than protos already are), and the path to getting it programmatically isn't as seamless as getting the FQN for Protobuf messages. But I'm fine either way, this probably also doesn't have much discoverability without demonstrating the use in public facing samples
efevans
left a comment
There was a problem hiding this comment.
Addressed feedback
| /// <summary> | ||
| /// Convenience methods for working with Protobuf enums | ||
| /// </summary> | ||
| public static class ProtobufEnumExtensions |
There was a problem hiding this comment.
The hope for this was to be a convenience for users writing DDL when knowing the FQN of a Protobuf enum might be more obscure knowledge (even more than protos already are), and the path to getting it programmatically isn't as seamless as getting the FQN for Protobuf messages. But I'm fine either way, this probably also doesn't have much discoverability without demonstrating the use in public facing samples
04e2d21 to
d8d2b5e
Compare
robertvoinescu-work
left a comment
There was a problem hiding this comment.
Mostly looks good to me, my only hang up is the extension method @amanda-tarafa please let us know your take on including this.
d8d2b5e to
71114e5
Compare
@robertvoinescu-work narrowed it down from public to internal |
71114e5 to
186c437
Compare
amanda-tarafa
left a comment
There was a problem hiding this comment.
There's some fundamental feature creep here that we don't want, and some improvements. I didn't review all of the tests but I'm pretty sure they'll need the same fixes as the ones I did review once we stop supporting conversions of non-
Protobuf enums and conversions to other than Protobuf enums (like string and numeric) .
| } | ||
| throw new ArgumentException($"Interval parameters must be of type {typeof(Interval).FullName} or string"); | ||
| case TypeCode.Enum: | ||
| if (value is string enumStr) |
There was a problem hiding this comment.
Is this string value meant to be the integer value or the name associated with the Enum value? Or would spanner accept both?
For instance:
enum Season
{
Spring=0,
Summer=1 ,
Autumn=2,
Winter=3
}
Can enumStr be 0 and Spring indistinctly, or just one of those? If it can be Spring, what about spring?
There was a problem hiding this comment.
Spanner is accepting a stringified integrer constant, so "0" would work, but "Spring" and other case variations will fail
| Value.KindOneofCase.NumberValue => System.Enum.ToObject(targetClrType, Convert.ToInt64(wireValue.NumberValue, InvariantCulture)), | ||
| Value.KindOneofCase.StringValue => System.Enum.Parse(targetClrType, wireValue.StringValue), |
There was a problem hiding this comment.
In which cases does Spanner returns a NumberValue or a StringValue? I would expect they always return one or the other so I don't understand this. Also here you need to check that the type code is Enum, otherwise you are allowing the random conversion of string and numerical values to enums and we shouldn't do that, users can always do that if they want to.
There was a problem hiding this comment.
Spanner returns values from a Protobuf enum field in the StringValue field as the stringified integer constant.
Spanner returns NumberValue in the case of reading from a numeric column like Int64, and was included here for the case of supporting more conversions. I've removed most of the LoC here and replaced it with an additional check in the if to look for a TypeCode of Enum and only parsing the StringValue field.
| } | ||
| } | ||
|
|
||
| if (targetClrType.IsEnum) |
There was a problem hiding this comment.
nit: This block seems randomly placed here, add it after the Protobuf logic.
There was a problem hiding this comment.
Updated this block to be right after the Protobuf logic
| + $" {nameof(SpannerDbType.Timestamp)}, {nameof(SpannerDbType.Date)}, {nameof(SpannerDbType.String)}," | ||
| + $" {nameof(SpannerDbType.Bytes)}, {nameof(SpannerDbType.Json)}, {nameof(SpannerDbType.PgJsonb)}, {nameof(SpannerDbType.Numeric)}," | ||
| + $" {nameof(SpannerDbType.PgNumeric)}, {nameof(SpannerDbType.PgOid)}), {nameof(SpannerDbType.Interval)}, {nameof(SpannerDbType.Uuid)}"); | ||
| + $" {nameof(SpannerDbType.PgNumeric)}, {nameof(SpannerDbType.PgOid)}), {nameof(SpannerDbType.Interval)}, {nameof(SpannerDbType.Uuid)}," |
There was a problem hiding this comment.
At this point I'd prefer to simplify this error message to something like "{nameof(SpannerDbType)} set to {nameof(SpannerDbType.Unspecified)} for {Value}"
There was a problem hiding this comment.
Updated this whole block with your suggestion
| /// <summary> | ||
| /// Protobuf message | ||
| /// </summary> | ||
| internal const string ProtobufMessage = "Protobuf Message"; | ||
|
|
||
| internal const string ProtobufEnum = "Protobuf Enum"; | ||
|
|
There was a problem hiding this comment.
Just a reminder to get rid of these after the error message is simplified.
| yield return new object[] { Color.Red, SpannerDbType.Float64, "1" }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.Float64, "2" }; |
There was a problem hiding this comment.
Same, we don't want this.
| yield return new object[] { Color.Red, SpannerDbType.Int64, Quote("1") }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.Int64, Quote("2") }; |
| yield return new object[] { Color.Red, SpannerDbType.String, Quote(Color.Red.ToString()) }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.String, Quote(Pet.Types.Species.Dog.ToString()) }; | ||
|
|
There was a problem hiding this comment.
Also definitely not.
| yield return new object[] { "passthrubadbytes", SpannerDbType.Bytes, Quote("passthrubadbytes") }; | ||
|
|
||
| // Protobuf Enum | ||
| yield return new object[] { Color.Red, SpannerDbType.FromClrType(typeof(Pet.Types.Species)), Quote(((long) Color.Red).ToString()) }; |
There was a problem hiding this comment.
If this test pass, there's something very wrong here, you are using the Species enum to convert colors back and forth. (Precisely why we don't want random conversions that we cannot validate).
What is the wire format of a Protobuf enum value expected and sent by Spanner. That's the only thing we need to support back and forth.
After this is fixed, I want to see this test still in, but as a failing test, so that we make certain we validate the convertions.
There was a problem hiding this comment.
This was succeeding because the checks in the conversions back and forth between Protobuf Value and CLR type for Protobuf enums was only doing simple checks on if the CLR type was an enum. It lacked checks if the Full Name of the value (either Value Protobuf or enum value, depending on direction of conversion) was similar to the Full name of either the target CLR type and/or specified SpannerDbType.
I've updated the conversion logic for both ways to include a check that the Full Name matches, essentially that we are doing a conversion from the matching Protobuf enum types on both sides of the conversion.
To validate this, I've added a couple of separate Facts below, ToProtobufType_DifferentEnumTypeAndValue_ThrowsException and ConvertToClrType_DifferentEnumTypeAndValue_ThrowsException to ensure that these cases are properly failing as you requested.
| }; | ||
| yield return new object[] | ||
| { | ||
| new List<DayOfWeek>(GetEnumsForArray()), SpannerDbType.ArrayOf(SpannerDbType.String), |
There was a problem hiding this comment.
No. And if this was supported before as for any other object type that we could call ToString() on that's fine, but then we add a test confirming that non-protobuf enums cannot be converted to their Enum type, only to strings.
There was a problem hiding this comment.
Removed along with the other tests with non-Protobuf Enums
|
Do not merge until response PRs have been squashed |
efevans
left a comment
There was a problem hiding this comment.
@amanda-tarafa Updated the design like we discussed offline. The two-way conversions between Protobuf enum values and Protobuf enum types (columns) is limited to only the case when the Protobuf enum value is of the Protobuf enum type (e.g. no conversions from Color.Red to Species). There's a couple of new tests in SpannerDbTypeTests.ValueConversions that check for a proper failure in the case of mismatched Protobuf enums: ToProtobufType_DifferentEnumTypeAndValue_ThrowsException and ConvertToClrType_DifferentEnumTypeAndValue_ThrowsException.
All of the new change to your comments are in a separate commit in case you'd like to see just the diff'd changes, we will squash down to the single conventional commit before merging.
| } | ||
| throw new ArgumentException($"Interval parameters must be of type {typeof(Interval).FullName} or string"); | ||
| case TypeCode.Enum: | ||
| if (value is string enumStr) |
There was a problem hiding this comment.
Spanner is accepting a stringified integrer constant, so "0" would work, but "Spring" and other case variations will fail
| Value.KindOneofCase.NumberValue => System.Enum.ToObject(targetClrType, Convert.ToInt64(wireValue.NumberValue, InvariantCulture)), | ||
| Value.KindOneofCase.StringValue => System.Enum.Parse(targetClrType, wireValue.StringValue), |
There was a problem hiding this comment.
Spanner returns values from a Protobuf enum field in the StringValue field as the stringified integer constant.
Spanner returns NumberValue in the case of reading from a numeric column like Int64, and was included here for the case of supporting more conversions. I've removed most of the LoC here and replaced it with an additional check in the if to look for a TypeCode of Enum and only parsing the StringValue field.
| } | ||
| } | ||
|
|
||
| if (targetClrType.IsEnum) |
There was a problem hiding this comment.
Updated this block to be right after the Protobuf logic
| + $" {nameof(SpannerDbType.Timestamp)}, {nameof(SpannerDbType.Date)}, {nameof(SpannerDbType.String)}," | ||
| + $" {nameof(SpannerDbType.Bytes)}, {nameof(SpannerDbType.Json)}, {nameof(SpannerDbType.PgJsonb)}, {nameof(SpannerDbType.Numeric)}," | ||
| + $" {nameof(SpannerDbType.PgNumeric)}, {nameof(SpannerDbType.PgOid)}), {nameof(SpannerDbType.Interval)}, {nameof(SpannerDbType.Uuid)}"); | ||
| + $" {nameof(SpannerDbType.PgNumeric)}, {nameof(SpannerDbType.PgOid)}), {nameof(SpannerDbType.Interval)}, {nameof(SpannerDbType.Uuid)}," |
There was a problem hiding this comment.
Updated this whole block with your suggestion
| /// <summary> | ||
| /// Protobuf message | ||
| /// </summary> | ||
| internal const string ProtobufMessage = "Protobuf Message"; | ||
|
|
||
| internal const string ProtobufEnum = "Protobuf Enum"; | ||
|
|
| yield return new object[] { Color.Red, SpannerDbType.Float64, "1" }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.Float64, "2" }; |
| yield return new object[] { Color.Red, SpannerDbType.Int64, Quote("1") }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.Int64, Quote("2") }; |
| yield return new object[] { Color.Red, SpannerDbType.String, Quote(Color.Red.ToString()) }; | ||
| yield return new object[] { Pet.Types.Species.Dog, SpannerDbType.String, Quote(Pet.Types.Species.Dog.ToString()) }; | ||
|
|
| }; | ||
| yield return new object[] | ||
| { | ||
| new List<DayOfWeek>(GetEnumsForArray()), SpannerDbType.ArrayOf(SpannerDbType.String), |
There was a problem hiding this comment.
Removed along with the other tests with non-Protobuf Enums
| yield return new object[] { "passthrubadbytes", SpannerDbType.Bytes, Quote("passthrubadbytes") }; | ||
|
|
||
| // Protobuf Enum | ||
| yield return new object[] { Color.Red, SpannerDbType.FromClrType(typeof(Pet.Types.Species)), Quote(((long) Color.Red).ToString()) }; |
There was a problem hiding this comment.
This was succeeding because the checks in the conversions back and forth between Protobuf Value and CLR type for Protobuf enums was only doing simple checks on if the CLR type was an enum. It lacked checks if the Full Name of the value (either Value Protobuf or enum value, depending on direction of conversion) was similar to the Full name of either the target CLR type and/or specified SpannerDbType.
I've updated the conversion logic for both ways to include a check that the Full Name matches, essentially that we are doing a conversion from the matching Protobuf enum types on both sides of the conversion.
To validate this, I've added a couple of separate Facts below, ToProtobufType_DifferentEnumTypeAndValue_ThrowsException and ConvertToClrType_DifferentEnumTypeAndValue_ThrowsException to ensure that these cases are properly failing as you requested.
b/522532048