Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/integration-tests-on-emulator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ jobs:
run: go test -v
env:
SPANNER_EMULATOR_HOST: localhost:9010
- name: Run PostgreSQL integration tests on Emulator
run: go test -v
working-directory: postgresql
env:
SPANNER_EMULATOR_HOST: localhost:9010
- name: Run PostgreSQL sample application integration tests on Emulator
run: go test -v
working-directory: postgresql/samples/sample_application
env:
SPANNER_EMULATOR_HOST: localhost:9010
4 changes: 2 additions & 2 deletions .github/workflows/integration-tests-on-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Run integration tests on production
run: go test -v -timeout 45m ./... -run '^TestIntegration_'
run: go test -v -timeout 45m -run '^TestIntegration_'
env:
JOB_TYPE: test
SPANNER_TEST_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
Expand All @@ -42,7 +42,7 @@ jobs:
working-directory: ./samples/sample_application
run: |
go mod tidy
go test -v -timeout 45m ./... -run '^TestIntegration_'
go test -v -timeout 45m -run '^TestIntegration_'
env:
JOB_TYPE: test
SPANNER_TEST_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/postgresql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
name: PostgreSQL-dialect tests
jobs:
emulator-tests:
runs-on: ubuntu-latest
services:
emulator:
image: gcr.io/cloud-spanner-emulator/emulator:latest
ports:
- 9010:9010
- 9020:9020
steps:
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
- name: Checkout code
uses: actions/checkout@v6
- name: Checkout gorm
uses: actions/checkout@v6
with:
repository: go-gorm/gorm
path: postgresql/tests/gorm
- name: Run gorm PostgreSQL tests on Emulator
working-directory: postgresql/tests
run: ./run_gorm_tests.sh
env:
SPANNER_EMULATOR_HOST: localhost:9010
35 changes: 35 additions & 0 deletions .github/workflows/samples-postgresql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
on:
pull_request:
branches: [ main ]
name: Run Samples for PostgreSQL
jobs:
samples:
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v6
- name: Run Hello World
working-directory: ./postgresql/samples/helloworld
run: go run main.go
- name: Run Quickstart
working-directory: ./postgresql/samples/quickstart
run: go run main.go
- name: Run sample application
working-directory: ./postgresql/samples
run: go run run_sample.go
- name: Run snippets
working-directory: ./postgresql/samples
run: |
for filename in snippets/*.go; do
name=${filename##*/}
base=${name%.go}
echo "Running sample $base"
go run run_sample.go "$base" || ! break
done
93 changes: 65 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,83 @@
# go-gorm-spanner
# gorm Dialect for Spanner

[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/googleapis/go-gorm-spanner)

