Skip to content

Commit bbfeb3f

Browse files
I82Muchrahul2393sakthivelmanii
authored
fix(spanner/spannertest): add support for UUID column type (#20164)
Fixes #20163 --------- Co-authored-by: rahul2393 <irahul@google.com> Co-authored-by: Sakthivel Subramanian <179120858+sakthivelmanii@users.noreply.github.com>
1 parent 8e9f00f commit bbfeb3f

5 files changed

Lines changed: 198 additions & 0 deletions

File tree

spanner/spannertest/db.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

3939
"cloud.google.com/go/civil"
4040
"cloud.google.com/go/spanner/spansql"
41+
"github.com/google/uuid"
4142
)
4243

4344
type database struct {
@@ -1137,6 +1138,19 @@ func valForType(v *structpb.Value, t spansql.Type) (interface{}, error) {
11371138
}
11381139
return t, nil
11391140
}
1141+
case spansql.UUID:
1142+
sv, ok := v.Kind.(*structpb.Value_StringValue)
1143+
if ok {
1144+
s := sv.StringValue
1145+
if len(s) != 36 {
1146+
return nil, fmt.Errorf("invalid UUID %q", s)
1147+
}
1148+
id, err := uuid.Parse(s)
1149+
if err != nil {
1150+
return nil, fmt.Errorf("invalid UUID %q: %w", s, err)
1151+
}
1152+
return id.String(), nil
1153+
}
11401154
}
11411155
return nil, fmt.Errorf("unsupported inserting value kind %T into column of type %s", v.Kind, t.SQL())
11421156
}

spanner/spannertest/db_eval.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"cloud.google.com/go/civil"
3131
"cloud.google.com/go/spanner/spansql"
32+
"github.com/google/uuid"
3233
"google.golang.org/grpc/codes"
3334
"google.golang.org/grpc/status"
3435
)
@@ -665,6 +666,18 @@ func (ec evalContext) coerceString(target spansql.Expr, slit spansql.StringLiter
665666
val: t,
666667
orig: slit,
667668
}, nil
669+
case spansql.UUID:
670+
if len(slit) != 36 {
671+
return nil, fmt.Errorf("coercing string literal %q to UUID: invalid UUID length", slit)
672+
}
673+
id, err := uuid.Parse(string(slit))
674+
if err != nil {
675+
return nil, fmt.Errorf("coercing string literal %q to UUID: %w", slit, err)
676+
}
677+
return coercedValue{
678+
val: id.String(),
679+
orig: slit,
680+
}, nil
668681
}
669682

670683
// TODO: Any others?

