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
20 changes: 20 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ type ConnectorConfig struct {
Instance string
Database string

// AutoConfigEmulator automatically creates a connection for the emulator
// and also automatically creates the Instance and Database on the emulator.
// Setting this option to true will:
// 1. Set the SPANNER_EMULATOR_HOST environment variable to either Host or
// 'localhost:9010' if no other host has been set.
// 2. Use plain text communication and NoCredentials.
// 3. Automatically create the Instance and the Database on the emulator if
// any of those do not yet exist.
AutoConfigEmulator bool

// Params contains key/value pairs for commonly used configuration parameters
// for connections. The valid values are the same as the parameters that can
// be added to a connection string.
Expand Down Expand Up @@ -448,6 +458,11 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
connectorConfig.DecodeToNativeArrays = val
}
}
if strval, ok := connectorConfig.Params[strings.ToLower("AutoConfigEmulator")]; ok {
if val, err := strconv.ParseBool(strval); err == nil {
connectorConfig.AutoConfigEmulator = val
}
}
config.UserAgent = userAgent
var logger *slog.Logger
if connectorConfig.logger == nil {
Expand All @@ -464,6 +479,11 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
if connectorConfig.Configurator != nil {
connectorConfig.Configurator(&config, &opts)
}
if connectorConfig.AutoConfigEmulator {
if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database); err != nil {
return nil, err
}
}

c := &connector{
driver: d,
Expand Down
95 changes: 95 additions & 0 deletions emulator_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 spannerdriver

import (
"context"
"fmt"
"os"

"cloud.google.com/go/spanner"
database "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"
"google.golang.org/grpc/codes"
)

func autoConfigEmulator(ctx context.Context, host, project, instance, database string) error {
if host == "" {
host = "localhost:9010"
}
if err := os.Setenv("SPANNER_EMULATOR_HOST", host); err != nil {
return err
}
if err := createInstance(project, instance); err != nil {
if spanner.ErrCode(err) != codes.AlreadyExists {
return err
}
}
if err := createDatabase(project, instance, database); err != nil {
if spanner.ErrCode(err) != codes.AlreadyExists {
return err
}
}
return nil
}

func createInstance(projectId, instanceId string) error {
ctx := context.Background()
instanceAdmin, err := instance.NewInstanceAdminClient(ctx)
if err != nil {
return err
}
defer instanceAdmin.Close()
op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{
Parent: fmt.Sprintf("projects/%s", projectId),
InstanceId: instanceId,
Instance: &instancepb.Instance{
Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectId, "emulator-config"),
DisplayName: instanceId,
NodeCount: 1,
},
})
if err != 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.
if _, err := op.Wait(ctx); err != nil {
return fmt.Errorf("waiting for instance creation to finish failed: %v", err)
}
return nil
}

func createDatabase(projectId, instanceId, databaseId string) error {
ctx := context.Background()
databaseAdminClient, err := database.NewDatabaseAdminClient(ctx)
if err != nil {
return err
}
defer databaseAdminClient.Close()
opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId),
CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId),
})
if err != nil {
return err
}
// Wait for the database creation to finish.
if _, err := opDB.Wait(ctx); err != nil {
return fmt.Errorf("waiting for database creation to finish failed: %v", err)
}
return nil
}
121 changes: 121 additions & 0 deletions examples/emulator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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"
"log"

"github.com/docker/docker/api/types/container"
spannerdriver "github.com/googleapis/go-sql-spanner"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

// Sample application that shows how to use the Spanner Go sql driver to connect
// to the Spanner Emulator and automatically create the instance and database
// from the connection string on the Emulator.
//
// Execute the sample with the command `go run main.go` from this directory.
func emulator(projectId, instanceId, databaseId string) error {
ctx := context.Background()

// Start the Spanner emulator in a Docker container.
emulator, host, err := startEmulator()
if err != nil {
return err
}
defer func() { _ = emulator.Terminate(context.Background()) }()

config := spannerdriver.ConnectorConfig{
// AutoConfigEmulator instructs the driver to:
// 1. Connect to the emulator using plain text.
// 2. Create the instance and database if they do not already exist.
AutoConfigEmulator: true,

// You only have to set the host if it is different from the default
// 'localhost:9010' host for the Spanner emulator.
Host: host,

// The instance and database will automatically be created on the Emulator.
Project: projectId,
Instance: instanceId,
Database: databaseId,
}
connector, err := spannerdriver.CreateConnector(config)
if err != nil {
return err
}
db := sql.OpenDB(connector)
defer func() { _ = db.Close() }()

rows, err := db.QueryContext(ctx, "SELECT 'Hello World!'")
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 iterate over query results: %v", err)
}
return nil
}

func main() {
if err := emulator("emulator-project", "test-instance", "test-database"); err != nil {
log.Fatal(err)
}
}

// startEmulator starts the Spanner Emulator in a Docker container.
func startEmulator() (testcontainers.Container, string, error) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
AlwaysPullImage: true,
Image: "gcr.io/cloud-spanner-emulator/emulator",
ExposedPorts: []string{"9010/tcp"},
WaitingFor: wait.ForAll(wait.ForListeningPort("9010/tcp"), wait.ForLog("gRPC server listening")),
HostConfigModifier: func(hostConfig *container.HostConfig) {
hostConfig.AutoRemove = true
},
}
emulator, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return emulator, "", fmt.Errorf("failed to start the emulator: %v", err)
}
host, err := emulator.Host(ctx)
if err != nil {
return emulator, "", fmt.Errorf("failed to get host: %v", err)
}
mappedPort, err := emulator.MappedPort(ctx, "9010/tcp")
if err != nil {
return emulator, "", fmt.Errorf("failed to get mapped port: %v", err)
}
port := mappedPort.Int()

return emulator, fmt.Sprintf("%s:%v", host, port), nil
}
4 changes: 2 additions & 2 deletions examples/emulator_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import (
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"
"github.com/docker/docker/api/types"
"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"
)
Expand Down Expand Up @@ -77,7 +77,7 @@ func startEmulator() error {
return err
}
// Pull the Spanner Emulator docker image.
reader, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", types.ImagePullOptions{})
reader, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", image.PullOptions{})
if err != nil {
return err
}
Expand Down
39 changes: 31 additions & 8 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ replace github.com/googleapis/go-sql-spanner => ../
require (
cloud.google.com/go v0.119.0
cloud.google.com/go/spanner v1.77.0
github.com/docker/docker v25.0.6+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/docker v27.1.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/googleapis/go-sql-spanner v1.0.1
github.com/testcontainers/testcontainers-go v0.35.0
google.golang.org/api v0.226.0
)

Expand All @@ -23,31 +24,55 @@ require (
cloud.google.com/go/iam v1.4.0 // indirect
cloud.google.com/go/longrunning v0.6.6 // indirect
cloud.google.com/go/monitoring v1.24.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
Expand All @@ -60,18 +85,16 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gotest.tools/v3 v3.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading