diff --git a/spanner/integration_test.go b/spanner/integration_test.go index a2b441621f03..c6fcba31bdd3 100644 --- a/spanner/integration_test.go +++ b/spanner/integration_test.go @@ -48,6 +48,7 @@ import ( "cloud.google.com/go/spanner/internal" pb "cloud.google.com/go/spanner/testdata/protos" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" "go.opencensus.io/stats/view" "go.opencensus.io/tag" "google.golang.org/api/iterator" @@ -2034,7 +2035,9 @@ func TestIntegration_BasicTypes(t *testing.T) { Numeric NUMERIC, NumericArray ARRAY, JSON JSON, - JSONArray ARRAY + JSONArray ARRAY, + UUID UUID, + UUIDArray Array, ) PRIMARY KEY (RowID)`, } client, _, cleanup := prepareIntegrationTest(ctx, t, DefaultSessionPoolConfig, stmts) @@ -2191,6 +2194,14 @@ func TestIntegration_BasicTypes(t *testing.T) { {col: "NumericArray", val: nil, wantWithDefaultConfig: []NullNumeric(nil), wantWithNumber: []NullNumeric(nil)}, {col: "JSON", val: nil, wantWithDefaultConfig: NullJSON{}, wantWithNumber: NullJSON{}}, {col: "JSONArray", val: nil, wantWithDefaultConfig: []NullJSON(nil), wantWithNumber: []NullJSON(nil)}, + {col: "UUID", val: uuid1}, + {col: "UUID", val: uuid1, wantWithDefaultConfig: SpannerNullUUID{uuid1, true}, wantWithNumber: SpannerNullUUID{uuid1, true}}, + {col: "UUID", val: SpannerNullUUID{uuid1, true}}, + {col: "UUID", val: SpannerNullUUID{uuid1, true}, wantWithDefaultConfig: uuid1, wantWithNumber: uuid1}, + {col: "UUID", val: SpannerNullUUID{uuid.UUID{}, false}}, + {col: "UUIDArray", val: []uuid.UUID(nil), wantWithDefaultConfig: []SpannerNullUUID(nil), wantWithNumber: []SpannerNullUUID(nil)}, + {col: "UUIDArray", val: []uuid.UUID{}, wantWithDefaultConfig: []SpannerNullUUID{}, wantWithNumber: []SpannerNullUUID{}}, + {col: "UUIDArray", val: []uuid.UUID{uuid1, uuid2}, wantWithDefaultConfig: []SpannerNullUUID{{uuid1, true}, {uuid2, true}}, wantWithNumber: []SpannerNullUUID{{uuid1, true}, {uuid2, true}}}, } // See https://github.com/GoogleCloudPlatform/cloud-spanner-emulator/issues/31 diff --git a/spanner/key.go b/spanner/key.go index c87068def54c..603edefd3e52 100644 --- a/spanner/key.go +++ b/spanner/key.go @@ -24,6 +24,7 @@ import ( "cloud.google.com/go/civil" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/protobuf/reflect/protoreflect" proto3 "google.golang.org/protobuf/types/known/structpb" @@ -85,7 +86,7 @@ func keyPartValue(part interface{}) (pb *proto3.Value, err error) { pb, _, err = encodeValue(int64(v)) case uint32: pb, _, err = encodeValue(int64(v)) - case int64, float64, float32, NullInt64, NullFloat64, NullFloat32, bool, NullBool, []byte, string, NullString, time.Time, civil.Date, NullTime, NullDate, big.Rat, NullNumeric, protoreflect.Enum, NullProtoEnum: + case int64, float64, float32, NullInt64, NullFloat64, NullFloat32, bool, NullBool, []byte, string, NullString, time.Time, civil.Date, NullTime, NullDate, big.Rat, NullNumeric, protoreflect.Enum, NullProtoEnum, uuid.UUID, uuid.NullUUID, SpannerNullUUID: pb, _, err = encodeValue(v) case Encoder: part, err = v.EncodeSpanner() @@ -167,6 +168,14 @@ func (key Key) elemString(b *bytes.Buffer, part interface{}) { fmt.Fprintf(b, "%q", v.Format(time.RFC3339Nano)) case big.Rat: fmt.Fprintf(b, "%v", NumericString(&v)) + case uuid.UUID: + fmt.Fprintf(b, "%s", v.String()) + case uuid.NullUUID: + if !v.Valid { + fmt.Fprintf(b, "%s", nullString) + } else { + fmt.Fprintf(b, "%s", v.UUID.String()) + } case Encoder: var err error part, err = v.EncodeSpanner() diff --git a/spanner/key_test.go b/spanner/key_test.go index de54d7b5235e..bd286bb11490 100644 --- a/spanner/key_test.go +++ b/spanner/key_test.go @@ -18,6 +18,7 @@ package spanner import ( "errors" + "fmt" "math/big" "testing" "time" @@ -25,6 +26,7 @@ import ( "cloud.google.com/go/civil" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" pb "cloud.google.com/go/spanner/testdata/protos" + "github.com/google/uuid" proto3 "google.golang.org/protobuf/types/known/structpb" ) @@ -139,6 +141,36 @@ func TestKey(t *testing.T) { wantProto: listValueProto(stringProto("1.000000000")), wantStr: `(1.000000000)`, }, + { + k: Key{uuid1}, + wantProto: listValueProto(uuidProto(uuid1)), + wantStr: fmt.Sprintf("(%s)", uuid1.String()), + }, + { + k: Key{uuid.NullUUID{UUID: uuid1, Valid: true}}, + wantProto: listValueProto(uuidProto(uuid1)), + wantStr: fmt.Sprintf("(%s)", uuid1.String()), + }, + { + k: Key{uuid.NullUUID{UUID: uuid1, Valid: false}}, + wantProto: listValueProto(nullProto()), + wantStr: `()`, + }, + { + k: Key{SpannerNullUUID{uuid1, false}}, + wantProto: listValueProto(nullProto()), + wantStr: `()`, + }, + { + k: Key{SpannerNullUUID{uuid1, true}}, + wantProto: listValueProto(uuidProto(uuid1)), + wantStr: fmt.Sprintf("(%s)", uuid1.String()), + }, + { + k: Key{SpannerNullUUID{uuid1, false}}, + wantProto: listValueProto(nullProto()), + wantStr: `()`, + }, { k: Key{[]byte("value")}, wantProto: listValueProto(bytesProto([]byte("value"))), diff --git a/spanner/protoutils.go b/spanner/protoutils.go index 988e715941e2..1b05b8aa4b63 100644 --- a/spanner/protoutils.go +++ b/spanner/protoutils.go @@ -24,6 +24,7 @@ import ( "cloud.google.com/go/civil" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "github.com/google/uuid" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" proto3 "google.golang.org/protobuf/types/known/structpb" @@ -123,6 +124,14 @@ func dateType() *sppb.Type { return &sppb.Type{Code: sppb.TypeCode_DATE} } +func uuidProto(u uuid.UUID) *proto3.Value { + return stringProto(u.String()) +} + +func uuidType() *sppb.Type { + return &sppb.Type{Code: sppb.TypeCode_UUID} +} + func listProto(p ...*proto3.Value) *proto3.Value { return &proto3.Value{Kind: &proto3.Value_ListValue{ListValue: &proto3.ListValue{Values: p}}} } diff --git a/spanner/row.go b/spanner/row.go index b2be78a0a92c..57dfd8fe9075 100644 --- a/spanner/row.go +++ b/spanner/row.go @@ -50,28 +50,30 @@ import ( // // Supported types and their corresponding Cloud Spanner column type(s) are: // -// *string(not NULL), *NullString - STRING -// *[]string, *[]NullString - STRING ARRAY -// *[]byte - BYTES -// *[][]byte - BYTES ARRAY -// *int64(not NULL), *NullInt64 - INT64 -// *[]int64, *[]NullInt64 - INT64 ARRAY -// *bool(not NULL), *NullBool - BOOL -// *[]bool, *[]NullBool - BOOL ARRAY -// *float32(not NULL), *NullFloat32 - FLOAT32 -// *[]float32, *[]NullFloat32 - FLOAT32 ARRAY -// *float64(not NULL), *NullFloat64 - FLOAT64 -// *[]float64, *[]NullFloat64 - FLOAT64 ARRAY -// *big.Rat(not NULL), *NullNumeric - NUMERIC -// *[]big.Rat, *[]NullNumeric - NUMERIC ARRAY -// *time.Time(not NULL), *NullTime - TIMESTAMP -// *[]time.Time, *[]NullTime - TIMESTAMP ARRAY -// *Date(not NULL), *NullDate - DATE -// *[]civil.Date, *[]NullDate - DATE ARRAY -// *[]*some_go_struct, *[]NullRow - STRUCT ARRAY -// *NullJSON - JSON -// *[]NullJSON - JSON ARRAY -// *GenericColumnValue - any Cloud Spanner type +// *string(not NULL), *NullString - STRING +// *[]string, *[]NullString - STRING ARRAY +// *[]byte - BYTES +// *[][]byte - BYTES ARRAY +// *int64(not NULL), *NullInt64 - INT64 +// *[]int64, *[]NullInt64 - INT64 ARRAY +// *bool(not NULL), *NullBool - BOOL +// *[]bool, *[]NullBool - BOOL ARRAY +// *float32(not NULL), *NullFloat32 - FLOAT32 +// *[]float32, *[]NullFloat32 - FLOAT32 ARRAY +// *float64(not NULL), *NullFloat64 - FLOAT64 +// *[]float64, *[]NullFloat64 - FLOAT64 ARRAY +// *big.Rat(not NULL), *NullNumeric - NUMERIC +// *[]big.Rat, *[]NullNumeric - NUMERIC ARRAY +// *time.Time(not NULL), *NullTime - TIMESTAMP +// *[]time.Time, *[]NullTime - TIMESTAMP ARRAY +// *Date(not NULL), *NullDate - DATE +// *[]civil.Date, *[]NullDate - DATE ARRAY +// *uuid.UUID(not NULL), *SpannerNullUuid - UUID +// *[]uuid.UUID, *[]SpannerNullUuid - UUID Array +// *[]*some_go_struct, *[]NullRow - STRUCT ARRAY +// *NullJSON - JSON +// *[]NullJSON - JSON ARRAY +// *GenericColumnValue - any Cloud Spanner type // // For TIMESTAMP columns, the returned time.Time object will be in UTC. // diff --git a/spanner/row_test.go b/spanner/row_test.go index 593f9ae73029..cebf09e7d859 100644 --- a/spanner/row_test.go +++ b/spanner/row_test.go @@ -30,6 +30,7 @@ import ( "cloud.google.com/go/internal/testutil" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" "google.golang.org/api/iterator" proto "google.golang.org/protobuf/proto" proto3 "google.golang.org/protobuf/types/known/structpb" @@ -81,6 +82,11 @@ var ( {Name: "NULL_DATE", Type: dateType()}, {Name: "DATE_ARRAY", Type: listType(dateType())}, {Name: "NULL_DATE_ARRAY", Type: listType(dateType())}, + // UUID / UUID ARRAY + {Name: "UUID", Type: uuidType()}, + {Name: "NULL_UUID", Type: uuidType()}, + {Name: "UUID_ARRAY", Type: listType(uuidType())}, + {Name: "NULL_UUID_ARRAY", Type: listType(uuidType())}, // STRUCT ARRAY { @@ -91,6 +97,7 @@ var ( mkField("Col2", floatType()), mkField("Col3", float32Type()), mkField("Col4", stringType()), + mkField("Col5", uuidType()), ), ), }, @@ -102,6 +109,7 @@ var ( mkField("Col2", floatType()), mkField("Col3", float32Type()), mkField("Col4", stringType()), + mkField("Col5", uuidType()), ), ), }, @@ -147,10 +155,15 @@ var ( nullProto(), listProto(nullProto(), dateProto(dt)), nullProto(), + // UUID / UUID ARRAY + uuidProto(uuid1), + nullProto(), + listProto(nullProto(), uuidProto(uuid1)), + nullProto(), // STRUCT ARRAY listProto( nullProto(), - listProto(intProto(3), floatProto(33.3), float32Proto(0.3), stringProto("three")), + listProto(intProto(3), floatProto(33.3), float32Proto(0.3), stringProto("three"), uuidProto(uuid1)), nullProto(), ), nullProto(), @@ -204,6 +217,11 @@ func TestColumnValues(t *testing.T) { {NullDate{}}, {[]NullDate{{}, {dt, true}}}, {[]NullDate(nil)}, + // UUID / UUID ARRAY + {uuid1, SpannerNullUUID{uuid1, true}}, + {SpannerNullUUID{}}, + {[]SpannerNullUUID{{}, {uuid1, true}}}, + {[]SpannerNullUUID(nil)}, // STRUCT ARRAY { []*struct { @@ -211,6 +229,7 @@ func TestColumnValues(t *testing.T) { Col2 NullFloat64 Col3 NullFloat32 Col4 string + Col5 SpannerNullUUID }{ nil, @@ -219,6 +238,7 @@ func TestColumnValues(t *testing.T) { NullFloat64{33.3, true}, NullFloat32{0.3, true}, "three", + SpannerNullUUID{uuid1, true}, }, nil, }, @@ -231,12 +251,14 @@ func TestColumnValues(t *testing.T) { mkField("Col2", floatType()), mkField("Col3", float32Type()), mkField("Col4", stringType()), + mkField("Col5", uuidType()), }, vals: []*proto3.Value{ intProto(3), floatProto(33.3), float32Proto(0.3), stringProto("three"), + uuidProto(uuid1), }, }, Valid: true, @@ -250,6 +272,7 @@ func TestColumnValues(t *testing.T) { Col2 NullFloat64 Col3 NullFloat32 Col4 string + Col5 SpannerNullUUID }(nil), []NullRow(nil), }, @@ -334,29 +357,33 @@ func TestNilDst(t *testing.T) { mkField("Col1", intType()), mkField("Col2", floatType()), mkField("Col3", float32Type()), + mkField("Col4", uuidType()), ), ), }, }, []*proto3.Value{listProto( - listProto(intProto(3), floatProto(33.3), float32Proto(0.3)), + listProto(intProto(3), floatProto(33.3), float32Proto(0.3), uuidProto(uuid1)), )}, }, (*[]*struct { Col1 int Col2 float64 Col3 float32 + Col4 uuid.UUID })(nil), errDecodeColumn(0, errNilDst((*[]*struct { Col1 int Col2 float64 Col3 float32 + Col4 uuid.UUID })(nil))), (*struct { StructArray []*struct { Col1 int Col2 float64 Col3 float32 + Col4 uuid.UUID } `spanner:"STRUCT_ARRAY"` })(nil), errNilDst((*struct { @@ -364,6 +391,7 @@ func TestNilDst(t *testing.T) { Col1 int Col2 float64 Col3 float32 + Col4 uuid.UUID } `spanner:"STRUCT_ARRAY"` })(nil)), }, @@ -438,6 +466,10 @@ func TestNullTypeErr(t *testing.T) { "NULL_DATE", &dt, }, + { + "NULL_UUID", + &uuid1, + }, } { wantErr := errDecodeColumn(ntoi(test.colName), errDstNotForNull(test.dst)) if gotErr := row.ColumnByName(test.colName, test.dst); !testEqual(gotErr, wantErr) { @@ -1062,6 +1094,42 @@ func TestBrokenRow(t *testing.T) { return err }())), }, + { + // Field specifies UUID type, value is having a nil Kind. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: uuidType()}, + }, + []*proto3.Value{{Kind: (*proto3.Value_StringValue)(nil)}}, + }, + &SpannerNullUUID{uuid1, true}, + errDecodeColumn(0, errSrcVal(&proto3.Value{Kind: (*proto3.Value_StringValue)(nil)}, "String")), + }, + { + // Field specifies UUID type, but value is for BOOL type. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: uuidType()}, + }, + []*proto3.Value{boolProto(true)}, + }, + &SpannerNullUUID{uuid1, true}, + errDecodeColumn(0, errSrcVal(boolProto(true), "String")), + }, + { + // Field specifies UUID type, but value is incorrect UUID. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: uuidType()}, + }, + []*proto3.Value{stringProto("xyz")}, + }, + &SpannerNullUUID{}, + errDecodeColumn(0, errBadEncoding(stringProto("xyz"), func() error { + _, err := uuid.Parse("xyz") + return err + }())), + }, { // Field specifies ARRAY type, value is having a nil Kind. @@ -1378,6 +1446,51 @@ func TestBrokenRow(t *testing.T) { errDecodeColumn(0, errDecodeArrayElement(0, floatProto(1.0), "DATE", errSrcVal(floatProto(1.0), "String"))), }, + { + // Field specifies ARRAY type, value is having a nil Kind. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: listType(uuidType())}, + }, + []*proto3.Value{{Kind: (*proto3.Value_ListValue)(nil)}}, + }, + &[]SpannerNullUUID{}, + errDecodeColumn(0, errSrcVal(&proto3.Value{Kind: (*proto3.Value_ListValue)(nil)}, "List")), + }, + { + // Field specifies ARRAY type, value is having a nil ListValue. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: listType(uuidType())}, + }, + []*proto3.Value{{Kind: &proto3.Value_ListValue{}}}, + }, + &[]SpannerNullUUID{}, + errDecodeColumn(0, errNilListValue("UUID")), + }, + { + // Field specifies ARRAY type, but value is for FLOAT64 type. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: listType(uuidType())}, + }, + []*proto3.Value{floatProto(1.0)}, + }, + &[]SpannerNullUUID{}, + errDecodeColumn(0, errSrcVal(floatProto(1.0), "List")), + }, + { + // Field specifies ARRAY type, but value is for ARRAY type. + &Row{ + []*sppb.StructType_Field{ + {Name: "Col0", Type: listType(uuidType())}, + }, + []*proto3.Value{listProto(floatProto(1.0))}, + }, + &[]SpannerNullUUID{}, + errDecodeColumn(0, errDecodeArrayElement(0, floatProto(1.0), + "UUID", errSrcVal(floatProto(1.0), "String"))), + }, { // Field specifies ARRAY type, value is having a nil Kind. &Row{ @@ -1621,6 +1734,11 @@ func TestToStruct(t *testing.T) { NullDate NullDate `spanner:"NULL_DATE"` DateArray []NullDate `spanner:"DATE_ARRAY"` NullDateArray []NullDate `spanner:"NULL_DATE_ARRAY"` + // UUID / UUID ARRAY + Uuid uuid.UUID `spanner:"UUID"` + NullUuid SpannerNullUUID `spanner:"NULL_UUID"` + UuidArray []SpannerNullUUID `spanner:"UUID_ARRAY"` + NullUuidArray []SpannerNullUUID `spanner:"NULL_UUID_ARRAY"` // STRUCT ARRAY StructArray []*struct { @@ -1628,12 +1746,14 @@ func TestToStruct(t *testing.T) { Col2 float64 Col3 float32 Col4 string + Col5 uuid.UUID } `spanner:"STRUCT_ARRAY"` NullStructArray []*struct { Col1 int64 Col2 float64 Col3 float32 Col4 string + Col5 uuid.UUID } `spanner:"NULL_STRUCT_ARRAY"` }{ {}, // got @@ -1678,16 +1798,21 @@ func TestToStruct(t *testing.T) { NullDate{}, []NullDate{{}, {dt, true}}, []NullDate(nil), + // UUID / UUID ARRAY + uuid1, + SpannerNullUUID{}, + []SpannerNullUUID{{}, {uuid1, true}}, + []SpannerNullUUID(nil), // STRUCT ARRAY []*struct { Col1 int64 Col2 float64 Col3 float32 Col4 string + Col5 uuid.UUID }{ nil, - - {3, 33.3, float32(0.3), "three"}, + {3, 33.3, float32(0.3), "three", uuid1}, nil, }, []*struct { @@ -1695,6 +1820,7 @@ func TestToStruct(t *testing.T) { Col2 float64 Col3 float32 Col4 string + Col5 uuid.UUID }(nil), }, // want } @@ -1728,6 +1854,8 @@ func TestToStructWithCustomTypes(t *testing.T) { type CustomNullTime NullTime type CustomDate civil.Date type CustomNullDate NullDate + type CustomUUID uuid.UUID + type CustomNullUUID SpannerNullUUID for i, toStuct := range []func(ptr interface{}) error{row.ToStruct, row.ToStructLenient} { s := []struct { @@ -1772,18 +1900,26 @@ func TestToStructWithCustomTypes(t *testing.T) { DateArray []CustomNullDate `spanner:"DATE_ARRAY"` NullDateArray []CustomNullDate `spanner:"NULL_DATE_ARRAY"` + // UUID / UUID ARRAY + Uuid CustomUUID `spanner:"UUID"` + NullUuid CustomNullUUID `spanner:"NULL_UUID"` + UuidArray []CustomNullUUID `spanner:"UUID_ARRAY"` + NullUuidArray []CustomNullUUID `spanner:"NULL_UUID_ARRAY"` + // STRUCT ARRAY StructArray []*struct { Col1 CustomInt64 Col2 CustomFloat64 Col3 CustomFloat32 Col4 CustomString + Col5 CustomUUID } `spanner:"STRUCT_ARRAY"` NullStructArray []*struct { Col1 CustomInt64 Col2 CustomFloat64 Col3 CustomFloat32 Col4 CustomString + Col5 CustomUUID } `spanner:"NULL_STRUCT_ARRAY"` }{ {}, // got @@ -1828,16 +1964,21 @@ func TestToStructWithCustomTypes(t *testing.T) { CustomNullDate{}, []CustomNullDate{{}, {dt, true}}, []CustomNullDate(nil), + // UUID / UUID ARRAY + CustomUUID(uuid1), + CustomNullUUID{}, + []CustomNullUUID{{}, {uuid1, true}}, + []CustomNullUUID(nil), // STRUCT ARRAY []*struct { Col1 CustomInt64 Col2 CustomFloat64 Col3 CustomFloat32 Col4 CustomString + Col5 CustomUUID }{ nil, - - {3, 33.3, 0.3, "three"}, + {3, 33.3, 0.3, "three", CustomUUID(uuid1)}, nil, }, []*struct { @@ -1845,6 +1986,7 @@ func TestToStructWithCustomTypes(t *testing.T) { Col2 CustomFloat64 Col3 CustomFloat32 Col4 CustomString + Col5 CustomUUID }(nil), }, // want } diff --git a/spanner/statement_test.go b/spanner/statement_test.go index ac3da3c0e5e5..51f4dba8d356 100644 --- a/spanner/statement_test.go +++ b/spanner/statement_test.go @@ -24,6 +24,7 @@ import ( "cloud.google.com/go/civil" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "github.com/google/uuid" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" proto3 "google.golang.org/protobuf/types/known/structpb" @@ -147,6 +148,13 @@ func TestConvertParams(t *testing.T) { {[]time.Time{}, listProto(), listType(timeType())}, {[]time.Time{t1, t2, t3}, listProto(timeProto(t1), timeProto(t2), timeProto(t3)), listType(timeType())}, {[]NullTime{{t2, true}, {}}, listProto(timeProto(t2), nullProto()), listType(timeType())}, + // uuid + {uuid1, uuidProto(uuid1), uuidType()}, + {SpannerNullUUID{uuid1, false}, nullProto(), uuidType()}, + {[]uuid.UUID(nil), nullProto(), listType(uuidType())}, + {[]uuid.UUID{}, listProto(), listType(uuidType())}, + {[]uuid.UUID{uuid1, uuid2}, listProto(uuidProto(uuid1), uuidProto(uuid2)), listType(uuidType())}, + {[]SpannerNullUUID{{uuid1, true}, {}}, listProto(uuidProto(uuid1), nullProto()), listType(uuidType())}, // Struct { s1, diff --git a/spanner/value.go b/spanner/value.go index 8e75dfc15987..9e850f45d0f3 100644 --- a/spanner/value.go +++ b/spanner/value.go @@ -34,6 +34,7 @@ import ( "cloud.google.com/go/civil" "cloud.google.com/go/internal/fields" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" + "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/protoadapt" @@ -1056,6 +1057,84 @@ func (n *NullProtoEnum) UnmarshalJSON(payload []byte) error { return nil } +type SpannerNullUUID struct { + UUID uuid.UUID + Valid bool +} + +// IsNull implements NullableValue.IsNull for SpannerNullUUID. +func (n SpannerNullUUID) IsNull() bool { + return !n.Valid +} + +// String implements Stringer.String for SpannerNullUUID +func (n SpannerNullUUID) String() string { + if !n.Valid { + return nullString + } + return n.UUID.String() +} + +// MarshalJSON SpannerNullUUID json.Marshaler.MarshalJSON for SpannerNullUUID. +func (n SpannerNullUUID) MarshalJSON() ([]byte, error) { + return nulljson(n.Valid, n.UUID) +} + +// MarshalJSON SpannerNullUUID json.Marshaler.MarshalJSON for SpannerNullUUID. +func (n *SpannerNullUUID) UnmarshalJSON(payload []byte) error { + if payload == nil { + return fmt.Errorf("payload should not be nil") + } + if jsonIsNull(payload) { + n.Valid = false + return nil + } + parsed_uuid, err := uuid.ParseBytes(payload) + if err != nil { + return fmt.Errorf("payload cannot be converted to uuid: got %v", string(payload)) + } + n.UUID = parsed_uuid + n.Valid = true + return nil +} + +// Value implements the driver.Valuer interface. +func (n SpannerNullUUID) Value() (driver.Value, error) { + if n.IsNull() { + return nil, nil + } + return n.UUID, nil +} + +// Scan implements the sql.Scanner interface. +func (n *SpannerNullUUID) Scan(value interface{}) error { + if value == nil { + n.Valid = false + return nil + } + n.Valid = true + switch p := value.(type) { + default: + return spannerErrorf(codes.InvalidArgument, "invalid type for SpannerNullUUID: %v", p) + case *uuid.UUID: + n.UUID = *p + case uuid.UUID: + n.UUID = p + case *SpannerNullUUID: + n.UUID = p.UUID + n.Valid = p.Valid + case SpannerNullUUID: + n.UUID = p.UUID + n.Valid = p.Valid + } + return nil +} + +// GormDataType is used by gorm to determine the default data type for fields with this type. +func (n SpannerNullUUID) GormDataType() string { + return "UUID" +} + // NullRow represents a Cloud Spanner STRUCT that may be NULL. // See also the document for Row. // Note that NullRow is not a valid Cloud Spanner column Type. @@ -2345,6 +2424,102 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}, opts ...DecodeO return err } p.Valid = true + case *uuid.UUID: + if p == nil { + return errNilDst(p) + } + if code != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + return errDstNotForNull(ptr) + } + x, err := getUuidValue(v) + if err != nil { + return err + } + *p = x + case *SpannerNullUUID, **uuid.UUID: + if p == nil { + return errNilDst(p) + } + if code != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + switch sp := ptr.(type) { + case *SpannerNullUUID: + *sp = SpannerNullUUID{} + case **uuid.UUID: + *sp = nil + } + break + } + x, err := getUuidValue(v) + if err != nil { + return err + } + switch sp := ptr.(type) { + case *SpannerNullUUID: + sp.Valid = true + sp.UUID = x + case **uuid.UUID: + *sp = &x + } + case *[]uuid.UUID: + if p == nil { + return errNilDst(p) + } + if acode != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + *p = nil + break + } + x, err := getListValue(v) + if err != nil { + return err + } + y, err := decodeUuidArray(x) + if err != nil { + return err + } + *p = y + case *[]SpannerNullUUID, *[]*uuid.UUID: + if p == nil { + return errNilDst(p) + } + if acode != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + switch sp := ptr.(type) { + case *[]SpannerNullUUID: + *sp = nil + case *[]*SpannerNullUUID: + *sp = nil + } + break + } + x, err := getListValue(v) + if err != nil { + return err + } + switch sp := ptr.(type) { + case *[]SpannerNullUUID: + y, err := decodeNullUuidArray(x) + if err != nil { + return err + } + *sp = y + case *[]*uuid.UUID: + y, err := decodeUuidPointerArray(x) + if err != nil { + return err + } + *sp = y + } default: // Check if the pointer is a custom type that implements spanner.Decoder // interface. @@ -2457,6 +2632,7 @@ const ( spannerTypeNonNullNumeric spannerTypeNonNullTime spannerTypeNonNullDate + spannerTypeNonNullUuid spannerTypeNullString spannerTypeNullInt64 spannerTypeNullBool @@ -2466,6 +2642,7 @@ const ( spannerTypeNullDate spannerTypeNullNumeric spannerTypeNullJSON + spannerTypeNullUuid spannerTypePGNumeric spannerTypePGJsonB spannerTypeArrayOfNonNullString @@ -2477,6 +2654,7 @@ const ( spannerTypeArrayOfNonNullNumeric spannerTypeArrayOfNonNullTime spannerTypeArrayOfNonNullDate + spannerTypeArrayOfNonNullUuid spannerTypeArrayOfNullString spannerTypeArrayOfNullInt64 spannerTypeArrayOfNullBool @@ -2486,6 +2664,7 @@ const ( spannerTypeArrayOfNullJSON spannerTypeArrayOfNullTime spannerTypeArrayOfNullDate + spannerTypeArrayOfNullUuid spannerTypeArrayOfPGNumeric spannerTypeArrayOfPGJsonB ) @@ -2494,7 +2673,7 @@ const ( // Spanner. func (d decodableSpannerType) supportsNull() bool { switch d { - case spannerTypeNonNullString, spannerTypeNonNullInt64, spannerTypeNonNullBool, spannerTypeNonNullFloat64, spannerTypeNonNullFloat32, spannerTypeNonNullTime, spannerTypeNonNullDate, spannerTypeNonNullNumeric: + case spannerTypeNonNullString, spannerTypeNonNullInt64, spannerTypeNonNullBool, spannerTypeNonNullFloat64, spannerTypeNonNullFloat32, spannerTypeNonNullTime, spannerTypeNonNullDate, spannerTypeNonNullNumeric, spannerTypeNonNullUuid: return false default: return true @@ -2511,6 +2690,7 @@ func (d decodableSpannerType) supportsNull() bool { var typeOfNonNullTime = reflect.TypeOf(time.Time{}) var typeOfNonNullDate = reflect.TypeOf(civil.Date{}) var typeOfNonNullNumeric = reflect.TypeOf(big.Rat{}) +var typeOfNonNullUuid = reflect.TypeOf(uuid.UUID{}) var typeOfNullString = reflect.TypeOf(NullString{}) var typeOfNullInt64 = reflect.TypeOf(NullInt64{}) var typeOfNullBool = reflect.TypeOf(NullBool{}) @@ -2520,6 +2700,7 @@ var typeOfNullTime = reflect.TypeOf(NullTime{}) var typeOfNullDate = reflect.TypeOf(NullDate{}) var typeOfNullNumeric = reflect.TypeOf(NullNumeric{}) var typeOfNullJSON = reflect.TypeOf(NullJSON{}) +var typeOfNullUuid = reflect.TypeOf(SpannerNullUUID{}) var typeOfPGNumeric = reflect.TypeOf(PGNumeric{}) var typeOfPGJsonB = reflect.TypeOf(PGJsonB{}) @@ -2540,6 +2721,11 @@ func getDecodableSpannerType(ptr interface{}, isPtr bool) decodableSpannerType { switch kind { case reflect.Invalid: return spannerTypeInvalid + case reflect.Array: + t := val.Type() + if t.ConvertibleTo(typeOfNonNullUuid) { + return spannerTypeNonNullUuid + } case reflect.String: return spannerTypeNonNullString case reflect.Int64: @@ -2599,6 +2785,9 @@ func getDecodableSpannerType(ptr interface{}, isPtr bool) decodableSpannerType { if t.ConvertibleTo(typeOfNullJSON) { return spannerTypeNullJSON } + if t.ConvertibleTo(typeOfNullUuid) { + return spannerTypeNullUuid + } if t.ConvertibleTo(typeOfPGNumeric) { return spannerTypePGNumeric } @@ -2622,6 +2811,11 @@ func getDecodableSpannerType(ptr interface{}, isPtr bool) decodableSpannerType { return spannerTypeArrayOfNonNullFloat64 case reflect.Float32: return spannerTypeArrayOfNonNullFloat32 + case reflect.Array: + elemType := val.Type().Elem() + if elemType.ConvertibleTo(typeOfNonNullUuid) { + return spannerTypeArrayOfNonNullUuid + } case reflect.Ptr: t := val.Type().Elem() if t.ConvertibleTo(typeOfNullNumeric) { @@ -2671,6 +2865,9 @@ func getDecodableSpannerType(ptr interface{}, isPtr bool) decodableSpannerType { if t.ConvertibleTo(typeOfPGJsonB) { return spannerTypeArrayOfPGJsonB } + if t.ConvertibleTo(typeOfNullUuid) { + return spannerTypeArrayOfNullUuid + } case reflect.Slice: // The only array-of-array type that is supported is [][]byte. kind := val.Type().Elem().Elem().Kind() @@ -2680,6 +2877,8 @@ func getDecodableSpannerType(ptr interface{}, isPtr bool) decodableSpannerType { } } } + + // reflect.TypeOf(v) // Not convertible to a known base type. return spannerTypeUnknown } @@ -2895,6 +3094,23 @@ func (dsc decodableSpannerType) decodeValueToCustomType(v *proto3.Value, t *sppb } else { result = &NullDate{y, !isNull} } + case spannerTypeNonNullUuid, spannerTypeNullUuid: + if code != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + result = &SpannerNullUUID{} + break + } + x, err := getUuidValue(v) + if err != nil { + return err + } + if dsc == spannerTypeNonNullUuid { + result = &x + } else { + result = &SpannerNullUUID{x, !isNull} + } case spannerTypeArrayOfNonNullString, spannerTypeArrayOfNullString: if acode != sppb.TypeCode_STRING { return errTypeMismatch(code, acode, ptr) @@ -3099,6 +3315,23 @@ func (dsc decodableSpannerType) decodeValueToCustomType(v *proto3.Value, t *sppb return err } result = y + case spannerTypeArrayOfNonNullUuid, spannerTypeArrayOfNullUuid: + if acode != sppb.TypeCode_UUID { + return errTypeMismatch(code, acode, ptr) + } + if isNull { + ptr = nil + return nil + } + x, err := getListValue(v) + if err != nil { + return err + } + y, err := decodeGenericArray(reflect.TypeOf(ptr).Elem(), x, uuidType(), "UUID") + if err != nil { + return err + } + result = y default: // This should not be possible. return fmt.Errorf("unknown decodable type found: %v", dsc) @@ -3276,6 +3509,20 @@ func getFloat32Value(v *proto3.Value) (float32, error) { return 0, errSrcVal(v, "Number") } +// getUuidValue returns the uuid value encoded in proto3.Value v whose +// kind is proto3.Value_StringValue. +func getUuidValue(v *proto3.Value) (uuid.UUID, error) { + x, err := getStringValue(v) + if err != nil { + return uuid.UUID{}, err + } + u, err := uuid.Parse(x) + if err != nil { + return uuid.UUID{}, errBadEncoding(v, err) + } + return u, nil +} + // errNilListValue returns error for unexpected nil ListValue in decoding Cloud Spanner ARRAYs. func errNilListValue(sqlType string) error { return spannerErrorf(codes.FailedPrecondition, "unexpected nil ListValue in decoding %v array", sqlType) @@ -3799,6 +4046,48 @@ func decodeDateArray(pb *proto3.ListValue) ([]civil.Date, error) { return a, nil } +// decodeNullUuidArray decodes proto3.ListValue pb into a SpannerNullUUID slice. +func decodeNullUuidArray(pb *proto3.ListValue) ([]SpannerNullUUID, error) { + if pb == nil { + return nil, errNilListValue("UUID") + } + a := make([]SpannerNullUUID, len(pb.Values)) + for i, v := range pb.Values { + if err := decodeValue(v, uuidType(), &a[i]); err != nil { + return nil, errDecodeArrayElement(i, v, "UUID", err) + } + } + return a, nil +} + +// decodeUuidPointerArray decodes proto3.ListValue pb into a *uuid.UUID slice. +func decodeUuidPointerArray(pb *proto3.ListValue) ([]*uuid.UUID, error) { + if pb == nil { + return nil, errNilListValue("UUID") + } + a := make([]*uuid.UUID, len(pb.Values)) + for i, v := range pb.Values { + if err := decodeValue(v, uuidType(), &a[i]); err != nil { + return nil, errDecodeArrayElement(i, v, "UUID", err) + } + } + return a, nil +} + +// decodeUuidArray decodes proto3.ListValue pb into a uuid.UUID slice. +func decodeUuidArray(pb *proto3.ListValue) ([]uuid.UUID, error) { + if pb == nil { + return nil, errNilListValue("UUID") + } + a := make([]uuid.UUID, len(pb.Values)) + for i, v := range pb.Values { + if err := decodeValue(v, uuidType(), &a[i]); err != nil { + return nil, errDecodeArrayElement(i, v, "UUID", err) + } + } + return a, nil +} + func errNotStructElement(i int, v *proto3.Value) error { return errDecodeArrayElement(i, v, "STRUCT", spannerErrorf(codes.FailedPrecondition, "%v(type: %T) doesn't encode Cloud Spanner STRUCT", v, v)) @@ -4441,6 +4730,56 @@ func encodeValue(v interface{}) (*proto3.Value, *sppb.Type, error) { } } pt = listType(dateType()) + case uuid.UUID: + pb.Kind = stringKind(v.String()) + pt = uuidType() + case []uuid.UUID: + if v != nil { + pb, err = encodeArray(len(v), func(i int) interface{} { return v[i] }) + if err != nil { + return nil, nil, err + } + } + pt = listType(uuidType()) + case []*uuid.UUID: + if v != nil { + pb, err = encodeArray(len(v), func(i int) interface{} { return v[i] }) + if err != nil { + return nil, nil, err + } + } + pt = listType(uuidType()) + case SpannerNullUUID: + if v.Valid { + return encodeValue(v.UUID) + } + pt = uuidType() + case []SpannerNullUUID: + if v != nil { + pb, err = encodeArray(len(v), func(i int) interface{} { return v[i] }) + if err != nil { + return nil, nil, err + } + } + pt = listType(uuidType()) + case uuid.NullUUID: + null_uuid := SpannerNullUUID{UUID: v.UUID, Valid: v.Valid} + return encodeValue(null_uuid) + case *uuid.UUID: + if v != nil { + return encodeValue(*v) + } + pt = uuidType() + case *SpannerNullUUID: + if v != nil { + return encodeValue(*v) + } + pt = uuidType() + case *uuid.NullUUID: + if v != nil { + return encodeValue(*v) + } + pt = uuidType() case GenericColumnValue: // Deep clone to ensure subsequent changes to v before // transmission don't affect our encoded value. @@ -4598,6 +4937,10 @@ func convertCustomTypeValue(sourceType decodableSpannerType, v interface{}) (int destination = reflect.Indirect(reflect.New(reflect.TypeOf(PGJsonB{}))) case spannerTypePGNumeric: destination = reflect.Indirect(reflect.New(reflect.TypeOf(PGNumeric{}))) + case spannerTypeNonNullUuid: + destination = reflect.Indirect(reflect.New(reflect.TypeOf(uuid.UUID{}))) + case spannerTypeNullUuid: + destination = reflect.Indirect(reflect.New(reflect.TypeOf(SpannerNullUUID{}))) case spannerTypeArrayOfNonNullString: if reflect.ValueOf(v).IsNil() { return []string(nil), nil @@ -4698,6 +5041,16 @@ func convertCustomTypeValue(sourceType decodableSpannerType, v interface{}) (int return []PGNumeric(nil), nil } destination = reflect.MakeSlice(reflect.TypeOf([]PGNumeric{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap()) + case spannerTypeArrayOfNonNullUuid: + if reflect.ValueOf(v).IsNil() { + return []uuid.UUID{}, nil + } + destination = reflect.MakeSlice(reflect.TypeOf([]uuid.UUID{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap()) + case spannerTypeArrayOfNullUuid: + if reflect.ValueOf(v).IsNil() { + return []SpannerNullUUID(nil), nil + } + destination = reflect.MakeSlice(reflect.TypeOf([]SpannerNullUUID{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap()) default: // This should not be possible. return nil, fmt.Errorf("unknown decodable type found: %v", sourceType) @@ -4882,7 +5235,7 @@ func isSupportedMutationType(v interface{}) bool { float32, *float32, []float32, []*float32, NullFloat32, []NullFloat32, time.Time, *time.Time, []time.Time, []*time.Time, NullTime, []NullTime, civil.Date, *civil.Date, []civil.Date, []*civil.Date, NullDate, []NullDate, - big.Rat, *big.Rat, []big.Rat, []*big.Rat, NullNumeric, []NullNumeric, + big.Rat, *big.Rat, []big.Rat, []*big.Rat, NullNumeric, []NullNumeric, uuid.UUID, []uuid.UUID, *uuid.UUID, []*uuid.UUID, SpannerNullUUID, []SpannerNullUUID, GenericColumnValue, proto.Message, protoreflect.Enum, NullProtoMessage, NullProtoEnum: return true default: diff --git a/spanner/value_test.go b/spanner/value_test.go index 6e7ac34c6f61..601b9e953dcf 100644 --- a/spanner/value_test.go +++ b/spanner/value_test.go @@ -34,6 +34,7 @@ import ( sppb "cloud.google.com/go/spanner/apiv1/spannerpb" pb "cloud.google.com/go/spanner/testdata/protos" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" @@ -50,6 +51,9 @@ var ( t4 = time.Now() d1 = mustParseDate("2016-11-15") d2 = mustParseDate("1678-01-01") + // UUID + uuid1 = uuid.MustParse("94dcd1d9-7582-464a-96d0-071effcc373c") + uuid2 = uuid.MustParse("a344e03b-5f8a-4cbf-87f3-482dc67abe78") ) func mustParseTime(s string) time.Time { @@ -272,6 +276,7 @@ func TestEncodeValue(t *testing.T) { var fNilPtr *float64 f32Value := float32(3.14) var f32NilPtr *float32 + var uuidNilPtr *uuid.UUID tValue := t1 var tNilPtr *time.Time dValue := d1 @@ -315,6 +320,7 @@ func TestEncodeValue(t *testing.T) { tPGJsonb = pgJsonbType() tProtoMessage = protoMessageType(protoMessagefqn) tProtoEnum = protoEnumType(protoEnumfqn) + tUUID = uuidType() ) for i, test := range []struct { in interface{} @@ -381,6 +387,16 @@ func TestEncodeValue(t *testing.T) { {[]float32{3.14, 0.618, float32(math.Inf(-1))}, listProto(float32Proto(3.14), float32Proto(0.618), float32Proto(float32(math.Inf(-1)))), listType(tFloat32), "[]float32"}, {[]NullFloat32{{3.14, true}, {0.618, false}}, listProto(float32Proto(3.14), nullProto()), listType(tFloat32), "[]NullFloat"}, {[]*float32{&f32Value, f32NilPtr}, listProto(float32Proto(3.14), nullProto()), listType(tFloat32), "[]NullFloat32"}, + // UUID / UUID ARRAY + {uuid1, uuidProto(uuid1), tUUID, "uuid.UUID"}, + {SpannerNullUUID{uuid1, true}, uuidProto(uuid1), tUUID, "SpannerNullUUID with value"}, + {SpannerNullUUID{uuid1, false}, nullProto(), tUUID, "SpannerNullUUID with null"}, + {&uuid1, uuidProto(uuid1), tUUID, "*uuid.UUID with value"}, + {uuidNilPtr, nullProto(), tUUID, "*uuid.UUID with null"}, + {[]uuid.UUID{uuid1, uuid2}, listProto(uuidProto(uuid1), uuidProto(uuid2)), listType(tUUID), "[]uuid.UUID"}, + {[]uuid.UUID(nil), nullProto(), listType(tUUID), "null []uuid.UUID"}, + {[]SpannerNullUUID{{uuid1, true}, {uuid2, false}}, listProto(uuidProto(uuid1), nullProto()), listType(tUUID), "[]SpannerNullUUID"}, + {[]*uuid.UUID{&uuid1, uuidNilPtr}, listProto(uuidProto(uuid1), nullProto()), listType(tUUID), "[]*uuid.UUID"}, // NUMERIC / NUMERIC ARRAY {*numValuePtr, numericProto(numValuePtr), tNumeric, "big.Rat"}, {numValuePtr, numericProto(numValuePtr), tNumeric, "*big.Rat"}, @@ -1000,6 +1016,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { type CustomFloat32 float32 type CustomTime time.Time type CustomDate civil.Date + type CustomUUID uuid.UUID type CustomNullString NullString type CustomNullInt64 NullInt64 @@ -1008,6 +1025,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { type CustomNullFloat32 NullFloat32 type CustomNullTime NullTime type CustomNullDate NullDate + type CustomNullUUID SpannerNullUUID sValue := "abc" iValue := int64(300) @@ -1016,6 +1034,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { f32Value := float32(3.14) tValue := t1 dValue := d1 + uuidValue := uuid1 StructTypeProto := structType( mkField("Stringf", stringType()), @@ -1025,7 +1044,9 @@ func TestEncodeStructValueBasicFields(t *testing.T) { mkField("Float32f", float32Type()), mkField("Bytef", bytesType()), mkField("Timef", timeType()), - mkField("Datef", dateType())) + mkField("Datef", dateType()), + mkField("Uuidf", uuidType()), + ) for _, test := range []encodeTest{ { @@ -1039,7 +1060,8 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef []byte Timef time.Time Datef civil.Date - }{"abc", 300, false, 3.45, float32(3.14), []byte("foo"), t1, d1}, + Uuidf uuid.UUID + }{"abc", 300, false, 3.45, float32(3.14), []byte("foo"), t1, d1, uuid1}, listProto( stringProto("abc"), intProto(300), @@ -1048,7 +1070,8 @@ func TestEncodeStructValueBasicFields(t *testing.T) { float32Proto(3.14), bytesProto([]byte("foo")), timeProto(t1), - dateProto(d1)), + dateProto(d1), + uuidProto(uuid1)), StructTypeProto, }, { @@ -1062,7 +1085,8 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef []byte Timef *time.Time Datef *civil.Date - }{&sValue, &iValue, &bValue, &fValue, &f32Value, []byte("foo"), &tValue, &dValue}, + Uuidf *uuid.UUID + }{&sValue, &iValue, &bValue, &fValue, &f32Value, []byte("foo"), &tValue, &dValue, &uuidValue}, listProto( stringProto("abc"), intProto(300), @@ -1071,7 +1095,9 @@ func TestEncodeStructValueBasicFields(t *testing.T) { float32Proto(3.14), bytesProto([]byte("foo")), timeProto(t1), - dateProto(d1)), + dateProto(d1), + uuidProto(uuid1), + ), StructTypeProto, }, { @@ -1085,7 +1111,8 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef []byte Timef *time.Time Datef *civil.Date - }{nil, nil, nil, nil, nil, nil, nil, nil}, + Uuidf *uuid.UUID + }{nil, nil, nil, nil, nil, nil, nil, nil, nil}, listProto( nullProto(), nullProto(), @@ -1094,6 +1121,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { nullProto(), nullProto(), nullProto(), + nullProto(), nullProto()), StructTypeProto, }, @@ -1108,7 +1136,8 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef CustomBytes Timef CustomTime Datef CustomDate - }{"abc", 300, false, 3.45, CustomFloat32(3.14), []byte("foo"), CustomTime(t1), CustomDate(d1)}, + Uuidf CustomUUID + }{"abc", 300, false, 3.45, CustomFloat32(3.14), []byte("foo"), CustomTime(t1), CustomDate(d1), CustomUUID(uuid1)}, listProto( stringProto("abc"), intProto(300), @@ -1117,7 +1146,9 @@ func TestEncodeStructValueBasicFields(t *testing.T) { float32Proto(3.14), bytesProto([]byte("foo")), timeProto(t1), - dateProto(d1)), + dateProto(d1), + uuidProto(uuid1), + ), StructTypeProto, }, { @@ -1131,6 +1162,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef []byte Timef NullTime Datef NullDate + Uuidf SpannerNullUUID }{ NullString{"abc", false}, NullInt64{4, false}, @@ -1140,6 +1172,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { nil, NullTime{t1, false}, NullDate{d1, false}, + SpannerNullUUID{uuid1, false}, }, listProto( nullProto(), @@ -1149,6 +1182,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { nullProto(), nullProto(), nullProto(), + nullProto(), nullProto()), StructTypeProto, }, @@ -1163,6 +1197,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { Bytef CustomBytes Timef CustomNullTime Datef CustomNullDate + Uuidf CustomNullUUID }{ CustomNullString{"abc", false}, CustomNullInt64{4, false}, @@ -1172,6 +1207,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { nil, CustomNullTime{t1, false}, CustomNullDate{d1, false}, + CustomNullUUID{uuid1, false}, }, listProto( nullProto(), @@ -1181,6 +1217,7 @@ func TestEncodeStructValueBasicFields(t *testing.T) { nullProto(), nullProto(), nullProto(), + nullProto(), nullProto()), StructTypeProto, }, @@ -1544,6 +1581,8 @@ func TestDecodeValue(t *testing.T) { protoMessagefqn := "examples.spanner.music.SingerInfo" protoEnumfqn := "examples.spanner.music.Genre" + var uuidNilPtr *uuid.UUID + for _, test := range []struct { desc string proto *proto3.Value @@ -1707,6 +1746,19 @@ func TestDecodeValue(t *testing.T) { // DATE ARRAY with []NullDate {desc: "decode ARRAY to []*civil.Date", proto: listProto(dateProto(d1), nullProto(), dateProto(d2)), protoType: listType(dateType()), want: []*civil.Date{&dValue, nil, &d2Value}}, {desc: "decode NULL to []*civil.Date", proto: nullProto(), protoType: listType(dateType()), want: []*civil.Date(nil)}, + // UUID + {desc: "decode UUID to uuid.UUID", proto: uuidProto(uuid1), protoType: uuidType(), want: uuid1}, + {desc: "decode NULL to uuid.UUID", proto: nullProto(), protoType: uuidType(), want: "", wantErr: true}, + {desc: "decode UUID to *uuid.UUID", proto: uuidProto(uuid1), protoType: uuidType(), want: &uuid1}, + {desc: "decode NULL to *uuid.UUID", proto: nullProto(), protoType: uuidType(), want: uuidNilPtr}, + {desc: "decode UUID to SpannerNullUUID", proto: uuidProto(uuid1), protoType: uuidType(), want: SpannerNullUUID{uuid1, true}}, + {desc: "decode NULL to SpannerNullUUID", proto: nullProto(), protoType: uuidType(), want: SpannerNullUUID{}}, + // UUID ARRAY with []SpannerNullUUID + {desc: "decode ARRAY to []SpannerNullUUID", proto: listProto(uuidProto(uuid1), nullProto(), uuidProto(uuid2)), protoType: listType(uuidType()), want: []SpannerNullUUID{{uuid1, true}, {}, {uuid2, true}}}, + {desc: "decode NULL to []SpannerNullUUID", proto: nullProto(), protoType: listType(uuidType()), want: []SpannerNullUUID(nil)}, + {desc: "decode ARRAY to []uuid.UUID", proto: listProto(uuidProto(uuid1), uuidProto(uuid2)), protoType: listType(uuidType()), want: []uuid.UUID{uuid1, uuid2}}, + {desc: "decode ARRAY to []*uuid.UUID", proto: listProto(uuidProto(uuid1), nullProto(), uuidProto(uuid2)), protoType: listType(uuidType()), want: []*uuid.UUID{&uuid1, nil, &uuid2}}, + {desc: "decode NULL to []*uuid.UUID", proto: nullProto(), protoType: listType(uuidType()), want: []*uuid.UUID(nil)}, // STRUCT ARRAY // STRUCT schema is equal to the following Go struct: // type s struct { @@ -2090,6 +2142,7 @@ func TestGetDecodableSpannerType(t *testing.T) { type CustomTime time.Time type CustomDate civil.Date type CustomNumeric big.Rat + type CustomUUID uuid.UUID type CustomNullString NullString type CustomNullInt64 NullInt64 @@ -2099,6 +2152,7 @@ func TestGetDecodableSpannerType(t *testing.T) { type CustomNullTime NullTime type CustomNullDate NullDate type CustomNullNumeric NullNumeric + type CustomNullUUID SpannerNullUUID type StringEmbedded struct { string @@ -2120,6 +2174,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {float32(3.14), spannerTypeNonNullFloat32}, {time.Now(), spannerTypeNonNullTime}, {civil.DateOf(time.Now()), spannerTypeNonNullDate}, + {uuid1, spannerTypeNonNullUuid}, {NullString{}, spannerTypeNullString}, {NullInt64{}, spannerTypeNullInt64}, {NullBool{}, spannerTypeNullBool}, @@ -2130,6 +2185,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {*big.NewRat(1234, 1000), spannerTypeNonNullNumeric}, {big.Rat{}, spannerTypeNonNullNumeric}, {NullNumeric{}, spannerTypeNullNumeric}, + {SpannerNullUUID{}, spannerTypeNullUuid}, {[]string{"foo", "bar"}, spannerTypeArrayOfNonNullString}, {[][]byte{{1, 2, 3}, {3, 2, 1}}, spannerTypeArrayOfByteArray}, @@ -2140,6 +2196,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {[]float32{3.14}, spannerTypeArrayOfNonNullFloat32}, {[]time.Time{time.Now()}, spannerTypeArrayOfNonNullTime}, {[]civil.Date{civil.DateOf(time.Now())}, spannerTypeArrayOfNonNullDate}, + {[]uuid.UUID{uuid1}, spannerTypeArrayOfNonNullUuid}, {[]NullString{}, spannerTypeArrayOfNullString}, {[]NullInt64{}, spannerTypeArrayOfNullInt64}, {[]NullBool{}, spannerTypeArrayOfNullBool}, @@ -2150,6 +2207,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {[]big.Rat{}, spannerTypeArrayOfNonNullNumeric}, {[]big.Rat{*big.NewRat(1234, 1000), *big.NewRat(1234, 100)}, spannerTypeArrayOfNonNullNumeric}, {[]NullNumeric{}, spannerTypeArrayOfNullNumeric}, + {[]SpannerNullUUID{}, spannerTypeArrayOfNullUuid}, {CustomString("foo"), spannerTypeNonNullString}, {CustomInt64(-100), spannerTypeNonNullInt64}, @@ -2159,6 +2217,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {CustomTime(time.Now()), spannerTypeNonNullTime}, {CustomDate(civil.DateOf(time.Now())), spannerTypeNonNullDate}, {CustomNumeric(*big.NewRat(1234, 1000)), spannerTypeNonNullNumeric}, + {CustomUUID(uuid1), spannerTypeNonNullUuid}, {[]CustomString{}, spannerTypeArrayOfNonNullString}, {[]CustomInt64{}, spannerTypeArrayOfNonNullInt64}, @@ -2168,6 +2227,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {[]CustomTime{}, spannerTypeArrayOfNonNullTime}, {[]CustomDate{}, spannerTypeArrayOfNonNullDate}, {[]CustomNumeric{}, spannerTypeArrayOfNonNullNumeric}, + {[]CustomUUID{}, spannerTypeArrayOfNonNullUuid}, {CustomNullString{}, spannerTypeNullString}, {CustomNullInt64{}, spannerTypeNullInt64}, @@ -2177,6 +2237,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {CustomNullTime{}, spannerTypeNullTime}, {CustomNullDate{}, spannerTypeNullDate}, {CustomNullNumeric{}, spannerTypeNullNumeric}, + {CustomNullUUID{}, spannerTypeNullUuid}, {[]CustomNullString{}, spannerTypeArrayOfNullString}, {[]CustomNullInt64{}, spannerTypeArrayOfNullInt64}, @@ -2186,6 +2247,7 @@ func TestGetDecodableSpannerType(t *testing.T) { {[]CustomNullTime{}, spannerTypeArrayOfNullTime}, {[]CustomNullDate{}, spannerTypeArrayOfNullDate}, {[]CustomNullNumeric{}, spannerTypeArrayOfNullNumeric}, + {[]CustomNullUUID{}, spannerTypeArrayOfNullUuid}, {StringEmbedded{}, spannerTypeUnknown}, {NullStringEmbedded{}, spannerTypeUnknown}, @@ -3055,6 +3117,15 @@ func TestJSONMarshal_NullTypes(t *testing.T) { {input: NullProtoEnum{}, expect: "null"}, }, }, + { + "NullUUID", + []testcase{ + {input: SpannerNullUUID{uuid1, true}, expect: fmt.Sprintf("%q", uuid1.String())}, + {input: &SpannerNullUUID{uuid2, true}, expect: fmt.Sprintf("%q", uuid2.String())}, + {input: &SpannerNullUUID{uuid1, false}, expect: "null"}, + {input: SpannerNullUUID{}, expect: "null"}, + }, + }, } { t.Run(test.name, func(t *testing.T) { for _, tc := range test.cases { @@ -3215,6 +3286,16 @@ func TestJSONUnmarshal_NullTypes(t *testing.T) { {input: []byte(`"hello`), got: NullProtoEnum{}, isNull: true, expect: nullString, expectError: true}, }, }, + { + "NullUUID", + []testcase{ + {input: []byte(fmt.Sprintf("%q", uuid1.String())), got: SpannerNullUUID{}, isNull: false, expect: uuid1.String(), expectError: false}, + {input: []byte("null"), got: SpannerNullUUID{}, isNull: true, expect: nullString, expectError: false}, + {input: nil, got: SpannerNullUUID{}, isNull: true, expect: nullString, expectError: true}, + {input: []byte(""), got: SpannerNullUUID{}, isNull: true, expect: nullString, expectError: true}, + {input: []byte(`"hello`), got: SpannerNullUUID{}, isNull: true, expect: nullString, expectError: true}, + }, + }, } { t.Run(test.name, func(t *testing.T) { for _, tc := range test.cases { @@ -3255,6 +3336,9 @@ func TestJSONUnmarshal_NullTypes(t *testing.T) { case NullProtoEnum: err := json.Unmarshal(tc.input, &v) expectUnmarshalNullableTypes(t, err, v, tc.isNull, tc.expect, tc.expectError) + case SpannerNullUUID: + err := json.Unmarshal(tc.input, &v) + expectUnmarshalNullableTypes(t, err, v, tc.isNull, tc.expect, tc.expectError) default: t.Fatalf("Unknown type: %T", v) }