diff --git a/README.md b/README.md index f9f8e571..189bbcc1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [Google Cloud Spanner](https://cloud.google.com/spanner) driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package. +This driver can be used with both Spanner GoogleSQL and Spanner PostgreSQL databases. + ```go import _ "github.com/googleapis/go-sql-spanner" @@ -13,8 +15,8 @@ if err != nil { log.Fatal(err) } -// Print tweets with more than 500 likes. -rows, err := db.QueryContext(ctx, "SELECT id, text FROM tweets WHERE likes > @likes", sql.Named("likes", 500)) +// Print singers with more than 500 likes. +rows, err := db.QueryContext(ctx, "SELECT id, name FROM singers WHERE likes > ?", 500) if err != nil { log.Fatal(err) } @@ -55,6 +57,17 @@ db.ExecContext(ctx, "INSERT INTO tweets (id, text, rts) VALUES (@id, @text, @rts sql.Named("id", id), sql.Named("text", text), sql.Named("rts", 10000)) ``` +### Using PostgreSQL-style positional arguments + +When connected to a PostgreSQL-dialect Spanner database, you can also use PostgreSQL-style +query parameters `$1, $2, ..., $n`. These query parameters are handled as positional parameters. + +```go +db.QueryContext(ctx, "SELECT id, text FROM tweets WHERE likes > $1", 500) + +db.ExecContext(ctx, "INSERT INTO tweets (id, text, rts) VALUES ($1, $2, $3)", id, text, 10000) +``` + ### Using named parameters with positional arguments (not recommended) Named parameters can also be used in combination with positional arguments, but this is __not recommended__, as the behavior can be hard to predict if @@ -215,10 +228,19 @@ $ export SPANNER_EMULATOR_HOST=localhost:9010 ## Spanner PostgreSQL Interface -This driver only works with the Spanner GoogleSQL dialect. For the -Spanner PostgreSQL dialect, any PostgreSQL driver that implements the -[database/sql](https://golang.org/pkg/database/sql/) interface can be used -in combination with +This driver works with both Spanner GoogleSQL and PostgreSQL dialects. +The driver automatically detects the dialect of the database that it is connected to. + +Note that the types of query parameters that are supported depends on the dialect of the +database that the driver is connected to: +1. Positional parameters (`?`): Supported for both dialects. +2. Named parameters (`@id`, `@name`, ...): Supported for both dialects. +3. PostgreSQL-style positional parameters (`$1`, `$2`, ...): Only supported for PostgreSQL databases. + +### Alternatives + +You can also use Spanner PostgreSQL dialect databases with any off-the-shelf PostgreSQL driver that +implements the [database/sql](https://golang.org/pkg/database/sql/) interface in combination with [PGAdapter](https://cloud.google.com/spanner/docs/pgadapter). For example, the [pgx](https://github.com/jackc/pgx) driver can be used in combination with diff --git a/examples/emulator_runner.go b/examples/emulator_runner.go index 51b4570e..59b5ce01 100644 --- a/examples/emulator_runner.go +++ b/examples/emulator_runner.go @@ -44,6 +44,10 @@ var containerId string // 3. Execute the sample function against the emulator. // 4. Stop the Docker container with the emulator. func RunSampleOnEmulator(sample func(string, string, string) error, ddlStatements ...string) { + RunSampleOnEmulatorWithDialect(sample, databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, ddlStatements...) +} + +func RunSampleOnEmulatorWithDialect(sample func(string, string, string) error, dialect databasepb.DatabaseDialect, ddlStatements ...string) { var err error if err = startEmulator(); err != nil { log.Fatalf("failed to start emulator: %v", err) @@ -53,7 +57,7 @@ func RunSampleOnEmulator(sample func(string, string, string) error, ddlStatement stopEmulator() log.Fatalf("failed to create instance on emulator: %v", err) } - if err = createSampleDB(projectId, instanceId, databaseId, ddlStatements...); err != nil { + if err = createSampleDB(projectId, instanceId, databaseId, dialect, ddlStatements...); err != nil { stopEmulator() log.Fatalf("failed to create database on emulator: %v", err) } @@ -146,17 +150,24 @@ func createInstance(projectId, instanceId string) error { return nil } -func createSampleDB(projectId, instanceId, databaseId string, statements ...string) error { +func createSampleDB(projectId, instanceId, databaseId string, dialect databasepb.DatabaseDialect, statements ...string) error { ctx := context.Background() databaseAdminClient, err := database.NewDatabaseAdminClient(ctx) if err != nil { return err } defer databaseAdminClient.Close() + var createStatement string + if dialect == databasepb.DatabaseDialect_POSTGRESQL { + createStatement = fmt.Sprintf(`CREATE DATABASE "%s"`, databaseId) + } else { + createStatement = fmt.Sprintf("CREATE DATABASE `%s`", databaseId) + } opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), - CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId), + CreateStatement: createStatement, ExtraStatements: statements, + DatabaseDialect: dialect, }) if err != nil { return err diff --git a/examples/postgresql/main.go b/examples/postgresql/main.go new file mode 100644 index 00000000..5a0a274b --- /dev/null +++ b/examples/postgresql/main.go @@ -0,0 +1,59 @@ +// Copyright 2025 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "database/sql" + "fmt" + + "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" + _ "github.com/googleapis/go-sql-spanner" + "github.com/googleapis/go-sql-spanner/examples" +) + +// Sample application that uses a PostgreSQL-dialect Spanner database. +// +// Execute the sample with the command `go run main.go` from this directory. +func helloWorldFromPostgreSQL(projectId, instanceId, databaseId string) error { + ctx := context.Background() + db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) + if err != nil { + return fmt.Errorf("failed to open database connection: %v", err) + } + defer db.Close() + + rows, err := db.QueryContext(ctx, "SELECT $1::varchar as message", "Hello World from Spanner PostgreSQL") + if err != nil { + return fmt.Errorf("failed to execute query: %v", err) + } + defer rows.Close() + + var msg string + for rows.Next() { + if err := rows.Scan(&msg); err != nil { + return fmt.Errorf("failed to scan row values: %v", err) + } + fmt.Printf("%s\n", msg) + } + if err := rows.Err(); err != nil { + return fmt.Errorf("failed to execute query: %v", err) + } + return nil +} + +func main() { + examples.RunSampleOnEmulatorWithDialect(helloWorldFromPostgreSQL, databasepb.DatabaseDialect_POSTGRESQL) +}