[Google Cloud Spanner](https://cloud.google.com/spanner) ORM for
Go's [GORM](https://gorm.io/) implementation.
[Google Cloud Spanner](https://cloud.google.com/spanner) ORM for Go's [GORM](https://gorm.io/) implementation.

This project contains `gorm` dialects for both Spanner GoogleSQL and Spanner PostgreSQL databases.

## GoogleSQL

This example shows how to connect `gorm` to a Spanner GoogleSQL database.

``` go
import (
"gorm.io/gorm"
_ "github.com/googleapis/go-sql-spanner"
"fmt"

spannergorm "github.com/googleapis/go-gorm-spanner"
spannergorm "github.com/googleapis/go-gorm-spanner"
_ "github.com/googleapis/go-sql-spanner"
"gorm.io/gorm"
)

db, err := gorm.Open(spannergorm.New(spannergorm.Config{
DriverName: "spanner",
DSN: "projects/PROJECT/instances/INSTANCE/databases/DATABASE",
}), &gorm.Config{PrepareStmt: true})
if err != nil {
log.Fatal(err)
func helloWorld(projectId, instanceId, databaseId string) error {
db, err := gorm.Open(spannergorm.New(spannergorm.Config{
DriverName: "spanner",
DSN: fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId),
}), &gorm.Config{PrepareStmt: true})
if err != nil {
return fmt.Errorf("failed to open database connection: %v\n", err)
}
var msg string
if err := db.Raw("SELECT 'Hello World!'").Scan(&msg).Error; err != nil {
return fmt.Errorf("failed to execute query: %v", err)
}
fmt.Println(msg)

return nil
}
```

// Print singers with more than 500 likes.
type Singer struct {
gorm.Model
Text string
Likes int
}
var singers []Singer
if err := db.Where("likes > ?", 500).Find(&singers).Error; err != nil {
log.Fatal(err)
}
for s := range singers {
fmt.Println(s.ID, s.Text)
See the [samples directory](samples) for more examples.

## PostgreSQL

This example shows how to connect `gorm` to a Spanner PostgreSQL database.

``` go
import (
"fmt"

spannerpg "github.com/googleapis/go-gorm-spanner/postgresql"
_ "github.com/googleapis/go-sql-spanner"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

func helloWorld(projectId, instanceId, databaseId string) error {
db, err := gorm.Open(spannerpg.New(postgres.Config{
DSN: fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId),
}), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to open database connection: %v\n", err)
}
var msg string
if err := db.Raw("SELECT $1::varchar as greeting", "Hello World from Spanner PostgreSQL!").Scan(&msg).Error; err != nil {
return fmt.Errorf("failed to execute query: %v", err)
}
fmt.Println(msg)

return nil
}
```

### Connection URL Properties
See the [PostgreSQL samples directory](postgresql/samples) for more examples for how to use this library with a Spanner
PostgreSQL database.

## Connection URL Properties

The Cloud Spanner GORM supports the following connection URL properties

#### Commonly Used Properties
### Commonly Used Properties
- credentials (String): File name for the credentials to use. The connection will use the default credentials of the environment if no credentials file is specified in the connection string. Example: `projects/my-project/instances/my-instance/databases/my-db;credentials=/path/to/credentials.json`
- optimizerVersion (String): Sets the default query optimizer version to use for this connection. See also https://cloud.google.com/spanner/docs/query-optimizer/query-optimizer-versions.
- isolationLevel (String): Sets the default isolation level for read/write transaction. The default is `sql.LevelSerializable`. Other supported values are `sql.LevelRepeatableRead`. Example: `fmt.Sprintf("projects/my-project/instances/my-instance/databases/my-db;isolationLevel=%s", sql.LevelRepeatableRead)`
Expand All @@ -51,15 +88,15 @@ minimum and maximum number of sessions for `gorm`.

See https://cloud.google.com/spanner/docs/sessions#multiplexed_sessions for more information about multiplexed sessions.

#### Advanced Properties
### Advanced Properties
- numChannels (int): Sets the number of gRPC channels to use. Defaults to 4.
- retryAbortsInternally (boolean): Boolean that indicates whether the connection should automatically retry aborted errors. The default is true.
- disableRouteToLeader (boolean): Boolean that indicates if all the requests of type read-write and PDML need to be routed to the leader region. The default is false.
- usePlainText (boolean): : Boolean that indicates whether the connection should use plain text communication or not. Set this to true to connect to local mock servers that do not use SSL. Example: `projects/test-project/instances/test-instance/databases/test-db;usePlainText=true`

Example: `projects/my-project/instances/my-instance/databases/my-db;numChannels=4;retryAbortsInternally=true;disableRouteToLeader=false;usePlainText=false`

#### Additional Spanner Configuration
### Additional Spanner Configuration
You can also connect `gorm` to Spanner using a `driver.Connector`. This allows you to supply additional configuration
for the Spanner client that should be used for `gorm`:

Expand Down
15 changes: 11 additions & 4 deletions array_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ func TestInsertNativeArrays(t *testing.T) {
if g, w := len(req.Params.Fields), 9; g != w {
t.Errorf("num params mismatch\n Got: %v\nWant: %v", g, w)
}
if g, w := len(req.ParamTypes), 9; g != w {
// String arrays are sent as untyped values to allow Spanner to infer the data type from the SQL statement.
if g, w := len(req.ParamTypes), 8; g != w {
t.Errorf("num param types mismatch\n Got: %v\nWant: %v", g, w)
}
wantParamTypes := []spannerpb.TypeCode{
Expand All @@ -86,13 +87,19 @@ func TestInsertNativeArrays(t *testing.T) {
spannerpb.TypeCode_INT64,
spannerpb.TypeCode_FLOAT32,
spannerpb.TypeCode_FLOAT64,
spannerpb.TypeCode_STRING,
spannerpb.TypeCode_TYPE_CODE_UNSPECIFIED,
spannerpb.TypeCode_TIMESTAMP,
}
for i, code := range wantParamTypes {
param := fmt.Sprintf("p%d", i+1)
if g, w := req.ParamTypes[param].ArrayElementType.Code, code; g != w {
t.Errorf("%s: param type mismatch\n Got: %v\nWant: %v", param, g, w)
if tp, ok := req.ParamTypes[param]; ok {
if g, w := tp.ArrayElementType.Code, code; g != w {
t.Errorf("%s: param type mismatch\n Got: %v\nWant: %v", param, g, w)
}
} else {
if g, w := spannerpb.TypeCode_TYPE_CODE_UNSPECIFIED, code; g != w {
t.Errorf("%s: param type mismatch\n Got: %v\nWant: %v", param, g, w)
}
}
}
wantValues := []string{
Expand Down
65 changes: 32 additions & 33 deletions benchmarks/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,65 @@ toolchain go1.25.5
replace github.com/googleapis/go-gorm-spanner => ../

require (
cloud.google.com/go/spanner v1.87.0
cloud.google.com/go/spanner v1.88.0
github.com/google/uuid v1.6.0
github.com/googleapis/go-gorm-spanner v1.9.0
github.com/googleapis/go-sql-spanner v1.22.0
google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b
github.com/googleapis/go-gorm-spanner v1.9.1
github.com/googleapis/go-sql-spanner v1.23.0
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409
google.golang.org/grpc v1.78.0
gorm.io/datatypes v1.2.7
gorm.io/gorm v1.31.1
)

require (
cel.dev/expr v0.24.0 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/longrunning v0.7.0 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/api v0.266.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
)
Loading
Loading