spanner/spannertest/db_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,3 +775,107 @@ func TestCreateAndManageChangeStream(t *testing.T) {
775775
t.Errorf("Generated SQL statement incorrect.\n got %v\nwant %v", got, want)
776776
}
777777
}
778+
779+
func TestUUIDColumn(t *testing.T) {
780+
uuidTable := &spansql.CreateTable{
781+
Name: "Tasks",
782+
Columns: []spansql.ColumnDef{
783+
{Name: "TaskId", Type: spansql.Type{Base: spansql.UUID}, NotNull: true},
784+
{Name: "Data", Type: spansql.Type{Base: spansql.String}},
785+
},
786+
PrimaryKey: []spansql.KeyPart{{Column: "TaskId"}},
787+
}
788+
789+
var db database
790+
if st := db.ApplyDDL(uuidTable); st.Code() != codes.OK {
791+
t.Fatalf("Creating table: %v", st.Err())
792+
}
793+
794+
tx := db.NewTransaction()
795+
tx.Start()
796+
uuidVal := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
797+
upperUUIDVal := "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A22"
798+
normalizedUpperUUIDVal := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22"
799+
800+
err := db.Insert(tx, "Tasks", []spansql.ID{"TaskId", "Data"}, []*structpb.ListValue{
801+
listV(stringV(uuidVal), stringV("some-data")),
802+
listV(stringV(upperUUIDVal), stringV("upper-data")),
803+
})
804+
if err != nil {
805+
t.Fatalf("Inserting data: %v", err)
806+
}
807+
if _, err := tx.Commit(); err != nil {
808+
t.Fatalf("Committing changes: %v", err)
809+
}
810+
811+
queryTests := []struct {
812+
name string
813+
sql string
814+
want [][]interface{}
815+
}{
816+
{
817+
name: "all rows",
818+
sql: `SELECT * FROM Tasks`,
819+
want: [][]interface{}{
820+
{uuidVal, "some-data"},
821+
{normalizedUpperUUIDVal, "upper-data"},
822+
},
823+
},
824+
{
825+
name: "uppercase literal filter for uppercase-inserted row",
826+
sql: `SELECT * FROM Tasks WHERE TaskId = 'A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A22'`,
827+
want: [][]interface{}{
828+
{normalizedUpperUUIDVal, "upper-data"},
829+
},
830+
},
831+
{
832+
name: "lowercase literal filter for uppercase-inserted row",
833+
sql: `SELECT * FROM Tasks WHERE TaskId = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22'`,
834+
want: [][]interface{}{
835+
{normalizedUpperUUIDVal, "upper-data"},
836+
},
837+
},
838+
{
839+
name: "uppercase literal filter for lowercase-inserted row",
840+
sql: `SELECT * FROM Tasks WHERE TaskId = 'A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'`,
841+
want: [][]interface{}{
842+
{uuidVal, "some-data"},
843+
},
844+
},
845+
}
846+
847+
for _, tc := range queryTests {
848+
q, err := spansql.ParseQuery(tc.sql)
849+
if err != nil {
850+
t.Fatalf("ParseQuery(%s) [%s]: %v", tc.sql, tc.name, err)
851+
}
852+
ri, err := db.Query(q, nil)
853+
if err != nil {
854+
t.Fatalf("Query(%s) [%s]: %v", tc.sql, tc.name, err)
855+
}
856+
got := slurp(t, ri)
857+
if !reflect.DeepEqual(got, tc.want) {
858+
t.Errorf("[%s] Query(%s) got %v, want %v", tc.name, tc.sql, got, tc.want)
859+
}
860+
}
861+
862+
// Test invalid UUID insertions
863+
invalidUUIDs := []string{
864+
"not-a-uuid",
865+
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a1", // too short (35 chars)
866+
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a111", // too long (37 chars)
867+
"a0eebc999c0b4ef8bb6d6bb9bd380a11", // missing hyphens
868+
"g0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", // non-hex char
869+
}
870+
for _, badUUID := range invalidUUIDs {
871+
txBad := db.NewTransaction()
872+
txBad.Start()
873+
errBad := db.Insert(txBad, "Tasks", []spansql.ID{"TaskId", "Data"}, []*structpb.ListValue{
874+
listV(stringV(badUUID), stringV("bad")),
875+
})
876+
if errBad == nil {
877+
t.Errorf("db.Insert succeeded for invalid UUID %q, want error", badUUID)
878+
}
879+
txBad.Rollback()
880+
}
881+
}

spanner/spannertest/inmem.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,8 @@ func spannerTypeFromType(typ spansql.Type) (*spannerpb.Type, error) {
957957
code = spannerpb.TypeCode_DATE
958958
case spansql.Timestamp:
959959
code = spannerpb.TypeCode_TIMESTAMP
960+
case spansql.UUID:
961+
code = spannerpb.TypeCode_UUID
960962
}
961963
st := &spannerpb.Type{Code: code}
962964
if typ.Array {

spanner/spannertest/integration_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,69 @@ func TestIntegration_RowDeletionPolicy(t *testing.T) {
16141614
}
16151615
}
16161616

1617+
func TestIntegration_UUID(t *testing.T) {
1618+
client, adminClient, _, cleanup := makeClient(t)
1619+
defer cleanup()
1620+
1621+
ctx := context.Background()
1622+
1623+
err := updateDDL(t, adminClient,
1624+
`CREATE TABLE UUIDTable (
1625+
TaskId UUID NOT NULL,
1626+
Data STRING(MAX),
1627+
) PRIMARY KEY (TaskId)`)
1628+
if err != nil {
1629+
t.Fatalf("updateDDL failed: %v", err)
1630+
}
1631+
1632+
taskID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
1633+
upperTaskID := "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A22"
1634+
normalizedUpperTaskID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22"
1635+
1636+
_, err = client.Apply(ctx, []*spanner.Mutation{
1637+
spanner.Insert("UUIDTable", []string{"TaskId", "Data"}, []interface{}{taskID, "payload"}),
1638+
spanner.Insert("UUIDTable", []string{"TaskId", "Data"}, []interface{}{upperTaskID, "upper-payload"}),
1639+
})
1640+
if err != nil {
1641+
t.Fatalf("client.Apply failed: %v", err)
1642+
}
1643+
1644+
readTests := []struct {
1645+
name string
1646+
key spanner.Key
1647+
wantUUID string
1648+
wantData string
1649+
}{
1650+
{name: "exact match", key: spanner.Key{taskID}, wantUUID: taskID, wantData: "payload"},
1651+
{name: "lowercase key for uppercase insert", key: spanner.Key{normalizedUpperTaskID}, wantUUID: normalizedUpperTaskID, wantData: "upper-payload"},
1652+
{name: "uppercase key for uppercase insert", key: spanner.Key{upperTaskID}, wantUUID: normalizedUpperTaskID, wantData: "upper-payload"},
1653+
}
1654+
1655+
for _, tc := range readTests {
1656+
row, err := client.Single().ReadRow(ctx, "UUIDTable", tc.key, []string{"TaskId", "Data"})
1657+
if err != nil {
1658+
t.Fatalf("[%s] client.Single().ReadRow failed: %v", tc.name, err)
1659+
}
1660+
var gotTaskID spanner.NullUUID
1661+
var gotData string
1662+
if err := row.Columns(&gotTaskID, &gotData); err != nil {
1663+
t.Fatalf("[%s] row.Columns failed: %v", tc.name, err)
1664+
}
1665+
if !gotTaskID.Valid || gotTaskID.UUID.String() != tc.wantUUID || gotData != tc.wantData {
1666+
t.Errorf("[%s] Read row mismatch: got (%v, %q), want (%q, %q)", tc.name, gotTaskID, gotData, tc.wantUUID, tc.wantData)
1667+
}
1668+
}
1669+
1670+
// Verify invalid UUID is rejected on Apply
1671+
invalidTaskID := "invalid-uuid-string"
1672+
_, err = client.Apply(ctx, []*spanner.Mutation{
1673+
spanner.Insert("UUIDTable", []string{"TaskId", "Data"}, []interface{}{invalidTaskID, "invalid-payload"}),
1674+
})
1675+
if err == nil {
1676+
t.Errorf("client.Apply succeeded for invalid UUID %q, want error", invalidTaskID)
1677+
}
1678+
}
1679+
16171680
func dropTable(t *testing.T, adminClient *dbadmin.DatabaseAdminClient, table string) error {
16181681
t.Helper()
16191682
err := updateDDL(t, adminClient, "DROP TABLE "+table)
@@ -1700,6 +1763,8 @@ func genericValue(t *testing.T, gcv spanner.GenericColumnValue) interface{} {
17001763
dst = new(string)
17011764
case spannerpb.TypeCode_BYTES:
17021765
dst = new([]byte)
1766+
case spannerpb.TypeCode_UUID:
1767+
dst = new(string)
17031768
}
17041769
if dst == nil {
17051770
t.Fatalf("Can't decode Spanner generic column value: %v", gcv.Type)

0 commit comments

Comments
 (0)