From 7d6ee5f2ee20fd6ad2cdcb64ffa63288f4c2727a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 20 May 2025 14:05:18 +0200 Subject: [PATCH 1/2] feat: uuid data type Add support for the UUID data type. --- examples/data-types/main.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/data-types/main.go b/examples/data-types/main.go index 8a74448e..eced9532 100644 --- a/examples/data-types/main.go +++ b/examples/data-types/main.go @@ -18,6 +18,7 @@ import ( "context" "database/sql" "fmt" + "github.com/google/uuid" "math/big" "time" @@ -38,6 +39,7 @@ var createTableStatement = `CREATE TABLE AllTypes ( numeric NUMERIC, date DATE, timestamp TIMESTAMP, + col_uuid UUID, boolArray ARRAY, stringArray ARRAY, bytesArray ARRAY, @@ -47,6 +49,7 @@ var createTableStatement = `CREATE TABLE AllTypes ( numericArray ARRAY, dateArray ARRAY, timestampArray ARRAY, + uuidArray ARRAY, ) PRIMARY KEY (key)` // Sample showing how to work with the different data types that are supported by Cloud Spanner: @@ -68,15 +71,16 @@ func dataTypes(projectId, instanceId, databaseId string) error { // Insert a test row with all non-null values using DML and native types. if _, err := db.ExecContext(ctx, `INSERT INTO AllTypes ( - key, bool, string, bytes, int64, float32, float64, numeric, date, timestamp, - boolArray, stringArray, bytesArray, int64Array, float32Array, float64Array, numericArray, dateArray, timestampArray) - VALUES (@key, @bool, @string, @bytes, @int64, @float32, @float64, @numeric, @date, @timestamp, - @boolArray, @stringArray, @bytesArray, @int64Array, @float32Array, @float64Array, @numericArray, @dateArray, @timestampArray)`, - 1, true, "string", []byte("bytes"), 100, float32(3.14), 3.14, *big.NewRat(1, 1), civil.DateOf(time.Now()), time.Now(), + key, bool, string, bytes, int64, float32, float64, numeric, date, timestamp, col_uuid, + boolArray, stringArray, bytesArray, int64Array, float32Array, float64Array, numericArray, dateArray, timestampArray, uuidArray) + VALUES (@key, @bool, @string, @bytes, @int64, @float32, @float64, @numeric, @date, @timestamp, @uuid, + @boolArray, @stringArray, @bytesArray, @int64Array, @float32Array, @float64Array, @numericArray, @dateArray, @timestampArray, @uuidArray)`, + 1, true, "string", []byte("bytes"), 100, float32(3.14), 3.14, *big.NewRat(1, 1), civil.DateOf(time.Now()), time.Now(), uuid.New(), []bool{true, false}, []string{"s1", "s2"}, [][]byte{[]byte("b1"), []byte("b2")}, []int64{1, 2}, []float32{1.1, 2.2}, []float64{1.1, 2.2}, []big.Rat{*big.NewRat(1, 2), *big.NewRat(1, 3)}, []civil.Date{{Year: 2021, Month: 10, Day: 12}, {Year: 2021, Month: 10, Day: 13}}, - []time.Time{time.Now(), time.Now().Add(24 * time.Hour)}); err != nil { + []time.Time{time.Now(), time.Now().Add(24 * time.Hour)}, + []uuid.UUID{uuid.New(), uuid.New()}); err != nil { return fmt.Errorf("failed to insert a record with all non-null values using DML: %v", err) } fmt.Print("Inserted a test record with all non-null values\n") From 74dfa13d3787995a81c01af07445028a4bebcac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 5 Mar 2026 11:35:36 +0100 Subject: [PATCH 2/2] chore: add uuid to scanned columns --- examples/data-types/main.go | 20 +++++++++++++------- examples/emulator_runner.go | 27 +++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/examples/data-types/main.go b/examples/data-types/main.go index eced9532..3de48c4a 100644 --- a/examples/data-types/main.go +++ b/examples/data-types/main.go @@ -18,12 +18,12 @@ import ( "context" "database/sql" "fmt" - "github.com/google/uuid" "math/big" "time" "cloud.google.com/go/civil" "cloud.google.com/go/spanner" + "github.com/google/uuid" spannerdriver "github.com/googleapis/go-sql-spanner" "github.com/googleapis/go-sql-spanner/examples" ) @@ -125,8 +125,8 @@ func dataTypes(projectId, instanceId, databaseId string) error { "SELECT * FROM AllTypes WHERE key=@key", spannerdriver.ExecOptions{DecodeToNativeArrays: true}, 1).Scan( - &r1.key, &r1.bool, &r1.string, &r1.bytes, &r1.int64, &r1.float32, &r1.float64, &r1.numeric, &r1.date, &r1.timestamp, - &r1.boolArray, &r1.stringArray, &r1.bytesArray, &r1.int64Array, &r1.float32Array, &r1.float64Array, &r1.numericArray, &r1.dateArray, &r1.timestampArray, + &r1.key, &r1.bool, &r1.string, &r1.bytes, &r1.int64, &r1.float32, &r1.float64, &r1.numeric, &r1.date, &r1.timestamp, &r1.uuid, + &r1.boolArray, &r1.stringArray, &r1.bytesArray, &r1.int64Array, &r1.float32Array, &r1.float64Array, &r1.numericArray, &r1.dateArray, &r1.timestampArray, &r1.uuidArray, ); err != nil { return fmt.Errorf("failed to get row with non-null values: %v", err) } @@ -135,8 +135,8 @@ func dataTypes(projectId, instanceId, databaseId string) error { // You can also use the spanner.Null* types to get data. These types can store both non-null and null values. var r2 nullTypes if err := db.QueryRowContext(ctx, "SELECT * FROM AllTypes WHERE key=@key", 1).Scan( - &r2.key, &r2.bool, &r2.string, &r2.bytes, &r2.int64, &r2.float32, &r2.float64, &r2.numeric, &r2.date, &r2.timestamp, - &r2.boolArray, &r2.stringArray, &r2.bytesArray, &r2.int64Array, &r2.float32Array, &r2.float64Array, &r2.numericArray, &r2.dateArray, &r2.timestampArray, + &r2.key, &r2.bool, &r2.string, &r2.bytes, &r2.int64, &r2.float32, &r2.float64, &r2.numeric, &r2.date, &r2.timestamp, &r2.uuid, + &r2.boolArray, &r2.stringArray, &r2.bytesArray, &r2.int64Array, &r2.float32Array, &r2.float64Array, &r2.numericArray, &r2.dateArray, &r2.timestampArray, &r2.uuidArray, ); err != nil { return fmt.Errorf("failed to get row with null values: %v", err) } @@ -147,8 +147,8 @@ func dataTypes(projectId, instanceId, databaseId string) error { // use spanner.NullNumeric and spanner.NullDate. var r3 sqlNullTypes if err := db.QueryRowContext(ctx, "SELECT * FROM AllTypes WHERE key=@key", 1).Scan( - &r3.key, &r3.bool, &r3.string, &r3.bytes, &r3.int64, &r3.float32, &r3.float64, &r3.numeric, &r3.date, &r3.timestamp, - &r3.boolArray, &r3.stringArray, &r3.bytesArray, &r3.int64Array, &r3.float32Array, &r3.float64Array, &r3.numericArray, &r3.dateArray, &r3.timestampArray, + &r3.key, &r3.bool, &r3.string, &r3.bytes, &r3.int64, &r3.float32, &r3.float64, &r3.numeric, &r3.date, &r3.timestamp, &r3.uuid, + &r3.boolArray, &r3.stringArray, &r3.bytesArray, &r3.int64Array, &r3.float32Array, &r3.float64Array, &r3.numericArray, &r3.dateArray, &r3.timestampArray, &r3.uuidArray, ); err != nil { return fmt.Errorf("failed to get row with null values using Go sql null types: %v", err) } @@ -168,6 +168,7 @@ type nativeTypes struct { numeric big.Rat date civil.Date timestamp time.Time + uuid uuid.UUID // Array types use Null* types by default, because an array may always // contain NULL elements in the array itself (even if the ARRAY column is // defined as NOT NULL). The Spanner database/sql driver can also return @@ -183,6 +184,7 @@ type nativeTypes struct { numericArray []spanner.NullNumeric dateArray []civil.Date timestampArray []time.Time + uuidArray []uuid.UUID } type nullTypes struct { @@ -196,6 +198,7 @@ type nullTypes struct { numeric spanner.NullNumeric date spanner.NullDate timestamp spanner.NullTime + uuid spanner.NullUUID boolArray []spanner.NullBool stringArray []spanner.NullString bytesArray [][]byte @@ -205,6 +208,7 @@ type nullTypes struct { numericArray []spanner.NullNumeric dateArray []spanner.NullDate timestampArray []spanner.NullTime + uuidArray []uuid.UUID } type sqlNullTypes struct { @@ -218,6 +222,7 @@ type sqlNullTypes struct { numeric spanner.NullNumeric // There is no sql.NullNumeric type date spanner.NullDate // There is no sql.NullDate type timestamp sql.NullTime + uuid spanner.NullUUID // There is no sql.NullUUID type // Array types must always use the spanner.Null* structs. boolArray []spanner.NullBool stringArray []spanner.NullString @@ -228,6 +233,7 @@ type sqlNullTypes struct { numericArray []spanner.NullNumeric dateArray []spanner.NullDate timestampArray []spanner.NullTime + uuidArray []uuid.UUID } func main() { diff --git a/examples/emulator_runner.go b/examples/emulator_runner.go index 59b5ce01..df6ad529 100644 --- a/examples/emulator_runner.go +++ b/examples/emulator_runner.go @@ -22,14 +22,16 @@ import ( "os" "time" + "cloud.google.com/go/spanner" database "cloud.google.com/go/spanner/admin/database/apiv1" - databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" + "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instance "cloud.google.com/go/spanner/admin/instance/apiv1" - instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" + "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" + "google.golang.org/grpc/codes" ) var cli *client.Client @@ -69,6 +71,10 @@ func RunSampleOnEmulatorWithDialect(sample func(string, string, string) error, d } func startEmulator() error { + if os.Getenv("SPANNER_EMULATOR_HOST") != "" { + return nil + } + ctx := context.Background() if err := os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010"); err != nil { return err @@ -130,7 +136,7 @@ func createInstance(projectId, instanceId string) error { if err != nil { return err } - defer instanceAdmin.Close() + defer silentClose(instanceAdmin) op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ Parent: fmt.Sprintf("projects/%s", projectId), InstanceId: instanceId, @@ -141,6 +147,9 @@ func createInstance(projectId, instanceId string) error { }, }) if err != nil { + if spanner.ErrCode(err) == codes.AlreadyExists { + return nil + } return fmt.Errorf("could not create instance %s: %v", fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), err) } // Wait for the instance creation to finish. @@ -156,7 +165,13 @@ func createSampleDB(projectId, instanceId, databaseId string, dialect databasepb if err != nil { return err } - defer databaseAdminClient.Close() + defer silentClose(databaseAdminClient) + + // Drop the database if it already exists. + _ = databaseAdminClient.DropDatabase(ctx, &databasepb.DropDatabaseRequest{ + Database: fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId), + }) + var createStatement string if dialect == databasepb.DatabaseDialect_POSTGRESQL { createStatement = fmt.Sprintf(`CREATE DATABASE "%s"`, databaseId) @@ -189,3 +204,7 @@ func stopEmulator() { log.Printf("failed to stop emulator: %v\n", err) } } + +func silentClose(c io.Closer) { + _ = c.Close() +}