├── .github ├── CODEOWNERS ├── release-please.yml ├── release-trigger.yml └── workflows │ ├── integration-tests-on-emulator.yml │ ├── integration-tests-on-production.yml │ ├── samples.yml │ ├── snippets.yml │ └── unit-tests.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── aborted_transactions_test.go ├── auto_dml_batch_test.go ├── batch.go ├── benchmarks ├── benchmark_test.go ├── benchwrapper │ ├── README.md │ ├── main.go │ └── proto │ │ ├── README.md │ │ ├── gen.go │ │ ├── spanner.pb.go │ │ ├── spanner.proto │ │ └── spanner_grpc.pb.go ├── go.mod └── go.sum ├── checksum_row_iterator.go ├── checksum_row_iterator_test.go ├── client_side_statement.go ├── client_side_statement_test.go ├── client_side_statements_json.go ├── conn.go ├── conn_with_mockserver_test.go ├── connection_leak_test.go ├── docs └── limitations.rst ├── driver.go ├── driver_test.go ├── driver_with_mockserver_test.go ├── emulator_util.go ├── examples ├── README.md ├── auto-partition-queries │ └── main.go ├── commit-timestamp │ └── main.go ├── connect │ ├── connect.go │ └── main.go ├── custom-client-configuration │ └── main.go ├── data-types │ └── main.go ├── ddl-batches │ └── main.go ├── decode-options │ └── main.go ├── directed-reads │ └── main.go ├── dml-batches │ └── main.go ├── emulator │ └── main.go ├── emulator_runner.go ├── go.mod ├── go.sum ├── helloworld │ └── main.go ├── last-insert-id │ └── main.go ├── mutations │ └── main.go ├── partitioned-dml │ └── main.go ├── partitioned-queries-and-data-boost │ └── main.go ├── query-parameters │ └── main.go ├── read-only-transaction-with-options │ └── main.go ├── read-only-transactions │ └── main.go ├── run-transaction │ └── main.go ├── samples_test.go ├── stale-reads │ └── main.go ├── struct-types │ └── main.go ├── tags │ └── main.go ├── transactions │ └── main.go └── underlying-client │ └── main.go ├── go.mod ├── go.sum ├── integration_test.go ├── merged_row_iterator.go ├── partitioned_query.go ├── partitioned_query_test.go ├── release-please-config.json ├── renovate.json ├── return_last_insert_id_test.go ├── rows.go ├── rows_test.go ├── snippets ├── getting_started_guide.go ├── getting_started_guide_test.go ├── go.mod ├── go.sum └── samples │ ├── add_column.go │ ├── create_connection.go │ ├── create_tables.go │ ├── data_boost.go │ ├── ddl_batch.go │ ├── pdml.go │ ├── query_data.go │ ├── query_data_with_new_column.go │ ├── query_data_with_parameter.go │ ├── query_data_with_timeout.go │ ├── read_only_transaction.go │ ├── tags.go │ ├── update_data_with_mutations.go │ ├── update_data_with_transaction.go │ ├── write_data_with_dml.go │ ├── write_data_with_dml_batch.go │ └── write_data_with_mutations.go ├── statement_parser.go ├── statement_parser_test.go ├── stmt.go ├── stmt_test.go ├── stmt_with_mockserver_test.go ├── testdata └── protos │ ├── README.md │ ├── concertspb │ ├── order.go │ └── order.pb.go │ ├── order.pb │ └── order.proto ├── testutil ├── inmem_database_admin_server.go ├── inmem_instance_admin_server.go ├── inmem_instance_admin_server_test.go ├── inmem_spanner_server.go ├── inmem_spanner_server_test.go └── mocked_inmem_server.go ├── transaction.go ├── value.go └── wrapped_row_iterator.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | # 4 | # For syntax help see: 5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 6 | 7 | * @googleapis/api-spanner-go 8 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | handleGHRelease: true 2 | manifest: true 3 | releaseType: go-yoshi 4 | 5 | branches: 6 | - branch: main 7 | releaseType: go-yoshi 8 | handleGHRelease: true 9 | manifest: true 10 | manifestFile: .release-please-manifest.json 11 | manifestConfig: release-please-config.json -------------------------------------------------------------------------------- /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | multiScmName: go-sql-spanner -------------------------------------------------------------------------------- /.github/workflows/integration-tests-on-emulator.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | name: Integration tests on emulator 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | services: 10 | emulator: 11 | image: gcr.io/cloud-spanner-emulator/emulator:latest 12 | ports: 13 | - 9010:9010 14 | - 9020:9020 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.24.x 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | - name: Run integration tests on emulator 23 | run: go test -race 24 | env: 25 | JOB_TYPE: test 26 | SPANNER_EMULATOR_HOST: localhost:9010 27 | SPANNER_TEST_PROJECT: emulator-test-project 28 | SPANNER_TEST_INSTANCE: test-instance 29 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests-on-production.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | branches: [ main ] 6 | name: Integration tests on production 7 | jobs: 8 | check-env: 9 | outputs: 10 | has-key: ${{ steps.project-id.outputs.defined }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - id: project-id 14 | env: 15 | GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} 16 | if: "${{ env.GCP_PROJECT_ID != '' }}" 17 | run: echo "defined=true" >> "$GITHUB_OUTPUT" 18 | 19 | test: 20 | needs: [check-env] 21 | if: needs.check-env.outputs.has-key == 'true' 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 45 24 | steps: 25 | - name: Install Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: 1.24.x 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | - name: Auth 32 | uses: google-github-actions/auth@v2 33 | with: 34 | credentials_json: ${{ secrets.GCP_SA_KEY }} 35 | - name: Run integration tests on production 36 | run: go test -race -v -timeout 45m ./... 37 | env: 38 | JOB_TYPE: test 39 | SPANNER_TEST_PROJECT: ${{ secrets.GCP_PROJECT_ID }} 40 | SPANNER_TEST_INSTANCE: test-instance 41 | -------------------------------------------------------------------------------- /.github/workflows/samples.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | name: Run Samples 4 | jobs: 5 | samples: 6 | strategy: 7 | matrix: 8 | go-version: [1.23.x, 1.24.x] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Run samples 18 | working-directory: ./examples 19 | run: go test -short 20 | -------------------------------------------------------------------------------- /.github/workflows/snippets.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | name: Run Snippets 4 | jobs: 5 | samples: 6 | strategy: 7 | matrix: 8 | go-version: [1.23.x, 1.24.x] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Run snippets 18 | working-directory: ./snippets 19 | run: go test -short 20 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | name: Unit Tests 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go-version: [1.23.x, 1.24.x] 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | - name: Run unit tests 21 | run: go test -race -short 22 | 23 | lint: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Install Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: 1.24.x 30 | 31 | - name: Install tools 32 | run: | 33 | go install honnef.co/go/tools/cmd/staticcheck@latest 34 | 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | 38 | - name: vet . 39 | run: go vet ./... 40 | 41 | - name: vet ./examples 42 | working-directory: ./examples 43 | run: go vet ./... 44 | 45 | - name: vet ./snippets 46 | working-directory: ./snippets 47 | run: go vet ./... 48 | 49 | - name: vet ./benchmarks 50 | working-directory: ./benchmarks 51 | run: go vet ./... 52 | 53 | - name: gofmt 54 | run: | 55 | OUT=$(gofmt -s -d .) 56 | if [ -n "$OUT" ]; then 57 | echo -e "$OUT" 58 | echo "Code unformatted, please run 'gofmt -w -s .'" 59 | exit 1 60 | fi 61 | 62 | - name: staticcheck . 63 | run: staticcheck -checks "inherit,-U1000" ./... 64 | 65 | - name: staticcheck ./examples 66 | working-directory: ./examples 67 | run: staticcheck -checks "inherit,-U1000" ./... 68 | 69 | - name: staticcheck ./benchmarks 70 | working-directory: ./benchmarks 71 | run: staticcheck -checks "inherit,-U1000" ./... 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gorm/ 2 | .idea 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.13.2" 3 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, 4 | and in the interest of fostering an open and welcoming community, 5 | we pledge to respect all people who contribute through reporting issues, 6 | posting feature requests, updating documentation, 7 | submitting pull requests or patches, and other activities. 8 | 9 | We are committed to making participation in this project 10 | a harassment-free experience for everyone, 11 | regardless of level of experience, gender, gender identity and expression, 12 | sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, 22 | such as physical or electronic 23 | addresses, without explicit permission 24 | * Other unethical or unprofessional conduct. 25 | 26 | Project maintainers have the right and responsibility to remove, edit, or reject 27 | comments, commits, code, wiki edits, issues, and other contributions 28 | that are not aligned to this Code of Conduct. 29 | By adopting this Code of Conduct, 30 | project maintainers commit themselves to fairly and consistently 31 | applying these principles to every aspect of managing this project. 32 | Project maintainers who do not follow or enforce the Code of Conduct 33 | may be permanently removed from the project team. 34 | 35 | This code of conduct applies both within project spaces and in public spaces 36 | when an individual is representing the project or its community. 37 | 38 | Instances of abusive, harassing, or otherwise unacceptable behavior 39 | may be reported by opening an issue 40 | or contacting one or more of the project maintainers. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, 43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. [File an issue](https://github.com/googleapis/go-sql-spanner/issues/new/choose). 4 | The issue will be used to discuss the bug or feature and should be created 5 | before sending a PR. 6 | 7 | 1. [Install Go](https://golang.org/dl/). 8 | 1. Ensure that your `GOBIN` directory (by default `$(go env GOPATH)/bin`) 9 | is in your `PATH`. 10 | 1. Check it's working by running `go version`. 11 | * If it doesn't work, check the install location, usually 12 | `/usr/local/go`, is on your `PATH`. 13 | 14 | 1. Sign one of the 15 | [contributor license agreements](#contributor-license-agreements) below. 16 | 17 | 1. Clone the repo: 18 | `git clone https://github.com/googleapis/go-sql-spanner` 19 | 20 | 1. Change into the checked out source: 21 | `cd go-sql-spanner` 22 | 23 | 1. Fork the repo. 24 | 25 | 1. Set your fork as a remote: 26 | `git remote add fork git@github.com:GITHUB_USERNAME/go-sql-spanner.git` 27 | 28 | 1. Make changes, commit to your fork. 29 | 30 | Commit messages should follow the 31 | [Conventional Commits Style](https://www.conventionalcommits.org). The scope 32 | portion should always be filled with the name of the package affected by the 33 | changes being made. For example: 34 | ``` 35 | feat(functions): add gophers codelab 36 | ``` 37 | 38 | 1. Send a pull request with your changes. 39 | 40 | To minimize friction, consider setting `Allow edits from maintainers` on the 41 | PR, which will enable project committers and automation to update your PR. 42 | 43 | 1. A maintainer will review the pull request and make comments. 44 | 45 | Prefer adding additional commits over amending and force-pushing since it can 46 | be difficult to follow code reviews when the commit history changes. 47 | 48 | Commits will be squashed when they're merged. 49 | 50 | ## Testing 51 | 52 | We test code against two versions of Go, the minimum and maximum versions 53 | supported by our clients. To see which versions these are checkout our 54 | [README](README.md#supported-versions). 55 | 56 | ### Integration Tests 57 | 58 | In addition to the unit tests, you may run the integration test suite. 59 | 60 | #### GCP Setup 61 | 62 | To run the integrations tests, creation and configuration of a project in 63 | the Google Developers Console is required. 64 | 65 | After creating the project, you must [create a service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount) 66 | for project. Ensure the project-level **Owner** 67 | [IAM role](https://console.cloud.google.com/iam-admin/iam/project) role is added to 68 | each service account. During the creation of the service account, you should 69 | download the JSON credential file for use later. 70 | 71 | #### Local Setup 72 | 73 | Once the project is created and configured, set the following environment 74 | variables: 75 | 76 | - `SPANNER_TEST_PROJECT`: Developers Console project's ID (e.g. 77 | bamboo-shift-455). 78 | 79 | Install the [gcloud command-line tool][gcloudcli] to your machine and use it to 80 | create some resources used in integration tests. 81 | 82 | From the project's root directory: 83 | 84 | ``` sh 85 | # Sets the default project in your env. 86 | $ gcloud config set project $SPANNER_TEST_PROJECT 87 | 88 | # Authenticates the gcloud tool with your account. 89 | $ gcloud auth login 90 | 91 | # Creates a Spanner instance for the spanner integration tests. 92 | $ gcloud beta spanner instances create go-integration-test --config regional-us-central1 --nodes 10 --description 'Instance for go client test' 93 | # NOTE: Spanner instances are priced by the node-hour, so you may want to 94 | # delete the instance after testing with 'gcloud beta spanner instances delete'. 95 | 96 | $ export SPANNER_TEST_INSTANCE=go-integration-test 97 | ``` 98 | 99 | It may be useful to add exports to your shell initialization for future use. 100 | For instance, in `.zshrc`: 101 | 102 | ```sh 103 | #### START Test Variables 104 | # Developers Console project's ID (e.g. bamboo-shift-455) for the general project. 105 | export SPANNER_TEST_PROJECT=your-project 106 | 107 | # Developers Console Spanner's instance ID (e.g. spanner-instance) for the running tests. 108 | export SPANNER_TEST_INSTANCE=go-integration-test 109 | #### END Test Variables 110 | ``` 111 | 112 | #### Running 113 | 114 | Once you've done the necessary setup, you can run the integration tests by 115 | running: 116 | 117 | ``` sh 118 | $ go test -v ./... 119 | ``` 120 | 121 | ## Contributor License Agreements 122 | 123 | Before we can accept your pull requests you'll need to sign a Contributor 124 | License Agreement (CLA): 125 | 126 | - **If you are an individual writing original source code** and **you own the 127 | intellectual property**, then you'll need to sign an [individual CLA][indvcla]. 128 | - **If you work for a company that wants to allow you to contribute your 129 | work**, then you'll need to sign a [corporate CLA][corpcla]. 130 | 131 | You can sign these electronically (just scroll to the bottom). After that, 132 | we'll be able to accept your pull requests. 133 | 134 | [gcloudcli]: https://developers.google.com/cloud/sdk/gcloud/ 135 | [indvcla]: https://developers.google.com/open-source/cla/individual 136 | [corpcla]: https://developers.google.com/open-source/cla/corporate -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. 8 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import "cloud.google.com/go/spanner" 18 | 19 | type batchType int 20 | 21 | const ( 22 | ddl batchType = iota 23 | dml 24 | ) 25 | 26 | type batch struct { 27 | tp batchType 28 | statements []spanner.Statement 29 | returnValues []int64 30 | options ExecOptions 31 | automatic bool 32 | } 33 | -------------------------------------------------------------------------------- /benchmarks/benchwrapper/README.md: -------------------------------------------------------------------------------- 1 | Benchwrapper 2 | A small gRPC wrapper around the Golang's spanner driver. This allows the benchmarking code to prod at spanner without speaking Go. 3 | 4 | Running 5 | cd benchmarks/benchwrapper 6 | export SPANNER_EMULATOR_HOST=localhost:9010 7 | go run *.go --port=8081 -------------------------------------------------------------------------------- /benchmarks/benchwrapper/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main wraps the client library in a gRPC interface that a benchmarker 16 | // can communicate through. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "flag" 23 | "fmt" 24 | "log" 25 | "net" 26 | "os" 27 | 28 | "cloud.google.com/go/spanner" 29 | "google.golang.org/grpc" 30 | 31 | spannerdriver "github.com/googleapis/go-sql-spanner" 32 | pb "github.com/googleapis/go-sql-spanner/benchmarks/benchwrapper/proto" 33 | ) 34 | 35 | var port = flag.String("port", "", "specify a port to run on") 36 | 37 | type server struct { 38 | pb.UnimplementedSpannerBenchWrapperServer 39 | db *sql.DB 40 | } 41 | 42 | func main() { 43 | flag.Parse() 44 | if *port == "" { 45 | log.Fatalf("usage: %s --port=8081", os.Args[0]) 46 | } 47 | 48 | if os.Getenv("SPANNER_EMULATOR_HOST") == "" { 49 | log.Fatal("This benchmarking server only works when connected to an emulator. Please set SPANNER_EMULATOR_HOST.") 50 | } 51 | db, err := sql.Open("spanner", "projects/someproject/instances/someinstance/databases/somedatabase") 52 | if err != nil { 53 | log.Fatalf("failed to open database connection: %v\n", err) 54 | } 55 | defer db.Close() 56 | lis, err := net.Listen("tcp", fmt.Sprintf(":%s", *port)) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | s := grpc.NewServer() 61 | pb.RegisterSpannerBenchWrapperServer(s, &server{ 62 | db: db, 63 | }) 64 | log.Printf("Running on localhost:%s\n", *port) 65 | log.Fatal(s.Serve(lis)) 66 | } 67 | 68 | func (s *server) Read(ctx context.Context, req *pb.ReadQuery) (*pb.EmptyResponse, error) { 69 | tx, err := s.db.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true}) 70 | if err != nil { 71 | log.Fatalf("failed to begin read-only transaction: %v", err) 72 | } 73 | it, err := tx.Query(req.Query) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | defer it.Close() 78 | for it.Next() { 79 | } 80 | if err = it.Err(); err != nil { 81 | log.Fatalf("error iterating over the rows: %v", err) 82 | } 83 | if err := tx.Commit(); err != nil { 84 | log.Fatalf("failed to commit tx: %v", err) 85 | } 86 | return &pb.EmptyResponse{}, nil 87 | } 88 | 89 | func (s *server) Insert(ctx context.Context, req *pb.InsertQuery) (*pb.EmptyResponse, error) { 90 | var mutations []*spanner.Mutation 91 | for _, i := range req.Singers { 92 | mutations = append(mutations, spanner.Insert("Singers", []string{"SingerId", "FirstName", "LastName"}, []interface{}{i.Id, i.FirstName, i.LastName})) 93 | } 94 | conn, err := s.db.Conn(ctx) 95 | if err != nil { 96 | log.Fatalf("failed to call db.Conn: %v", err) 97 | } 98 | defer conn.Close() 99 | if err := conn.Raw(func(driverConn interface{}) error { 100 | _, err := driverConn.(spannerdriver.SpannerConn).Apply(ctx, mutations) 101 | return err 102 | }); err != nil { 103 | log.Fatal(err) 104 | } 105 | // Do nothing with the data. 106 | return &pb.EmptyResponse{}, nil 107 | } 108 | 109 | func (s *server) Update(ctx context.Context, req *pb.UpdateQuery) (*pb.EmptyResponse, error) { 110 | tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) 111 | if err != nil { 112 | log.Fatalf("failed to begin transaction: %v", err) 113 | } 114 | // A DML batch can be executed using custom SQL statements. 115 | // Start a DML batch on the transaction. 116 | if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil { 117 | log.Fatalf("failed to execute START BATCH DML: %v", err) 118 | } 119 | for _, q := range req.Queries { 120 | _, err = tx.ExecContext(ctx, q) 121 | if err != nil { 122 | log.Fatalf("failed to execute query: %v", err) 123 | } 124 | } 125 | // Run the active DML batch. 126 | if _, err := tx.ExecContext(ctx, "RUN BATCH"); err != nil { 127 | log.Fatalf("failed to execute RUN BATCH: %v", err) 128 | } 129 | // Commit the transaction. 130 | if err := tx.Commit(); err != nil { 131 | log.Fatalf("failed to commit transaction: %v", err) 132 | } 133 | // Do nothing with the data. 134 | return &pb.EmptyResponse{}, nil 135 | } 136 | -------------------------------------------------------------------------------- /benchmarks/benchwrapper/proto/README.md: -------------------------------------------------------------------------------- 1 | README.md 2 | Regenerating protos 3 | cd benchmarks/benchwrapper/proto 4 | go generate . -------------------------------------------------------------------------------- /benchmarks/benchwrapper/proto/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main wraps the client library in a gRPC interface that a benchmarker 16 | // can communicate through. 17 | package proto 18 | 19 | //go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto 20 | -------------------------------------------------------------------------------- /benchmarks/benchwrapper/proto/spanner.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package spanner_bench; 18 | option go_package = "./proto"; 19 | 20 | 21 | message Singer { 22 | int64 id = 1; 23 | string first_name = 2; 24 | string last_name = 3; 25 | string singer_info = 4; 26 | } 27 | 28 | message Album { 29 | int64 id = 1; 30 | int64 singer_id = 2; 31 | string album_title = 3; 32 | } 33 | 34 | message ReadQuery { 35 | // The query to use in the read call. 36 | string query = 1; 37 | } 38 | 39 | message InsertQuery { 40 | // The query to use in the insert call. 41 | repeated Singer singers = 1; 42 | repeated Album albums = 2; 43 | } 44 | 45 | message UpdateQuery { 46 | // The queries to use in the update call. 47 | repeated string queries = 1; 48 | } 49 | 50 | message EmptyResponse {} 51 | 52 | service SpannerBenchWrapper { 53 | // Read represents operations like Go's ReadOnlyTransaction.Query, Java's 54 | // ReadOnlyTransaction.executeQuery, Python's snapshot.read, and Node's 55 | // Transaction.Read. 56 | // 57 | // It will typically be used to read many items. 58 | rpc Read(ReadQuery) returns (EmptyResponse) {} 59 | 60 | // Insert represents operations like Go's Client.Apply, Java's 61 | // DatabaseClient.writeAtLeastOnce, Python's transaction.commit, and Node's 62 | // Transaction.Commit. 63 | // 64 | // It will typically be used to insert many items. 65 | rpc Insert(InsertQuery) returns (EmptyResponse) {} 66 | 67 | // Update represents operations like Go's ReadWriteTransaction.BatchUpdate, 68 | // Java's TransactionRunner.run, Python's Batch.update, and Node's 69 | // Transaction.BatchUpdate. 70 | // 71 | // It will typically be used to update many items. 72 | rpc Update(UpdateQuery) returns (EmptyResponse) {} 73 | } 74 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/googleapis/go-sql-spanner/benchmarks 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | replace github.com/googleapis/go-sql-spanner => ../ 8 | 9 | require ( 10 | cloud.google.com/go v0.121.1 11 | cloud.google.com/go/spanner v1.82.0 12 | github.com/google/uuid v1.6.0 13 | github.com/googleapis/go-sql-spanner v1.13.2 14 | google.golang.org/api v0.233.0 15 | google.golang.org/grpc v1.72.1 16 | google.golang.org/protobuf v1.36.6 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.23.1 // indirect 21 | cloud.google.com/go/auth v0.16.1 // indirect 22 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 23 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 24 | cloud.google.com/go/iam v1.5.2 // indirect 25 | cloud.google.com/go/longrunning v0.6.7 // indirect 26 | cloud.google.com/go/monitoring v1.24.2 // indirect 27 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect 28 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 31 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 32 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 33 | github.com/felixge/httpsnoop v1.0.4 // indirect 34 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 35 | github.com/go-logr/logr v1.4.2 // indirect 36 | github.com/go-logr/stdr v1.2.2 // indirect 37 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 38 | github.com/google/s2a-go v0.1.9 // indirect 39 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 40 | github.com/googleapis/gax-go/v2 v2.14.2 // indirect 41 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 42 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 43 | github.com/zeebo/errs v1.4.0 // indirect 44 | go.opencensus.io v0.24.0 // indirect 45 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 46 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 47 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 49 | go.opentelemetry.io/otel v1.35.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 51 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 52 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 53 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 54 | golang.org/x/crypto v0.38.0 // indirect 55 | golang.org/x/net v0.40.0 // indirect 56 | golang.org/x/oauth2 v0.30.0 // indirect 57 | golang.org/x/sync v0.14.0 // indirect 58 | golang.org/x/sys v0.33.0 // indirect 59 | golang.org/x/text v0.25.0 // indirect 60 | golang.org/x/time v0.11.0 // indirect 61 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 62 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /docs/limitations.rst: -------------------------------------------------------------------------------- 1 | Limitations 2 | ------------------------------------ 3 | 4 | Session Labeling 5 | ~~~~~~~~~~~~~~~~ 6 | Cloud Spanner Session Labeling is not supported. 7 | 8 | Request Priority 9 | ~~~~~~~~~~~~~~~~ 10 | Request priority can be set by unwrapping the Spanner-specific `SpannerConn` interface and setting the request priority as part of a db call. 11 | 12 | Tagging 13 | ~~~~~~~ 14 | Tags can be set by unwrapping the Spanner-specific `SpannerConn` interface and setting the tags using that interface. 15 | 16 | Partition Reads 17 | ~~~~~~~ 18 | Partition Reads can be done by unwrapping the Spanner-specific `SpannerConn` interface and doing the parition reads using that interface. 19 | 20 | Backups 21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | Backups are not supported by this driver. Use the [Cloud Spanner Go client library](https://github.com/googleapis/google-cloud-go/tree/main/spanner) to manage backups programmatically. 23 | -------------------------------------------------------------------------------- /emulator_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | 22 | "cloud.google.com/go/spanner" 23 | database "cloud.google.com/go/spanner/admin/database/apiv1" 24 | databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" 25 | instance "cloud.google.com/go/spanner/admin/instance/apiv1" 26 | instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" 27 | "google.golang.org/grpc/codes" 28 | ) 29 | 30 | func autoConfigEmulator(ctx context.Context, host, project, instance, database string) error { 31 | if host == "" { 32 | host = "localhost:9010" 33 | } 34 | if err := os.Setenv("SPANNER_EMULATOR_HOST", host); err != nil { 35 | return err 36 | } 37 | if err := createInstance(project, instance); err != nil { 38 | if spanner.ErrCode(err) != codes.AlreadyExists { 39 | return err 40 | } 41 | } 42 | if err := createDatabase(project, instance, database); err != nil { 43 | if spanner.ErrCode(err) != codes.AlreadyExists { 44 | return err 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func createInstance(projectId, instanceId string) error { 51 | ctx := context.Background() 52 | instanceAdmin, err := instance.NewInstanceAdminClient(ctx) 53 | if err != nil { 54 | return err 55 | } 56 | defer instanceAdmin.Close() 57 | op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ 58 | Parent: fmt.Sprintf("projects/%s", projectId), 59 | InstanceId: instanceId, 60 | Instance: &instancepb.Instance{ 61 | Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectId, "emulator-config"), 62 | DisplayName: instanceId, 63 | NodeCount: 1, 64 | }, 65 | }) 66 | if err != nil { 67 | return fmt.Errorf("could not create instance %s: %v", fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), err) 68 | } 69 | // Wait for the instance creation to finish. 70 | if _, err := op.Wait(ctx); err != nil { 71 | return fmt.Errorf("waiting for instance creation to finish failed: %v", err) 72 | } 73 | return nil 74 | } 75 | 76 | func createDatabase(projectId, instanceId, databaseId string) error { 77 | ctx := context.Background() 78 | databaseAdminClient, err := database.NewDatabaseAdminClient(ctx) 79 | if err != nil { 80 | return err 81 | } 82 | defer databaseAdminClient.Close() 83 | opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ 84 | Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), 85 | CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId), 86 | }) 87 | if err != nil { 88 | return err 89 | } 90 | // Wait for the database creation to finish. 91 | if _, err := opDB.Wait(ctx); err != nil { 92 | return fmt.Errorf("waiting for database creation to finish failed: %v", err) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Spanner Go Sql Examples 2 | 3 | This directory contains samples for how to use the Spanner go-sql driver. Each sample can be executed 4 | as a standalone application without the need for any prior setup, other than that Docker must be installed 5 | on your system. Each sample will automatically: 6 | 1. Download and start the [Spanner Emulator](https://cloud.google.com/spanner/docs/emulator) in a Docker container. 7 | 2. Create a sample database and execute the sample on the sample database. 8 | 3. Shutdown the Docker container that is running the emulator. 9 | 10 | Running a sample is done by navigating to the corresponding sample directory and executing the following command: 11 | 12 | ```shell 13 | # Change the 'helloword' directory below to any of the samples in this directory. 14 | cd helloworld 15 | go run main.go 16 | ``` 17 | 18 | ## Prerequisites 19 | 20 | Your system must have [Docker installed](https://docs.docker.com/get-docker/) for these samples to be executed, 21 | as each sample will automatically start the Spanner Emulator in a Docker container. -------------------------------------------------------------------------------- /examples/auto-partition-queries/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 28 | 29 | // Sample showing how to use a batch read-only transaction to auto-partition a query 30 | // and execute all partitions as if it was one query. This sample also shows how to 31 | // use DataBoost. 32 | // 33 | // See https://cloud.google.com/spanner/docs/databoost/databoost-overview for more 34 | // information. 35 | // 36 | // Execute the sample with the command `go run main.go` from this directory. 37 | func autoPartitionQuery(projectId, instanceId, databaseId string) error { 38 | ctx := context.Background() 39 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 40 | if err != nil { 41 | return err 42 | } 43 | defer db.Close() 44 | 45 | // Insert 10 test rows. 46 | if err := insertTestRows(db, 10); err != nil { 47 | return err 48 | } 49 | 50 | // Start a batch read-only transaction. 51 | tx, err := spannerdriver.BeginBatchReadOnlyTransaction(ctx, db, spannerdriver.BatchReadOnlyTransactionOptions{}) 52 | if err != nil { 53 | return err 54 | } 55 | // Committing or rolling back a read-only transaction will not execute an actual Commit or Rollback 56 | // on the database, but it is needed in order to release the resources that are held by the read-only 57 | // transaction. 58 | defer func() { _ = tx.Commit() }() 59 | 60 | // Auto-partition and execute a query. The driver internal calls PartitionQuery 61 | // and then executes each partition in a goroutine. The results of the various 62 | // partitions are streamed into the row iterator that is returned for the query. 63 | // The iterator will return the rows in arbitrary order. 64 | rows, err := tx.QueryContext(ctx, "select * from singers", spannerdriver.ExecOptions{ 65 | PartitionedQueryOptions: spannerdriver.PartitionedQueryOptions{ 66 | // Set this option to true to automatically partition the query 67 | // and execute each partition. The results are returned as one 68 | // row iterator. 69 | AutoPartitionQuery: true, 70 | // MaxParallelism sets the maximum number of goroutines that 71 | // will be used by the driver to execute the returned partitions. 72 | // If not set, it will default to the number of available CPUs. 73 | // The number of actual goroutines will never exceed the number of 74 | // partitions that is returned by Spanner. 75 | MaxParallelism: 8, 76 | }, 77 | QueryOptions: spanner.QueryOptions{ 78 | // Set DataBoostEnabled to true to enable DataBoost. 79 | // See https://cloud.google.com/spanner/docs/databoost/databoost-overview 80 | // for more information. 81 | DataBoostEnabled: true, 82 | }, 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | defer rows.Close() 88 | 89 | count := 0 90 | var id int64 91 | var name string 92 | for rows.Next() { 93 | if err := rows.Scan(&id, &name); err != nil { 94 | return err 95 | } 96 | fmt.Printf("Id: %v, Name: %v\n", id, name) 97 | count++ 98 | } 99 | if rows.Err() != nil { 100 | return rows.Err() 101 | } 102 | fmt.Printf("Executing all partitions returned a total of %v rows\n", count) 103 | 104 | return nil 105 | } 106 | 107 | func main() { 108 | examples.RunSampleOnEmulator(autoPartitionQuery, createTableStatement) 109 | } 110 | 111 | func insertTestRows(db *sql.DB, numRows int) error { 112 | ctx := context.Background() 113 | 114 | // Insert a few test rows. 115 | tx, err := db.BeginTx(ctx, &sql.TxOptions{}) 116 | if err != nil { 117 | return err 118 | } 119 | // Insert the test rows in one batch. 120 | if _, err := tx.ExecContext(ctx, "start batch dml"); err != nil { 121 | tx.Rollback() 122 | return err 123 | } 124 | stmt, err := tx.PrepareContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (?, ?)") 125 | if err != nil { 126 | tx.Rollback() 127 | return err 128 | } 129 | for i := 0; i < numRows; i++ { 130 | if _, err := stmt.ExecContext(ctx, int64(i), fmt.Sprintf("Test %v", i)); err != nil { 131 | _, _ = tx.ExecContext(ctx, "abort batch") 132 | tx.Rollback() 133 | return err 134 | } 135 | } 136 | if _, err := tx.ExecContext(ctx, "run batch"); err != nil { 137 | tx.Rollback() 138 | return err 139 | } 140 | return tx.Commit() 141 | } 142 | -------------------------------------------------------------------------------- /examples/commit-timestamp/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "time" 22 | 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 28 | 29 | // Example for getting the commit timestamp of a read/write transaction. 30 | // 31 | // Execute the sample with the command `go run main.go` from this directory. 32 | func commitTimestamp(projectId, instanceId, databaseId string) error { 33 | ctx := context.Background() 34 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 35 | if err != nil { 36 | return err 37 | } 38 | defer db.Close() 39 | 40 | // Get a connection from the pool and start the transaction on that connection. 41 | conn, err := db.Conn(ctx) 42 | if err != nil { 43 | return fmt.Errorf("failed to get a connection from the pool: %v", err) 44 | } 45 | defer conn.Close() 46 | 47 | tx, err := conn.BeginTx(ctx, &sql.TxOptions{}) 48 | if err != nil { 49 | return err 50 | } 51 | if _, err := tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 123, "Bruce Allison"); err != nil { 52 | _ = tx.Rollback() 53 | return fmt.Errorf("failed to insert test record: %v", err) 54 | } 55 | if err := tx.Commit(); err != nil { 56 | return fmt.Errorf("failed to commit transaction: %v", err) 57 | } 58 | 59 | // Get the commit timestamp of the last transaction that was executed on the connection. 60 | var ct time.Time 61 | if err := conn.Raw(func(driverConn interface{}) (err error) { 62 | ct, err = driverConn.(spannerdriver.SpannerConn).CommitTimestamp() 63 | return err 64 | }); err != nil { 65 | return fmt.Errorf("failed to get commit timestamp: %v", err) 66 | } 67 | 68 | // The commit timestamp can also be obtained by executing the custom SQL statement `SHOW VARIABLE COMMIT_TIMESTAMP` 69 | var ct2 time.Time 70 | if err := conn.QueryRowContext(ctx, "SHOW VARIABLE COMMIT_TIMESTAMP").Scan(&ct2); err != nil { 71 | return fmt.Errorf("failed to execute SHOW VARIABLE COMMIT_TIMESTAMP") 72 | } 73 | 74 | // The commit timestamp that is obtained directly from the connection is in the local timezone. 75 | fmt.Printf("Transaction committed at %v\n", ct) 76 | // The commit timestamp that is obtained through SHOW VARIABLE COMMIT_TIMESTAMP is in UTC. 77 | fmt.Printf("SHOW VARIABLE COMMIT_TIMESTAMP returned %v\n", ct2) 78 | 79 | return nil 80 | } 81 | 82 | func main() { 83 | examples.RunSampleOnEmulator(commitTimestamp, createTableStatement) 84 | } 85 | -------------------------------------------------------------------------------- /examples/connect/connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | // [START spanner_database_sql_connect] 18 | import ( 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | ) 24 | 25 | func connect(projectId, instanceId, databaseId string) error { 26 | dsn := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId) 27 | db, err := sql.Open("spanner", dsn) 28 | if err != nil { 29 | return fmt.Errorf("failed to open database connection: %v", err) 30 | } 31 | defer func() { _ = db.Close() }() 32 | 33 | fmt.Printf("Connected to %s\n", dsn) 34 | 35 | return nil 36 | } 37 | 38 | // [END spanner_database_sql_connect] 39 | -------------------------------------------------------------------------------- /examples/connect/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "github.com/googleapis/go-sql-spanner/examples" 18 | 19 | func main() { 20 | examples.RunSampleOnEmulator(connect) 21 | } 22 | -------------------------------------------------------------------------------- /examples/custom-client-configuration/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | "cloud.google.com/go/spanner/apiv1/spannerpb" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/googleapis/go-sql-spanner/examples" 26 | "google.golang.org/api/option" 27 | ) 28 | 29 | // Sample that shows how to use supply a custom configuration for the Spanner client 30 | // that is used by the Go sql driver. 31 | // 32 | // Execute the sample with the command `go run main.go` from this directory. 33 | func customClientConfiguration(projectId, instanceId, databaseId string) error { 34 | ctx := context.Background() 35 | 36 | connectorConfig := spannerdriver.ConnectorConfig{ 37 | Project: projectId, 38 | Instance: instanceId, 39 | Database: databaseId, 40 | 41 | // Create a function that sets the Spanner client configuration for the database connection. 42 | Configurator: func(config *spanner.ClientConfig, opts *[]option.ClientOption) { 43 | // Set a default query optimizer version that the client should use. 44 | config.QueryOptions = spanner.QueryOptions{Options: &spannerpb.ExecuteSqlRequest_QueryOptions{OptimizerVersion: "1"}} 45 | }, 46 | } 47 | 48 | // Create a Connector for Spanner to create a DB with a custom configuration. 49 | c, err := spannerdriver.CreateConnector(connectorConfig) 50 | if err != nil { 51 | return fmt.Errorf("failed to create connector: %v", err) 52 | } 53 | 54 | // Create a DB using the Connector. 55 | db := sql.OpenDB(c) 56 | defer db.Close() 57 | 58 | rows, err := db.QueryContext(ctx, "SELECT 'Hello World!'") 59 | if err != nil { 60 | return fmt.Errorf("failed to execute query: %v", err) 61 | } 62 | defer rows.Close() 63 | 64 | var msg string 65 | for rows.Next() { 66 | if err := rows.Scan(&msg); err != nil { 67 | return fmt.Errorf("failed to scan row values: %v", err) 68 | } 69 | fmt.Printf("%s\n", msg) 70 | } 71 | if err := rows.Err(); err != nil { 72 | return fmt.Errorf("failed to execute query: %v", err) 73 | } 74 | return nil 75 | } 76 | 77 | func main() { 78 | examples.RunSampleOnEmulator(customClientConfiguration) 79 | } 80 | -------------------------------------------------------------------------------- /examples/ddl-batches/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | spannerdriver "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | // Sample showing how to execute a batch of DDL statements. 27 | // Batching DDL statements together instead of executing them one by one leads to a much lower total execution time. 28 | // It is therefore recommended that DDL statements are always executed in batches whenever possible. 29 | // 30 | // DDL batches can be executed in two ways using the Spanner go sql driver: 31 | // 1. By executing the SQL statements `START BATCH DDL` and `RUN BATCH`. 32 | // 2. By unwrapping the Spanner specific driver interface spannerdriver.Driver and calling the 33 | // spannerdriver.Driver#StartBatchDDL and spannerdriver.Driver#RunBatch methods. 34 | // 35 | // This sample shows how to use both possibilities. 36 | // 37 | // Execute the sample with the command `go run main.go` from this directory. 38 | func ddlBatches(projectId, instanceId, databaseId string) error { 39 | ctx := context.Background() 40 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 41 | if err != nil { 42 | return fmt.Errorf("failed to open database connection: %v", err) 43 | } 44 | defer db.Close() 45 | 46 | // Execute a DDL batch by executing the `START BATCH DDL` and `RUN BATCH` statements. 47 | // First we need to get a connection from the pool to ensure that all statements are executed on the same 48 | // connection. 49 | conn, err := db.Conn(ctx) 50 | if err != nil { 51 | return fmt.Errorf("failed to get connection: %v", err) 52 | } 53 | defer conn.Close() 54 | // Start a DDL batch on the connection. 55 | if _, err := conn.ExecContext(ctx, "START BATCH DDL"); err != nil { 56 | return fmt.Errorf("START BATCH DDL failed: %v", err) 57 | } 58 | // Execute the DDL statements on the same connection as where we started the batch. 59 | // These statements will be buffered in the connection and executed on Spanner when we execute `RUN BATCH`. 60 | _, _ = conn.ExecContext(ctx, "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)") 61 | _, _ = conn.ExecContext(ctx, "CREATE INDEX Idx_Singers_Name ON Singers (Name)") 62 | // Executing `RUN BATCH` will run the previous DDL statements as one batch. 63 | if _, err := conn.ExecContext(ctx, "RUN BATCH"); err != nil { 64 | return fmt.Errorf("RUN BATCH failed: %v", err) 65 | } 66 | fmt.Printf("Executed DDL batch using SQL statements\n") 67 | 68 | // A DDL batch can also be executed programmatically by unwrapping the spannerdriver.Driver interface. 69 | if err := conn.Raw(func(driverConn interface{}) error { 70 | // Get the Spanner connection interface and start a DDL batch on the connection. 71 | return driverConn.(spannerdriver.SpannerConn).StartBatchDDL() 72 | }); err != nil { 73 | return fmt.Errorf("conn.Raw failed: %v", err) 74 | } 75 | _, _ = conn.ExecContext(ctx, "CREATE TABLE Albums (SingerId INT64, AlbumId INT64, Title STRING(MAX)) PRIMARY KEY (SingerId, AlbumId), INTERLEAVE IN PARENT Singers") 76 | _, _ = conn.ExecContext(ctx, "CREATE TABLE Tracks (SingerId INT64, AlbumId INT64, TrackId INT64, Title STRING(MAX)) PRIMARY KEY (SingerId, AlbumId, TrackId), INTERLEAVE IN PARENT Albums") 77 | if err := conn.Raw(func(driverConn interface{}) error { 78 | return driverConn.(spannerdriver.SpannerConn).RunBatch(ctx) 79 | }); err != nil { 80 | return fmt.Errorf("conn.Raw failed: %v", err) 81 | } 82 | fmt.Printf("Executed DDL batch using Spanner connection methods\n") 83 | 84 | // Get the number of tables and indexes. 85 | var tc int64 86 | if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_CATALOG='' AND TABLE_SCHEMA=''").Scan(&tc); err != nil { 87 | return fmt.Errorf("failed to execute count tables query: %v", err) 88 | } 89 | var ic int64 90 | if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_CATALOG='' AND TABLE_SCHEMA='' AND INDEX_TYPE != 'PRIMARY_KEY'").Scan(&ic); err != nil { 91 | return fmt.Errorf("failed to execute count indexes query: %v", err) 92 | } 93 | fmt.Println() 94 | fmt.Printf("The database now contains %v tables and %v indexes\n", tc, ic) 95 | 96 | return nil 97 | } 98 | 99 | func main() { 100 | examples.RunSampleOnEmulator(ddlBatches) 101 | } 102 | -------------------------------------------------------------------------------- /examples/decode-options/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | // Example for getting the underlying protobuf objects from a query result 28 | // instead of decoding the values into Go types. This can be used for 29 | // advanced use cases where you want to have full control over how data 30 | // is decoded, or where you want to skip the decode step for performance 31 | // reasons. 32 | func decodeOptions(projectId, instanceId, databaseId string) error { 33 | ctx := context.Background() 34 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 35 | if err != nil { 36 | return fmt.Errorf("failed to open database connection: %v", err) 37 | } 38 | defer db.Close() 39 | 40 | // Pass an ExecOptions value with DecodeOption set to DecodeOptionProto 41 | // as an argument to QueryContext to instruct the Spanner driver to skip 42 | // decoding the data into Go types. 43 | rows, err := db.QueryContext(ctx, 44 | `SELECT JSON '{"key1": "value1", "key2": 2, "key3": ["value1", "value2"]}'`, 45 | spannerdriver.ExecOptions{DecodeOption: spannerdriver.DecodeOptionProto}) 46 | if err != nil { 47 | return fmt.Errorf("failed to execute query: %v", err) 48 | } 49 | defer rows.Close() 50 | 51 | for rows.Next() { 52 | // As we are using DecodeOptionProto, all values must be scanned 53 | // into spanner.GenericColumnValue. 54 | var value spanner.GenericColumnValue 55 | 56 | if err := rows.Scan(&value); err != nil { 57 | return fmt.Errorf("failed to scan row values: %v", err) 58 | } 59 | fmt.Printf("Received value %v\n", value.Value.GetStringValue()) 60 | } 61 | if err := rows.Err(); err != nil { 62 | return fmt.Errorf("failed to execute query: %v", err) 63 | } 64 | return nil 65 | } 66 | 67 | func main() { 68 | examples.RunSampleOnEmulator(decodeOptions) 69 | } 70 | -------------------------------------------------------------------------------- /examples/directed-reads/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | sppb "cloud.google.com/go/spanner/apiv1/spannerpb" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/googleapis/go-sql-spanner/examples" 26 | ) 27 | 28 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 29 | 30 | // Example for using directed reads with the Spanner database/sql driver. 31 | func directedRead(projectId, instanceId, databaseId string) error { 32 | ctx := context.Background() 33 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 34 | if err != nil { 35 | return fmt.Errorf("failed to open database connection: %v", err) 36 | } 37 | defer db.Close() 38 | 39 | // Pass an ExecOptions value as an argument to QueryContext to specify 40 | // specific query options for a query. 41 | directedReadOptions := &sppb.DirectedReadOptions{ 42 | Replicas: &sppb.DirectedReadOptions_IncludeReplicas_{ 43 | IncludeReplicas: &sppb.DirectedReadOptions_IncludeReplicas{ 44 | ReplicaSelections: []*sppb.DirectedReadOptions_ReplicaSelection{ 45 | { 46 | Type: sppb.DirectedReadOptions_ReplicaSelection_READ_ONLY, 47 | }, 48 | }, 49 | AutoFailoverDisabled: true, 50 | }, 51 | }, 52 | } 53 | fmt.Println("Executing a query with a DirectedRead option") 54 | rows, err := db.QueryContext(ctx, 55 | `SELECT SingerId, Name FROM Singers`, 56 | spannerdriver.ExecOptions{QueryOptions: spanner.QueryOptions{DirectedReadOptions: directedReadOptions}}) 57 | if err != nil { 58 | return fmt.Errorf("failed to execute query: %v", err) 59 | } 60 | defer rows.Close() 61 | 62 | for rows.Next() { 63 | var singerId int64 64 | var name string 65 | 66 | if err := rows.Scan(&singerId, &name); err != nil { 67 | return fmt.Errorf("failed to scan row values: %v", err) 68 | } 69 | fmt.Printf("Singer: %v: %v\n", singerId, name) 70 | } 71 | if err := rows.Err(); err != nil { 72 | return fmt.Errorf("failed to execute query: %v", err) 73 | } 74 | return nil 75 | } 76 | 77 | func main() { 78 | examples.RunSampleOnEmulator(directedRead, createTableStatement) 79 | } 80 | -------------------------------------------------------------------------------- /examples/dml-batches/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | spannerdriver "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 27 | 28 | // Sample showing how to execute a batch of DML statements. 29 | // Batching DML statements together instead of executing them one by one reduces the number of round trips to Spanner 30 | // that are needed. 31 | // 32 | // DML batches can be executed in two ways using the Spanner go sql driver: 33 | // 1. By executing the SQL statements `START BATCH DML` and `RUN BATCH`. 34 | // 2. By unwrapping the Spanner specific driver interface spannerdriver.Driver and calling the 35 | // spannerdriver.Driver#StartBatchDML and spannerdriver.Driver#RunBatch methods. 36 | // 37 | // This sample shows how to use both possibilities. 38 | // 39 | // Execute the sample with the command `go run main.go` from this directory. 40 | func dmlBatch(projectId, instanceId, databaseId string) error { 41 | ctx := context.Background() 42 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 43 | if err != nil { 44 | return err 45 | } 46 | defer db.Close() 47 | 48 | // Start a read/write transaction. 49 | tx, err := db.BeginTx(ctx, &sql.TxOptions{}) 50 | if err != nil { 51 | return fmt.Errorf("failed to begin transaction: %v", err) 52 | } 53 | 54 | // A DML batch can be executed using custom SQL statements. 55 | // Start a DML batch on the transaction. 56 | if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil { 57 | return fmt.Errorf("failed to execute START BATCH DML: %v", err) 58 | } 59 | // Insert a number of DML statements on the transaction. These statements will be buffered locally in the 60 | // transaction and will only be sent to Spanner once RUN BATCH is executed. 61 | if _, err := tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 1, "Singer 1"); err != nil { 62 | return fmt.Errorf("failed to insert: %v", err) 63 | } 64 | if _, err := tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 2, "Singer 2"); err != nil { 65 | return fmt.Errorf("failed to insert: %v", err) 66 | } 67 | if _, err := tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 3, "Singer 3"); err != nil { 68 | return fmt.Errorf("failed to insert: %v", err) 69 | } 70 | // Run the active DML batch. 71 | if _, err := tx.ExecContext(ctx, "RUN BATCH"); err != nil { 72 | return fmt.Errorf("failed to execute RUN BATCH: %v", err) 73 | } 74 | // Commit the transaction. 75 | if err := tx.Commit(); err != nil { 76 | return fmt.Errorf("failed to commit transaction: %v", err) 77 | } 78 | 79 | // Verify that all three records were inserted. 80 | var c int64 81 | if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 82 | return fmt.Errorf("failed to get singers count: %v", err) 83 | } 84 | fmt.Printf("# of Singer records after first batch: %v\n", c) 85 | 86 | // A DML batch can also be executed using the StartBatchDML and RunBatch methods on the SpannerConn interface. 87 | // A DML batch also does not need to be executed on a transaction. 88 | conn, err := db.Conn(ctx) 89 | if err != nil { 90 | return fmt.Errorf("failed to get connection: %v", err) 91 | } 92 | defer conn.Close() 93 | if err := conn.Raw(func(driverConn interface{}) error { 94 | return driverConn.(spannerdriver.SpannerConn).StartBatchDML() 95 | }); err != nil { 96 | return fmt.Errorf("failed to start DML batch: %v", err) 97 | } 98 | // Note that we execute the DML statements on the connection that started the DML batch. 99 | if _, err := conn.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 4, "Singer 4"); err != nil { 100 | return fmt.Errorf("failed to insert: %v", err) 101 | } 102 | if _, err := conn.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 5, "Singer 5"); err != nil { 103 | return fmt.Errorf("failed to insert: %v", err) 104 | } 105 | if _, err := conn.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 6, "Singer 6"); err != nil { 106 | return fmt.Errorf("failed to insert: %v", err) 107 | } 108 | // Run the batch. This will apply all the batched DML statements to the database in one atomic operation. 109 | if err := conn.Raw(func(driverConn interface{}) error { 110 | return driverConn.(spannerdriver.SpannerConn).RunBatch(ctx) 111 | }); err != nil { 112 | return fmt.Errorf("failed to run DML batch: %v", err) 113 | } 114 | if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 115 | return fmt.Errorf("failed to get singers count: %v", err) 116 | } 117 | fmt.Printf("# of Singer records after second batch: %v\n", c) 118 | 119 | return nil 120 | } 121 | 122 | func main() { 123 | examples.RunSampleOnEmulator(dmlBatch, createTableStatement) 124 | } 125 | -------------------------------------------------------------------------------- /examples/emulator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "log" 22 | 23 | "github.com/docker/docker/api/types/container" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/testcontainers/testcontainers-go" 26 | "github.com/testcontainers/testcontainers-go/wait" 27 | ) 28 | 29 | // Sample application that shows how to use the Spanner Go sql driver to connect 30 | // to the Spanner Emulator and automatically create the instance and database 31 | // from the connection string on the Emulator. 32 | // 33 | // Execute the sample with the command `go run main.go` from this directory. 34 | func emulator(projectId, instanceId, databaseId string) error { 35 | ctx := context.Background() 36 | 37 | // Start the Spanner emulator in a Docker container. 38 | emulator, host, err := startEmulator() 39 | if err != nil { 40 | return err 41 | } 42 | defer func() { _ = emulator.Terminate(context.Background()) }() 43 | 44 | config := spannerdriver.ConnectorConfig{ 45 | // AutoConfigEmulator instructs the driver to: 46 | // 1. Connect to the emulator using plain text. 47 | // 2. Create the instance and database if they do not already exist. 48 | AutoConfigEmulator: true, 49 | 50 | // You only have to set the host if it is different from the default 51 | // 'localhost:9010' host for the Spanner emulator. 52 | Host: host, 53 | 54 | // The instance and database will automatically be created on the Emulator. 55 | Project: projectId, 56 | Instance: instanceId, 57 | Database: databaseId, 58 | } 59 | connector, err := spannerdriver.CreateConnector(config) 60 | if err != nil { 61 | return err 62 | } 63 | db := sql.OpenDB(connector) 64 | defer func() { _ = db.Close() }() 65 | 66 | rows, err := db.QueryContext(ctx, "SELECT 'Hello World!'") 67 | if err != nil { 68 | return fmt.Errorf("failed to execute query: %v", err) 69 | } 70 | defer rows.Close() 71 | 72 | var msg string 73 | for rows.Next() { 74 | if err := rows.Scan(&msg); err != nil { 75 | return fmt.Errorf("failed to scan row values: %v", err) 76 | } 77 | fmt.Printf("%s\n", msg) 78 | } 79 | if err := rows.Err(); err != nil { 80 | return fmt.Errorf("failed to iterate over query results: %v", err) 81 | } 82 | return nil 83 | } 84 | 85 | func main() { 86 | if err := emulator("emulator-project", "test-instance", "test-database"); err != nil { 87 | log.Fatal(err) 88 | } 89 | } 90 | 91 | // startEmulator starts the Spanner Emulator in a Docker container. 92 | func startEmulator() (testcontainers.Container, string, error) { 93 | ctx := context.Background() 94 | req := testcontainers.ContainerRequest{ 95 | AlwaysPullImage: true, 96 | Image: "gcr.io/cloud-spanner-emulator/emulator", 97 | ExposedPorts: []string{"9010/tcp"}, 98 | WaitingFor: wait.ForAll(wait.ForListeningPort("9010/tcp"), wait.ForLog("gRPC server listening")), 99 | HostConfigModifier: func(hostConfig *container.HostConfig) { 100 | hostConfig.AutoRemove = true 101 | }, 102 | } 103 | emulator, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 104 | ContainerRequest: req, 105 | Started: true, 106 | }) 107 | if err != nil { 108 | return emulator, "", fmt.Errorf("failed to start the emulator: %v", err) 109 | } 110 | host, err := emulator.Host(ctx) 111 | if err != nil { 112 | return emulator, "", fmt.Errorf("failed to get host: %v", err) 113 | } 114 | mappedPort, err := emulator.MappedPort(ctx, "9010/tcp") 115 | if err != nil { 116 | return emulator, "", fmt.Errorf("failed to get mapped port: %v", err) 117 | } 118 | port := mappedPort.Int() 119 | 120 | return emulator, fmt.Sprintf("%s:%v", host, port), nil 121 | } 122 | -------------------------------------------------------------------------------- /examples/emulator_runner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package examples 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "log" 22 | "os" 23 | "time" 24 | 25 | database "cloud.google.com/go/spanner/admin/database/apiv1" 26 | databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" 27 | instance "cloud.google.com/go/spanner/admin/instance/apiv1" 28 | instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" 29 | "github.com/docker/docker/api/types/container" 30 | "github.com/docker/docker/api/types/image" 31 | "github.com/docker/docker/client" 32 | "github.com/docker/go-connections/nat" 33 | ) 34 | 35 | var cli *client.Client 36 | var containerId string 37 | 38 | // RunSampleOnEmulator will run a sample function against a Spanner emulator instance in a Docker container. 39 | // It requires Docker to be installed your local system to work. 40 | // 41 | // It will execute the following steps: 42 | // 1. Start a Spanner emulator in a Docker container. 43 | // 2. Create a sample instance and database on the emulator. 44 | // 3. Execute the sample function against the emulator. 45 | // 4. Stop the Docker container with the emulator. 46 | func RunSampleOnEmulator(sample func(string, string, string) error, ddlStatements ...string) { 47 | var err error 48 | if err = startEmulator(); err != nil { 49 | log.Fatalf("failed to start emulator: %v", err) 50 | } 51 | projectId, instanceId, databaseId := "my-project", "my-instance", "my-database" 52 | if err = createInstance(projectId, instanceId); err != nil { 53 | stopEmulator() 54 | log.Fatalf("failed to create instance on emulator: %v", err) 55 | } 56 | if err = createSampleDB(projectId, instanceId, databaseId, ddlStatements...); err != nil { 57 | stopEmulator() 58 | log.Fatalf("failed to create database on emulator: %v", err) 59 | } 60 | err = sample(projectId, instanceId, databaseId) 61 | stopEmulator() 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | 67 | func startEmulator() error { 68 | ctx := context.Background() 69 | if err := os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010"); err != nil { 70 | return err 71 | } 72 | 73 | // Initialize a Docker client. 74 | var err error 75 | cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 76 | if err != nil { 77 | return err 78 | } 79 | // Pull the Spanner Emulator docker image. 80 | reader, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", image.PullOptions{}) 81 | if err != nil { 82 | return err 83 | } 84 | defer func() { _ = reader.Close() }() 85 | // cli.ImagePull is asynchronous. 86 | // The reader needs to be read completely for the pull operation to complete. 87 | if _, err := io.Copy(io.Discard, reader); err != nil { 88 | return err 89 | } 90 | 91 | // Create and start a container with the emulator. 92 | resp, err := cli.ContainerCreate(ctx, &container.Config{ 93 | Image: "gcr.io/cloud-spanner-emulator/emulator", 94 | ExposedPorts: nat.PortSet{"9010": {}}, 95 | }, &container.HostConfig{ 96 | AutoRemove: true, 97 | PortBindings: map[nat.Port][]nat.PortBinding{"9010": {{HostIP: "0.0.0.0", HostPort: "9010"}}}, 98 | }, nil, nil, "") 99 | if err != nil { 100 | return err 101 | } 102 | containerId = resp.ID 103 | if err := cli.ContainerStart(ctx, containerId, container.StartOptions{}); err != nil { 104 | return err 105 | } 106 | // Wait max 10 seconds or until the emulator is running. 107 | for c := 0; c < 20; c++ { 108 | // Always wait at least 500 milliseconds to ensure that the emulator is actually ready, as the 109 | // state can be reported as ready, while the emulator (or network interface) is actually not ready. 110 | <-time.After(500 * time.Millisecond) 111 | resp, err := cli.ContainerInspect(ctx, containerId) 112 | if err != nil { 113 | return fmt.Errorf("failed to inspect container state: %v", err) 114 | } 115 | if resp.State.Running { 116 | break 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func createInstance(projectId, instanceId string) error { 124 | ctx := context.Background() 125 | instanceAdmin, err := instance.NewInstanceAdminClient(ctx) 126 | if err != nil { 127 | return err 128 | } 129 | defer instanceAdmin.Close() 130 | op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ 131 | Parent: fmt.Sprintf("projects/%s", projectId), 132 | InstanceId: instanceId, 133 | Instance: &instancepb.Instance{ 134 | Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectId, "emulator-config"), 135 | DisplayName: instanceId, 136 | NodeCount: 1, 137 | }, 138 | }) 139 | if err != nil { 140 | return fmt.Errorf("could not create instance %s: %v", fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), err) 141 | } 142 | // Wait for the instance creation to finish. 143 | if _, err := op.Wait(ctx); err != nil { 144 | return fmt.Errorf("waiting for instance creation to finish failed: %v", err) 145 | } 146 | return nil 147 | } 148 | 149 | func createSampleDB(projectId, instanceId, databaseId string, statements ...string) error { 150 | ctx := context.Background() 151 | databaseAdminClient, err := database.NewDatabaseAdminClient(ctx) 152 | if err != nil { 153 | return err 154 | } 155 | defer databaseAdminClient.Close() 156 | opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ 157 | Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), 158 | CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId), 159 | ExtraStatements: statements, 160 | }) 161 | if err != nil { 162 | return err 163 | } 164 | // Wait for the database creation to finish. 165 | if _, err := opDB.Wait(ctx); err != nil { 166 | return fmt.Errorf("waiting for database creation to finish failed: %v", err) 167 | } 168 | return nil 169 | } 170 | 171 | func stopEmulator() { 172 | if cli == nil || containerId == "" { 173 | return 174 | } 175 | ctx := context.Background() 176 | timeout := 10 177 | if err := cli.ContainerStop(ctx, containerId, container.StopOptions{Timeout: &timeout}); err != nil { 178 | log.Printf("failed to stop emulator: %v\n", err) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/googleapis/go-sql-spanner/examples 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.2 6 | 7 | replace github.com/googleapis/go-sql-spanner => ../ 8 | 9 | require ( 10 | cloud.google.com/go v0.121.1 11 | cloud.google.com/go/spanner v1.82.0 12 | github.com/docker/docker v28.1.1+incompatible 13 | github.com/docker/go-connections v0.5.0 14 | github.com/googleapis/go-sql-spanner v1.0.1 15 | github.com/testcontainers/testcontainers-go v0.35.0 16 | google.golang.org/api v0.233.0 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.23.1 // indirect 21 | cloud.google.com/go/auth v0.16.1 // indirect 22 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 23 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 24 | cloud.google.com/go/iam v1.5.2 // indirect 25 | cloud.google.com/go/longrunning v0.6.7 // indirect 26 | cloud.google.com/go/monitoring v1.24.2 // indirect 27 | dario.cat/mergo v1.0.0 // indirect 28 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 29 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 31 | github.com/Microsoft/go-winio v0.6.2 // indirect 32 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 33 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 34 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 35 | github.com/containerd/log v0.1.0 // indirect 36 | github.com/containerd/platforms v0.2.1 // indirect 37 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/distribution/reference v0.6.0 // indirect 40 | github.com/docker/go-units v0.5.0 // indirect 41 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 42 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 43 | github.com/felixge/httpsnoop v1.0.4 // indirect 44 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 45 | github.com/go-logr/logr v1.4.2 // indirect 46 | github.com/go-logr/stdr v1.2.2 // indirect 47 | github.com/go-ole/go-ole v1.2.6 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 50 | github.com/google/s2a-go v0.1.9 // indirect 51 | github.com/google/uuid v1.6.0 // indirect 52 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 53 | github.com/googleapis/gax-go/v2 v2.14.2 // indirect 54 | github.com/klauspost/compress v1.18.0 // indirect 55 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 56 | github.com/magiconair/properties v1.8.7 // indirect 57 | github.com/moby/docker-image-spec v1.3.1 // indirect 58 | github.com/moby/go-archive v0.1.0 // indirect 59 | github.com/moby/patternmatcher v0.6.0 // indirect 60 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 61 | github.com/moby/sys/sequential v0.6.0 // indirect 62 | github.com/moby/sys/user v0.4.0 // indirect 63 | github.com/moby/sys/userns v0.1.0 // indirect 64 | github.com/moby/term v0.5.0 // indirect 65 | github.com/morikuni/aec v1.0.0 // indirect 66 | github.com/opencontainers/go-digest v1.0.0 // indirect 67 | github.com/opencontainers/image-spec v1.1.1 // indirect 68 | github.com/pkg/errors v0.9.1 // indirect 69 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 70 | github.com/pmezard/go-difflib v1.0.0 // indirect 71 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 72 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 73 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 74 | github.com/sirupsen/logrus v1.9.3 // indirect 75 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 76 | github.com/stretchr/testify v1.10.0 // indirect 77 | github.com/tklauser/go-sysconf v0.3.12 // indirect 78 | github.com/tklauser/numcpus v0.6.1 // indirect 79 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 80 | github.com/zeebo/errs v1.4.0 // indirect 81 | go.opencensus.io v0.24.0 // indirect 82 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 83 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 84 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 85 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 86 | go.opentelemetry.io/otel v1.35.0 // indirect 87 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect 88 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 89 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 90 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 91 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 92 | golang.org/x/crypto v0.38.0 // indirect 93 | golang.org/x/net v0.40.0 // indirect 94 | golang.org/x/oauth2 v0.30.0 // indirect 95 | golang.org/x/sync v0.14.0 // indirect 96 | golang.org/x/sys v0.33.0 // indirect 97 | golang.org/x/text v0.25.0 // indirect 98 | golang.org/x/time v0.11.0 // indirect 99 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 100 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 101 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 102 | google.golang.org/grpc v1.72.1 // indirect 103 | google.golang.org/protobuf v1.36.6 // indirect 104 | gopkg.in/yaml.v3 v3.0.1 // indirect 105 | ) 106 | -------------------------------------------------------------------------------- /examples/helloworld/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | // Simple sample application that shows how to use the Spanner Go sql driver. 27 | // 28 | // Execute the sample with the command `go run main.go` from this directory. 29 | func helloWorld(projectId, instanceId, databaseId string) error { 30 | ctx := context.Background() 31 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 32 | if err != nil { 33 | return fmt.Errorf("failed to open database connection: %v", err) 34 | } 35 | defer db.Close() 36 | 37 | rows, err := db.QueryContext(ctx, "SELECT 'Hello World!'") 38 | if err != nil { 39 | return fmt.Errorf("failed to execute query: %v", err) 40 | } 41 | defer rows.Close() 42 | 43 | var msg string 44 | for rows.Next() { 45 | if err := rows.Scan(&msg); err != nil { 46 | return fmt.Errorf("failed to scan row values: %v", err) 47 | } 48 | fmt.Printf("%s\n", msg) 49 | } 50 | if err := rows.Err(); err != nil { 51 | return fmt.Errorf("failed to execute query: %v", err) 52 | } 53 | return nil 54 | } 55 | 56 | func main() { 57 | examples.RunSampleOnEmulator(helloWorld) 58 | } 59 | -------------------------------------------------------------------------------- /examples/last-insert-id/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | var createSequenceStatement = `CREATE SEQUENCE SingerIdSequence OPTIONS ( 27 | sequence_kind="bit_reversed_positive" 28 | )` 29 | var createTableStatement = `CREATE TABLE Singers ( 30 | SingerId INT64 DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE SingerIdSequence)), 31 | Name STRING(MAX) 32 | ) PRIMARY KEY (SingerId)` 33 | 34 | // Example using LastInsertId with Spanner. 35 | // 36 | // Execute the sample with the command `go run main.go` from this directory. 37 | func lastInsertId(projectId, instanceId, databaseId string) error { 38 | ctx := context.Background() 39 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 40 | if err != nil { 41 | return err 42 | } 43 | defer db.Close() 44 | 45 | // Insert a new Singer row. Spanner will generate a new primary key value from 46 | // the bit-reversed sequence and return this value to the client. 47 | // 48 | // NOTE: ExecContext can only be used for INSERT statements with a THEN RETURN clause 49 | // that return exactly ONE row and ONE column of type INT64. The Spanner database/sql 50 | // driver disallows execution of any other type of DML statement with a THEN RETURN 51 | // clause using ExecContext. Use QueryContext for these statements instead. 52 | res, err := db.ExecContext(ctx, "INSERT INTO Singers (Name) VALUES (@name) THEN RETURN SingerId", "Bruce Allison") 53 | if err != nil { 54 | return fmt.Errorf("failed to insert test record: %v", err) 55 | } 56 | rowsInserted, err := res.RowsAffected() 57 | if err != nil { 58 | return fmt.Errorf("failed to get rows affected: %v", err) 59 | } 60 | // We can get the generated ID by calling LastInsertId(). 61 | singerId, err := res.LastInsertId() 62 | if err != nil { 63 | return fmt.Errorf("failed to get LastInsertId: %v", err) 64 | } 65 | 66 | fmt.Printf("Inserted %v singer with auto-generated id %v\n", rowsInserted, singerId) 67 | 68 | return nil 69 | } 70 | 71 | func main() { 72 | examples.RunSampleOnEmulator(lastInsertId, createSequenceStatement, createTableStatement) 73 | } 74 | -------------------------------------------------------------------------------- /examples/mutations/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "time" 22 | 23 | "cloud.google.com/go/spanner" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/googleapis/go-sql-spanner/examples" 26 | ) 27 | 28 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 29 | 30 | // Example that shows how to use Mutations to insert data in a Cloud Spanner database using the Go sql driver. 31 | // Mutations can be more efficient than DML statements for bulk insert/update operations. 32 | // 33 | // Execute the sample with the command `go run main.go` from this directory. 34 | func mutations(projectId, instanceId, databaseId string) error { 35 | ctx := context.Background() 36 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 37 | if err != nil { 38 | return err 39 | } 40 | defer db.Close() 41 | 42 | // Get a connection so that we can get access to the Spanner specific connection interface SpannerConn. 43 | conn, err := db.Conn(ctx) 44 | if err != nil { 45 | return err 46 | } 47 | defer conn.Close() 48 | 49 | // Mutations can be written outside an explicit transaction using SpannerConn#Apply. 50 | var commitTimestamp time.Time 51 | if err := conn.Raw(func(driverConn interface{}) error { 52 | spannerConn, ok := driverConn.(spannerdriver.SpannerConn) 53 | if !ok { 54 | return fmt.Errorf("unexpected driver connection %v, expected SpannerConn", driverConn) 55 | } 56 | commitTimestamp, err = spannerConn.Apply(ctx, []*spanner.Mutation{ 57 | spanner.Insert("Singers", []string{"SingerId", "Name"}, []interface{}{int64(1), "Richard Moore"}), 58 | spanner.Insert("Singers", []string{"SingerId", "Name"}, []interface{}{int64(2), "Alice Henderson"}), 59 | }) 60 | return err 61 | }); err != nil { 62 | return err 63 | } 64 | fmt.Printf("The transaction with two singer mutations was committed at %v\n", commitTimestamp) 65 | 66 | // Mutations can also be executed as part of a read/write transaction. 67 | // Note: The transaction is started using the connection that we had obtained. This is necessary in order to 68 | // ensure that the conn.Raw call below will use the same connection as the one that just started the transaction. 69 | tx, err := conn.BeginTx(ctx, &sql.TxOptions{}) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Get the name of a singer and update it using a mutation. 75 | id := int64(1) 76 | row := tx.QueryRowContext(ctx, "SELECT Name FROM Singers WHERE SingerId=@id", id) 77 | var name string 78 | if err := row.Scan(&name); err != nil { 79 | return err 80 | } 81 | if err := conn.Raw(func(driverConn interface{}) error { 82 | spannerConn, ok := driverConn.(spannerdriver.SpannerConn) 83 | if !ok { 84 | return fmt.Errorf("unexpected driver connection %v, expected SpannerConn", driverConn) 85 | } 86 | return spannerConn.BufferWrite([]*spanner.Mutation{ 87 | spanner.Update("Singers", []string{"SingerId", "Name"}, []interface{}{id, name + "-Henderson"}), 88 | }) 89 | }); err != nil { 90 | return err 91 | } 92 | if err := tx.Commit(); err != nil { 93 | return err 94 | } 95 | fmt.Print("Updated the name of the first singer\n") 96 | 97 | // Read back the updated row. 98 | row = db.QueryRowContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = @id", id) 99 | if err := row.Err(); err != nil { 100 | return err 101 | } 102 | if err := row.Scan(&id, &name); err != nil { 103 | return err 104 | } 105 | fmt.Printf("Updated singer: %v %v\n", id, name) 106 | 107 | return nil 108 | } 109 | 110 | func main() { 111 | examples.RunSampleOnEmulator(mutations, createTableStatement) 112 | } 113 | -------------------------------------------------------------------------------- /examples/partitioned-dml/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX), Active BOOL NOT NULL DEFAULT (TRUE)) PRIMARY KEY (SingerId)" 28 | 29 | // Example for executing a Partitioned DML transaction on a Google Cloud Spanner database. 30 | // See https://cloud.google.com/spanner/docs/dml-partitioned for more information on Partitioned DML. 31 | // 32 | // Execute the sample with the command `go run main.go` from this directory. 33 | func partitionedDml(projectId, instanceId, databaseId string) error { 34 | ctx := context.Background() 35 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 36 | if err != nil { 37 | return err 38 | } 39 | defer db.Close() 40 | 41 | // First insert a couple of test records that we will update and delete using Partitioned DML. 42 | conn, err := db.Conn(ctx) 43 | if err != nil { 44 | return fmt.Errorf("failed to get a connection: %v", err) 45 | } 46 | if err := conn.Raw(func(driverConn interface{}) error { 47 | _, err := driverConn.(spannerdriver.SpannerConn).Apply(ctx, []*spanner.Mutation{ 48 | spanner.InsertOrUpdateMap("Singers", map[string]interface{}{"SingerId": 1, "Name": "Singer 1"}), 49 | spanner.InsertOrUpdateMap("Singers", map[string]interface{}{"SingerId": 2, "Name": "Singer 2"}), 50 | spanner.InsertOrUpdateMap("Singers", map[string]interface{}{"SingerId": 3, "Name": "Singer 3"}), 51 | spanner.InsertOrUpdateMap("Singers", map[string]interface{}{"SingerId": 4, "Name": "Singer 4"}), 52 | }) 53 | return err 54 | }); err != nil { 55 | return fmt.Errorf("failed to insert test records: %v", err) 56 | } 57 | 58 | // Now update all records in the Singers table using Partitioned DML. 59 | if _, err := conn.ExecContext(ctx, "SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"); err != nil { 60 | return fmt.Errorf("failed to change DML mode to Partitioned_Non_Atomic: %v", err) 61 | } 62 | res, err := conn.ExecContext(ctx, "UPDATE Singers SET Active = FALSE WHERE TRUE") 63 | if err != nil { 64 | return fmt.Errorf("failed to execute UPDATE statement: %v", err) 65 | } 66 | affected, err := res.RowsAffected() 67 | if err != nil { 68 | return fmt.Errorf("failed to get affected rows: %v", err) 69 | } 70 | 71 | // Partitioned DML returns the minimum number of records that were affected. 72 | fmt.Printf("Updated %v records using Partitioned DML\n", affected) 73 | 74 | // Closing the connection will return it to the connection pool. The DML mode will automatically be reset to the 75 | // default TRANSACTIONAL mode when the connection is returned to the pool, so we do not need to change it back 76 | // manually. 77 | _ = conn.Close() 78 | 79 | // The AutoCommitDMLMode can also be specified as an ExecOption for a single statement. 80 | conn, err = db.Conn(ctx) 81 | if err != nil { 82 | return fmt.Errorf("failed to get a connection: %v", err) 83 | } 84 | res, err = conn.ExecContext(ctx, "DELETE FROM Singers WHERE NOT Active", 85 | spannerdriver.ExecOptions{AutocommitDMLMode: spannerdriver.PartitionedNonAtomic}) 86 | if err != nil { 87 | return fmt.Errorf("failed to execute DELETE statement: %v", err) 88 | } 89 | affected, err = res.RowsAffected() 90 | if err != nil { 91 | return fmt.Errorf("failed to get affected rows: %v", err) 92 | } 93 | 94 | // Partitioned DML returns the minimum number of records that were affected. 95 | fmt.Printf("Deleted %v records using Partitioned DML\n", affected) 96 | 97 | return nil 98 | } 99 | 100 | func main() { 101 | examples.RunSampleOnEmulator(partitionedDml, createTableStatement) 102 | } 103 | -------------------------------------------------------------------------------- /examples/partitioned-queries-and-data-boost/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "sync" 22 | "sync/atomic" 23 | 24 | "cloud.google.com/go/spanner" 25 | spannerdriver "github.com/googleapis/go-sql-spanner" 26 | "github.com/googleapis/go-sql-spanner/examples" 27 | ) 28 | 29 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 30 | 31 | // Sample showing how to use a batch read-only transaction to partition a query and 32 | // execute each of the partitions. This sample also shows how to use DataBoost. 33 | // See https://cloud.google.com/spanner/docs/databoost/databoost-overview for more 34 | // information. 35 | // 36 | // You can also use the AutoPartition option with the Spanner database/sql driver 37 | // to instruct the driver to automatically partition a query, execute each 38 | // partition in parallel, and return the results as a single result stream. 39 | // See the auto-partition-queries sample for more information: 40 | // https://github.com/googleapis/go-sql-spanner/blob/-/examples/auto-partition-queries 41 | // 42 | // Execute the sample with the command `go run main.go` from this directory. 43 | func readOnlyTransaction(projectId, instanceId, databaseId string) error { 44 | ctx := context.Background() 45 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 46 | if err != nil { 47 | return err 48 | } 49 | defer db.Close() 50 | 51 | // Insert 10 test rows. 52 | if err := insertTestRows(db, 10); err != nil { 53 | return err 54 | } 55 | 56 | // Start a batch read-only transaction. 57 | tx, err := spannerdriver.BeginBatchReadOnlyTransaction(ctx, db, spannerdriver.BatchReadOnlyTransactionOptions{}) 58 | if err != nil { 59 | return err 60 | } 61 | // Committing or rolling back a read-only transaction will not execute an actual Commit or Rollback 62 | // on the database, but it is needed in order to release the resources that are held by the read-only 63 | // transaction. 64 | defer func() { _ = tx.Commit() }() 65 | 66 | // Partition a query and get the partitions. 67 | row := tx.QueryRowContext(ctx, "select * from singers", spannerdriver.ExecOptions{ 68 | PartitionedQueryOptions: spannerdriver.PartitionedQueryOptions{ 69 | // Set this option to true to only partition the query. 70 | // The query will return one row containing a spannerdriver.PartitionedQuery 71 | PartitionQuery: true, 72 | }, 73 | QueryOptions: spanner.QueryOptions{ 74 | // Set DataBoostEnabled to true to enable DataBoost. 75 | // See https://cloud.google.com/spanner/docs/databoost/databoost-overview 76 | // for more information. 77 | DataBoostEnabled: true, 78 | }, 79 | }) 80 | var pq spannerdriver.PartitionedQuery 81 | if err := row.Scan(&pq); err != nil { 82 | return err 83 | } 84 | 85 | fmt.Printf("PartitionQuery returned %v partitions\n", len(pq.Partitions)) 86 | 87 | // Execute all partitions in parallel. 88 | // Each partition will be executed on a separate connection. 89 | // The BatchReadOnlyTransaction must be kept open while the 90 | // partitions are being executed. 91 | count := atomic.Int64{} 92 | var wg sync.WaitGroup 93 | for index := range pq.Partitions { 94 | partitionIndex := index 95 | wg.Add(1) 96 | go func() { 97 | defer wg.Done() 98 | rows, err := pq.Execute(ctx, partitionIndex, db) 99 | if err != nil { 100 | fmt.Printf("executing partition failed: %v", err) 101 | return 102 | } 103 | partitionCount := 0 104 | for rows.Next() { 105 | partitionCount++ 106 | count.Add(1) 107 | } 108 | _ = rows.Close() 109 | fmt.Printf("Partition %v found %v rows\n", partitionIndex, partitionCount) 110 | }() 111 | } 112 | wg.Wait() 113 | fmt.Printf("Executing all partitions returned a total of %v rows\n", count.Load()) 114 | 115 | return nil 116 | } 117 | 118 | func main() { 119 | examples.RunSampleOnEmulator(readOnlyTransaction, createTableStatement) 120 | } 121 | 122 | func insertTestRows(db *sql.DB, numRows int) error { 123 | ctx := context.Background() 124 | 125 | // Insert a few test rows. 126 | tx, err := db.BeginTx(ctx, &sql.TxOptions{}) 127 | if err != nil { 128 | return err 129 | } 130 | // Insert the test rows in one batch. 131 | if _, err := tx.ExecContext(ctx, "start batch dml"); err != nil { 132 | tx.Rollback() 133 | return err 134 | } 135 | stmt, err := tx.PrepareContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (?, ?)") 136 | if err != nil { 137 | tx.Rollback() 138 | return err 139 | } 140 | for i := 0; i < numRows; i++ { 141 | if _, err := stmt.ExecContext(ctx, int64(i), fmt.Sprintf("Test %v", i)); err != nil { 142 | _, _ = tx.ExecContext(ctx, "abort batch") 143 | tx.Rollback() 144 | return err 145 | } 146 | } 147 | if _, err := tx.ExecContext(ctx, "run batch"); err != nil { 148 | tx.Rollback() 149 | return err 150 | } 151 | return tx.Commit() 152 | } 153 | -------------------------------------------------------------------------------- /examples/query-parameters/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 27 | 28 | // Sample showing how to use both positional and named query parameters. 29 | // Using query parameters instead of literal values in SQL statements 30 | // improves the execution time of those statements, as Spanner can cache 31 | // and re-use the execution plan for those statements. 32 | // 33 | // Execute the sample with the command `go run main.go` from this directory. 34 | func queryParameters(projectId, instanceId, databaseId string) error { 35 | ctx := context.Background() 36 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 37 | if err != nil { 38 | return err 39 | } 40 | defer db.Close() 41 | 42 | // The Spanner database/sql driver supports both named parameters and positional 43 | // parameters. This DML statement uses named parameters. 44 | _, err = db.ExecContext(ctx, 45 | "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 46 | sql.Named("id", int64(1)), 47 | sql.Named("name", "Bruce Allison")) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // You can also use '@name' style parameters in the SQL statement 53 | // in combination with positional arguments in Go. 54 | _, err = db.ExecContext(ctx, 55 | "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 56 | int64(2), 57 | "Alice Henderson") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // SQL statements can also contain positional parameters using '?'. 63 | row := db.QueryRowContext(ctx, 64 | "SELECT Name FROM Singers WHERE SingerId = ?", 65 | int64(1)) 66 | var name string 67 | if err := row.Scan(&name); err != nil { 68 | return err 69 | } 70 | fmt.Printf("Found singer %s\n", name) 71 | 72 | return nil 73 | } 74 | 75 | func main() { 76 | examples.RunSampleOnEmulator(queryParameters, createTableStatement) 77 | } 78 | -------------------------------------------------------------------------------- /examples/read-only-transaction-with-options/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "time" 22 | 23 | "cloud.google.com/go/spanner" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/googleapis/go-sql-spanner/examples" 26 | ) 27 | 28 | // Sample showing how to execute a read-only transaction with specific options on a Spanner database. 29 | // 30 | // Execute the sample with the command `go run main.go` from this directory. 31 | func readOnlyTransactionWithOptions(projectId, instanceId, databaseId string) error { 32 | ctx := context.Background() 33 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 34 | if err != nil { 35 | return err 36 | } 37 | defer db.Close() 38 | 39 | // Start a read-only transaction on the Spanner database using a staleness option. 40 | tx, err := spannerdriver.BeginReadOnlyTransaction( 41 | ctx, db, spannerdriver.ReadOnlyTransactionOptions{ 42 | TimestampBound: spanner.ExactStaleness(time.Second * 10), 43 | }) 44 | if err != nil { 45 | return err 46 | } 47 | fmt.Println("Started a read-only transaction with a staleness option") 48 | 49 | // Use the read-only transaction... 50 | 51 | // Committing or rolling back a read-only transaction will not execute an actual Commit or Rollback 52 | // on the database, but it is needed in order to release the resources that are held by the read-only 53 | // transaction. Failing to do so will cause the connection that is used for the transaction to be leaked. 54 | if err := tx.Commit(); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func main() { 62 | examples.RunSampleOnEmulator(readOnlyTransactionWithOptions) 63 | } 64 | -------------------------------------------------------------------------------- /examples/read-only-transactions/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 27 | 28 | // Sample showing how to execute a read-only transaction on a Spanner database. 29 | // 30 | // Execute the sample with the command `go run main.go` from this directory. 31 | func readOnlyTransaction(projectId, instanceId, databaseId string) error { 32 | ctx := context.Background() 33 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 34 | if err != nil { 35 | return err 36 | } 37 | defer db.Close() 38 | 39 | // Start a read-only transaction on the Spanner database. 40 | tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) 41 | if err != nil { 42 | return err 43 | } 44 | // Committing or rolling back a read-only transaction will not execute an actual Commit or Rollback 45 | // on the database, but it is needed in order to release the resources that are held by the read-only 46 | // transaction. 47 | defer tx.Commit() 48 | 49 | // Verify that the Singers table is empty. 50 | var c int64 51 | if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 52 | return err 53 | } 54 | fmt.Printf("Singers count is initially %v\n", c) 55 | 56 | // Now insert a new record in the Singers table using an implicit transaction. 57 | // This change will be applied immediately to the database. 58 | if _, err := db.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", int64(1), "Bruce Allison"); err != nil { 59 | return err 60 | } 61 | 62 | // The read-only transaction was started before the row was inserted and will continue to read data at a timestamp 63 | // that was before the row was inserted. It will therefore not see the new record. 64 | if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 65 | return err 66 | } 67 | fmt.Printf("Singers count as seen in the read-only transaction is %v\n", c) 68 | 69 | // Start a new read-only transaction on the Spanner database. This transaction will be started after the new test 70 | // row was inserted, and the test row should now be visible to the read-only transaction. 71 | tx, err = db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) 72 | if err != nil { 73 | return err 74 | } 75 | defer tx.Commit() 76 | if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 77 | return err 78 | } 79 | fmt.Printf("Singers count in a new read-only transaction is %v\n", c) 80 | 81 | return nil 82 | } 83 | 84 | func main() { 85 | examples.RunSampleOnEmulator(readOnlyTransaction, createTableStatement) 86 | } 87 | -------------------------------------------------------------------------------- /examples/run-transaction/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "sync" 22 | 23 | "cloud.google.com/go/spanner" 24 | spannerdriver "github.com/googleapis/go-sql-spanner" 25 | "github.com/googleapis/go-sql-spanner/examples" 26 | ) 27 | 28 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 29 | 30 | // Example for running a read/write transaction in a retry loop on a Spanner database. 31 | // The RunTransaction function automatically retries Aborted transactions using a 32 | // retry loop. This guarantees that the transaction will not fail with an 33 | // ErrAbortedDueToConcurrentModification. 34 | // 35 | // Execute the sample with the command `go run main.go` from this directory. 36 | func runTransaction(projectId, instanceId, databaseId string) error { 37 | ctx := context.Background() 38 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 39 | if err != nil { 40 | return err 41 | } 42 | defer db.Close() 43 | 44 | // Insert a new record that will be updated by multiple different transactions at the same time. 45 | _, err = db.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 123, "Bruce Allison") 46 | if err != nil { 47 | return err 48 | } 49 | 50 | numTransactions := 10 51 | errors := make([]error, numTransactions) 52 | wg := sync.WaitGroup{} 53 | for i := 0; i < numTransactions; i++ { 54 | index := i 55 | wg.Add(1) 56 | go func() { 57 | defer wg.Done() 58 | // Run a transaction that adds an index to the name of the singer. 59 | // As we are doing this multiple times in parallel, these transactions 60 | // will be aborted and retried by Spanner multiple times. The end result 61 | // will still be that all transactions succeed and the name contains all 62 | // indexes in an undefined order. 63 | errors[index] = spannerdriver.RunTransactionWithOptions(ctx, db, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error { 64 | // Query the singer in the transaction. This will take a lock on the row and guarantee that 65 | // the value that we read is still the same when the transaction is committed. If not, Spanner 66 | // will abort the transaction, and the transaction will be retried. 67 | row := tx.QueryRowContext(ctx, "select Name from Singers where SingerId=@id", 123) 68 | var name string 69 | if err := row.Scan(&name); err != nil { 70 | return err 71 | } 72 | // Update the name with the transaction index. 73 | name = fmt.Sprintf("%s %d", name, index) 74 | res, err := tx.ExecContext(ctx, "update Singers set Name=@name where SingerId=@id", name, 123) 75 | if err != nil { 76 | return err 77 | } 78 | affected, err := res.RowsAffected() 79 | if err != nil { 80 | return err 81 | } 82 | if affected != 1 { 83 | return fmt.Errorf("unexpected affected row count: %d", affected) 84 | } 85 | return nil 86 | }, spanner.TransactionOptions{TransactionTag: "sample_transaction"}) 87 | }() 88 | } 89 | wg.Wait() 90 | 91 | // The name of the singer should now contain all the indexes that were added in the 92 | // transactions above in arbitrary order. 93 | row := db.QueryRowContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = ?", 123) 94 | if err := row.Err(); err != nil { 95 | return err 96 | } 97 | var id int64 98 | var name string 99 | if err := row.Scan(&id, &name); err != nil { 100 | return err 101 | } 102 | fmt.Printf("Singer after %d transactions: %v %v\n", numTransactions, id, name) 103 | 104 | return nil 105 | } 106 | 107 | func main() { 108 | examples.RunSampleOnEmulator(runTransaction, createTableStatement) 109 | } 110 | -------------------------------------------------------------------------------- /examples/samples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package examples 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestRunSamples(t *testing.T) { 28 | items, _ := os.ReadDir(".") 29 | for _, item := range items { 30 | if item.IsDir() { 31 | mainFile, err := os.Stat(filepath.Join(item.Name(), "main.go")) 32 | if err != nil { 33 | t.Fatalf("failed to check for main.go file in %v: %v", item.Name(), err) 34 | } 35 | if !mainFile.IsDir() { 36 | t.Run(item.Name(), func(t *testing.T) { 37 | // Verify that we can run the sample. 38 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 39 | cmd := exec.CommandContext(ctx, "go", "run", "-race", ".") 40 | cmd.Dir = item.Name() 41 | var stderr bytes.Buffer 42 | cmd.Stderr = &stderr 43 | if err := cmd.Run(); err != nil { 44 | cancel() 45 | t.Fatalf("failed to run sample %v: %v", item.Name(), stderr.String()) 46 | } 47 | cancel() 48 | }) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/stale-reads/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | "time" 22 | 23 | _ "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 28 | 29 | // Example for executing read-only transactions with a different timestamp bound than Strong. Both single-use and 30 | // multi-use read-only transactions can be set up to use a different timestamp bound by executing a 31 | // `SET READ_ONLY_STALENESS=` statement before executing the read-only transaction. `` must 32 | // be one of the following: 33 | // 34 | // 'READ_TIMESTAMP yyyy-mm-ddTHH:mi:ssZ' 35 | // 'MIN_READ_TIMESTAMP yyyy-mm-ddTHH:mi:ssZ' (only for single queries outside a multi-use read-only transaction) 36 | // 'EXACT_STALENESS s|ms|us|ns' (example: 'EXACT_STALENESS 10s' for 10 seconds) 37 | // 'MAX_STALENESS s|ms|us|ns' (only for single queries outside a multi-use read-only transaction) 38 | // 39 | // Execute the sample with the command `go run main.go` from this directory. 40 | func staleReads(projectId, instanceId, databaseId string) error { 41 | ctx := context.Background() 42 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 43 | if err != nil { 44 | return err 45 | } 46 | defer db.Close() 47 | 48 | // First get the current time on the server. This timestamp can be used to execute 49 | // a stale read at an exact timestamp that lays before any data was inserted to the 50 | // test database. 51 | var t time.Time 52 | if err := db.QueryRowContext(ctx, "SELECT CURRENT_TIMESTAMP").Scan(&t); err != nil { 53 | return err 54 | } 55 | // Insert a sample row. 56 | if _, err := db.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", int64(1), "Bruce Allison"); err != nil { 57 | return err 58 | } 59 | 60 | // Execute a single read using the timestamp that was gotten before we inserted any data. 61 | // We need to get a connection from the pool in order to be able to set the read-only 62 | // staleness on that connection. The connection will use the specified read-only staleness 63 | // until it is set to a different value, or until it is returned to the pool. 64 | conn, err := db.Conn(ctx) 65 | if err != nil { 66 | return err 67 | } 68 | defer conn.Close() 69 | if _, err := conn.ExecContext(ctx, fmt.Sprintf("SET READ_ONLY_STALENESS='READ_TIMESTAMP %s'", t.Format(time.RFC3339Nano))); err != nil { 70 | return err 71 | } 72 | var c int64 73 | if err := conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 74 | return err 75 | } 76 | // The count at the specified timestamp should be zero, as it was before the test data was inserted. 77 | fmt.Printf("Singers count at timestamp %s was: %v\n", t.Format(time.RFC3339Nano), c) 78 | 79 | // Get a new timestamp that is after the data was inserted and use that as the read-timestamp. 80 | if err := db.QueryRowContext(ctx, "SELECT CURRENT_TIMESTAMP").Scan(&t); err != nil { 81 | return err 82 | } 83 | if _, err := conn.ExecContext(ctx, fmt.Sprintf("SET READ_ONLY_STALENESS='READ_TIMESTAMP %s'", t.Format(time.RFC3339Nano))); err != nil { 84 | return err 85 | } 86 | // Now start a read-only transaction that will read at the given timestamp. 87 | tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) 88 | if err != nil { 89 | return err 90 | } 91 | // This should now find the singer we inserted, as the read timestamp is after the moment we 92 | // inserted the test data. 93 | if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil { 94 | return err 95 | } 96 | fmt.Printf("Singers count at timestamp %s was: %v\n", t.Format(time.RFC3339Nano), c) 97 | 98 | // Commit the read-only transaction and close the connection to release the resources they are using. 99 | // Committing or rolling back a read-only transaction will execute an actual Commit or Rollback on the database, 100 | // but it is needed in order to release the resources that are held by the read-only transaction. 101 | if err := tx.Commit(); err != nil { 102 | return err 103 | } 104 | _ = conn.Close() 105 | 106 | return nil 107 | } 108 | 109 | func main() { 110 | examples.RunSampleOnEmulator(staleReads, createTableStatement) 111 | } 112 | -------------------------------------------------------------------------------- /examples/struct-types/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | // Example for executing a query with struct arguments described in: 27 | // 28 | // * https://cloud.google.com/spanner/docs/structs 29 | // * https://pkg.go.dev/cloud.google.com/go/spanner#hdr-Structs 30 | func structTypes(projectId, instanceId, databaseId string) error { 31 | ctx := context.Background() 32 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 33 | if err != nil { 34 | return fmt.Errorf("failed to open database connection: %v", err) 35 | } 36 | defer db.Close() 37 | 38 | type Entry struct { 39 | ID int64 40 | Name string 41 | } 42 | 43 | entries := []Entry{ 44 | {ID: 0, Name: "Hello"}, 45 | {ID: 1, Name: "World"}, 46 | } 47 | 48 | rows, err := db.QueryContext(ctx, "SELECT id, name FROM UNNEST(@entries)", entries) 49 | if err != nil { 50 | return fmt.Errorf("failed to execute query: %v", err) 51 | } 52 | defer rows.Close() 53 | 54 | for rows.Next() { 55 | var id int64 56 | var name string 57 | 58 | if err := rows.Scan(&id, &name); err != nil { 59 | return fmt.Errorf("failed to scan row values: %v", err) 60 | } 61 | fmt.Printf("%v %v\n", id, name) 62 | } 63 | if err := rows.Err(); err != nil { 64 | return fmt.Errorf("failed to execute query: %v", err) 65 | } 66 | return nil 67 | } 68 | 69 | func main() { 70 | examples.RunSampleOnEmulator(structTypes) 71 | } 72 | -------------------------------------------------------------------------------- /examples/tags/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 28 | 29 | // Example for using transaction tags and statement tags through SQL statements. 30 | // 31 | // Tags can also be set programmatically using spannerdriver.RunTransactionWithOptions 32 | // and the spannerdriver.ExecOptions. 33 | // 34 | // Execute the sample with the command `go run main.go` from this directory. 35 | func tagsWithSqlStatements(projectId, instanceId, databaseId string) error { 36 | fmt.Println("Running sample for setting tags with SQL statements") 37 | 38 | ctx := context.Background() 39 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 40 | if err != nil { 41 | return err 42 | } 43 | defer db.Close() 44 | 45 | // Obtain a connection for the database in order to ensure that we 46 | // set the transaction tag on the same connection as the connection 47 | // that will execute the transaction. 48 | conn, err := db.Conn(ctx) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Set a transaction tag on the connection and start a transaction. 54 | // This transaction tag will be applied to the next transaction that 55 | // is executed by this connection. The transaction tag is automatically 56 | // included with all statements of that transaction. 57 | if _, err := conn.ExecContext(ctx, "set transaction_tag = 'my_transaction_tag'"); err != nil { 58 | return err 59 | } 60 | fmt.Println("Executing transaction with transaction tag 'my_transaction_tag'") 61 | tx, err := conn.BeginTx(ctx, &sql.TxOptions{}) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | // Set a statement tag and insert a new record using the transaction that we just started. 67 | if _, err := tx.ExecContext(ctx, "set statement_tag = 'insert_singer'"); err != nil { 68 | _ = tx.Rollback() 69 | return err 70 | } 71 | fmt.Println("Executing statement with tag 'insert_singer'") 72 | _, err = tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 123, "Bruce Allison") 73 | if err != nil { 74 | _ = tx.Rollback() 75 | return err 76 | } 77 | 78 | // Set another statement tag and execute a query. 79 | if _, err := tx.ExecContext(ctx, "set statement_tag = 'select_singer'"); err != nil { 80 | _ = tx.Rollback() 81 | return err 82 | } 83 | fmt.Println("Executing statement with tag 'select_singer'") 84 | rows, err := tx.QueryContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = ?", 123) 85 | if err != nil { 86 | _ = tx.Rollback() 87 | return err 88 | } 89 | var ( 90 | id int64 91 | name string 92 | ) 93 | for rows.Next() { 94 | if err := rows.Scan(&id, &name); err != nil { 95 | _ = tx.Rollback() 96 | return err 97 | } 98 | fmt.Printf("Found singer: %v %v\n", id, name) 99 | } 100 | if err := rows.Err(); err != nil { 101 | _ = tx.Rollback() 102 | return err 103 | } 104 | _ = rows.Close() 105 | if err := tx.Commit(); err != nil { 106 | return err 107 | } 108 | 109 | fmt.Println("Finished transaction with tag 'my_transaction_tag'") 110 | 111 | return nil 112 | } 113 | 114 | // tagsProgrammatically shows how to set transaction tags and statement tags 115 | // programmatically. 116 | // 117 | // Note: It is not recommended to mix using SQL statements and passing in 118 | // tags or other QueryOptions programmatically. 119 | func tagsProgrammatically(projectId, instanceId, databaseId string) error { 120 | fmt.Println("Running sample for setting tags programmatically") 121 | 122 | ctx := context.Background() 123 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 124 | if err != nil { 125 | return err 126 | } 127 | defer db.Close() 128 | 129 | // Use RunTransactionWithOptions to set a transaction tag programmatically. 130 | fmt.Println("Executing transaction with transaction tag 'my_transaction_tag'") 131 | if err := spannerdriver.RunTransactionWithOptions(ctx, db, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error { 132 | fmt.Println("Executing statement with tag 'insert_singer'") 133 | // Pass in a value of spanner.QueryOptions to specify the options that should be used for a DML statement. 134 | _, err = tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", spannerdriver.ExecOptions{QueryOptions: spanner.QueryOptions{RequestTag: "insert_singer"}}, 123, "Bruce Allison") 135 | if err != nil { 136 | return err 137 | } 138 | 139 | fmt.Println("Executing statement with tag 'select_singer'") 140 | // Pass in a value of spanner.QueryOptions to specify the options that should be used for a query. 141 | rows, err := tx.QueryContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = ?", spannerdriver.ExecOptions{QueryOptions: spanner.QueryOptions{RequestTag: "select_singer"}}, 123) 142 | if err != nil { 143 | return err 144 | } 145 | var ( 146 | id int64 147 | name string 148 | ) 149 | for rows.Next() { 150 | if err := rows.Scan(&id, &name); err != nil { 151 | _ = tx.Rollback() 152 | return err 153 | } 154 | fmt.Printf("Found singer: %v %v\n", id, name) 155 | } 156 | if err := rows.Err(); err != nil { 157 | return err 158 | } 159 | _ = rows.Close() 160 | 161 | return nil 162 | }, spanner.TransactionOptions{TransactionTag: "my_transaction_tag"}); err != nil { 163 | return err 164 | } 165 | 166 | fmt.Println("Finished transaction with tag 'my_transaction_tag'") 167 | 168 | return nil 169 | } 170 | 171 | func main() { 172 | examples.RunSampleOnEmulator(tagsWithSqlStatements, createTableStatement) 173 | fmt.Println() 174 | examples.RunSampleOnEmulator(tagsProgrammatically, createTableStatement) 175 | } 176 | -------------------------------------------------------------------------------- /examples/transactions/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | _ "github.com/googleapis/go-sql-spanner" 23 | "github.com/googleapis/go-sql-spanner/examples" 24 | ) 25 | 26 | var createTableStatement = "CREATE TABLE Singers (SingerId INT64, Name STRING(MAX)) PRIMARY KEY (SingerId)" 27 | 28 | // Example for executing a read/write transaction on a Google Cloud Spanner database. 29 | // 30 | // Execute the sample with the command `go run main.go` from this directory. 31 | func transaction(projectId, instanceId, databaseId string) error { 32 | ctx := context.Background() 33 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 34 | if err != nil { 35 | return err 36 | } 37 | defer db.Close() 38 | 39 | tx, err := db.BeginTx(ctx, &sql.TxOptions{}) // The default options will start a read/write transaction. 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Insert a new record using the transaction that we just started. 45 | _, err = tx.ExecContext(ctx, "INSERT INTO Singers (SingerId, Name) VALUES (@id, @name)", 123, "Bruce Allison") 46 | if err != nil { 47 | _ = tx.Rollback() 48 | return err 49 | } 50 | 51 | // The row that we inserted will be readable for the same transaction that started it. 52 | rows, err := tx.QueryContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = ?", 123) 53 | if err != nil { 54 | _ = tx.Rollback() 55 | return err 56 | } 57 | var ( 58 | id int64 59 | name string 60 | ) 61 | for rows.Next() { 62 | if err := rows.Scan(&id, &name); err != nil { 63 | _ = tx.Rollback() 64 | return err 65 | } 66 | fmt.Printf("Found singer: %v %v\n", id, name) 67 | } 68 | if err := rows.Err(); err != nil { 69 | _ = tx.Rollback() 70 | return err 71 | } 72 | _ = rows.Close() 73 | 74 | // The row that has been inserted using the transaction is not readable for other transactions before the 75 | // transaction has been committed. Note that the following statement does not use `tx` to try to read the 76 | // row, but executes a read directly on `db`. This operation will use a separate single use read-only transaction. 77 | row := db.QueryRowContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = @id", 123) 78 | if err := row.Err(); err != nil { 79 | _ = tx.Rollback() 80 | return err 81 | } 82 | // This should return sql.ErrNoRows as the row should not be visible to other transactions. 83 | if err := row.Scan(&id, &name); err != sql.ErrNoRows { 84 | _ = tx.Rollback() 85 | return fmt.Errorf("expected sql.ErrNoRows, but got %v", err) 86 | } 87 | fmt.Printf("Could not read singer outside of transaction before commit\n") 88 | 89 | // Commit the transaction to make the changes permanent and readable for other transactions. 90 | if err := tx.Commit(); err != nil { 91 | return err 92 | } 93 | 94 | // This should now find the row. 95 | row = db.QueryRowContext(ctx, "SELECT SingerId, Name FROM Singers WHERE SingerId = ?", 123) 96 | if err := row.Err(); err != nil { 97 | return err 98 | } 99 | if err := row.Scan(&id, &name); err != nil { 100 | return err 101 | } 102 | fmt.Printf("Found singer after commit: %v %v\n", id, name) 103 | 104 | return nil 105 | } 106 | 107 | func main() { 108 | examples.RunSampleOnEmulator(transaction, createTableStatement) 109 | } 110 | -------------------------------------------------------------------------------- /examples/underlying-client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "fmt" 21 | 22 | "cloud.google.com/go/spanner" 23 | spannerdriver "github.com/googleapis/go-sql-spanner" 24 | "github.com/googleapis/go-sql-spanner/examples" 25 | ) 26 | 27 | // Example of using the underlying *spanner.Client. 28 | func underlyingClient(projectId, instanceId, databaseId string) error { 29 | ctx := context.Background() 30 | db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId)) 31 | if err != nil { 32 | return fmt.Errorf("failed to open database connection: %v", err) 33 | } 34 | defer db.Close() 35 | 36 | conn, err := db.Conn(ctx) 37 | if err != nil { 38 | return err 39 | } 40 | defer conn.Close() 41 | 42 | if err := conn.Raw(func(driverConn any) error { 43 | spannerConn, ok := driverConn.(spannerdriver.SpannerConn) 44 | if !ok { 45 | return fmt.Errorf("unexpected driver connection %v, expected SpannerConn", driverConn) 46 | } 47 | client, err := spannerConn.UnderlyingClient() 48 | if err != nil { 49 | return fmt.Errorf("unable to access underlying client: %w", err) 50 | } 51 | 52 | row := client.Single().Query(ctx, spanner.Statement{SQL: "SELECT 1"}) 53 | return row.Do(func(r *spanner.Row) error { 54 | var value int64 55 | err := r.Columns(&value) 56 | if err != nil { 57 | return fmt.Errorf("failed to read column: %w", err) 58 | } 59 | fmt.Println(value) 60 | return nil 61 | }) 62 | }); err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | func main() { 69 | examples.RunSampleOnEmulator(underlyingClient) 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/googleapis/go-sql-spanner 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | cloud.google.com/go v0.121.1 9 | cloud.google.com/go/longrunning v0.6.7 10 | cloud.google.com/go/spanner v1.82.0 11 | github.com/golang/protobuf v1.5.4 12 | github.com/google/go-cmp v0.7.0 13 | github.com/google/uuid v1.6.0 14 | github.com/googleapis/gax-go/v2 v2.14.2 15 | google.golang.org/api v0.233.0 16 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 17 | google.golang.org/grpc v1.72.1 18 | google.golang.org/protobuf v1.36.6 19 | ) 20 | 21 | require ( 22 | cel.dev/expr v0.23.1 // indirect 23 | cloud.google.com/go/auth v0.16.1 // indirect 24 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 25 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 26 | cloud.google.com/go/iam v1.5.2 // indirect 27 | cloud.google.com/go/monitoring v1.24.2 // indirect 28 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 32 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 33 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 36 | github.com/go-logr/logr v1.4.2 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 39 | github.com/google/s2a-go v0.1.9 // indirect 40 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 41 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 42 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 43 | github.com/zeebo/errs v1.4.0 // indirect 44 | go.opencensus.io v0.24.0 // indirect 45 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 46 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 47 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 49 | go.opentelemetry.io/otel v1.35.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 51 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 52 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 53 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 54 | golang.org/x/crypto v0.38.0 // indirect 55 | golang.org/x/net v0.40.0 // indirect 56 | golang.org/x/oauth2 v0.30.0 // indirect 57 | golang.org/x/sync v0.14.0 // indirect 58 | golang.org/x/sys v0.33.0 // indirect 59 | golang.org/x/text v0.25.0 // indirect 60 | golang.org/x/time v0.11.0 // indirect 61 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 62 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "release-type": "go-yoshi", 3 | "separate-pull-requests": true, 4 | "include-component-in-tag": false, 5 | "packages": { 6 | ".": { 7 | "component": "main" 8 | } 9 | }, 10 | "plugins": ["sentence-case"], 11 | "extra-files": [ 12 | "driver.go" 13 | ] 14 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "schedule:daily", 5 | "docker:disable" 6 | ], 7 | "semanticCommits": "enabled", 8 | "semanticCommitType": "deps", 9 | "semanticCommitScope": "", 10 | "postUpdateOptions": [ 11 | "gomodTidy" 12 | ], 13 | "commitMessageAction": "update", 14 | "groupName": "all dependencies" 15 | } 16 | -------------------------------------------------------------------------------- /rows_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "database/sql/driver" 19 | "fmt" 20 | "io" 21 | "testing" 22 | 23 | "cloud.google.com/go/spanner" 24 | sppb "cloud.google.com/go/spanner/apiv1/spannerpb" 25 | "google.golang.org/protobuf/types/known/structpb" 26 | ) 27 | 28 | type testIterator struct { 29 | metadata *sppb.ResultSetMetadata 30 | rows []*spanner.Row 31 | index int 32 | } 33 | 34 | func (t *testIterator) Next() (*spanner.Row, error) { 35 | if t.index == len(t.rows) { 36 | return nil, io.EOF 37 | } 38 | row := t.rows[t.index] 39 | t.index++ 40 | return row, nil 41 | } 42 | 43 | func (t *testIterator) Stop() { 44 | } 45 | 46 | func (t *testIterator) Metadata() (*sppb.ResultSetMetadata, error) { 47 | return t.metadata, nil 48 | } 49 | 50 | func newRow(t *testing.T, cols []string, vals []interface{}) *spanner.Row { 51 | row, err := spanner.NewRow(cols, vals) 52 | if err != nil { 53 | t.Fatalf("failed to create test row: %v", err) 54 | } 55 | return row 56 | } 57 | 58 | func TestRows_Next(t *testing.T) { 59 | cols := []string{"COL1", "COL2", "COL3"} 60 | it := testIterator{ 61 | metadata: &sppb.ResultSetMetadata{ 62 | RowType: &sppb.StructType{ 63 | Fields: []*sppb.StructType_Field{ 64 | {Name: "COL1", Type: &sppb.Type{Code: sppb.TypeCode_INT64}}, 65 | {Name: "COL2", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, 66 | {Name: "COL3", Type: &sppb.Type{Code: sppb.TypeCode_FLOAT64}}, 67 | }, 68 | }, 69 | }, 70 | rows: []*spanner.Row{ 71 | newRow(t, cols, []interface{}{int64(1), "Test1", 3.14 * 1.0}), 72 | newRow(t, cols, []interface{}{int64(2), spanner.NullString{}, spanner.NullFloat64{}}), 73 | newRow(t, cols, []interface{}{int64(3), "Test3", 3.14 * 3.0}), 74 | }, 75 | } 76 | 77 | rows := rows{it: &it} 78 | dest := make([]driver.Value, 3) 79 | values := make([][]driver.Value, 0) 80 | for { 81 | err := rows.Next(dest) 82 | if err == io.EOF { 83 | break 84 | } 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | row := make([]driver.Value, 3) 89 | copy(row, dest) 90 | values = append(values, row) 91 | } 92 | if g, w := len(values), 3; g != w { 93 | t.Fatalf("values length mismatch\nGot: %v\nWant: %v", g, w) 94 | } 95 | for i := int64(0); i < int64(len(values)); i++ { 96 | if g, w := values[i][0], i+1; g != w { 97 | t.Fatalf("COL1 value mismatch for row %d\nGot: %v\nWant: %v", i, g, w) 98 | } 99 | if i == 0 || i == 2 { 100 | if g, w := values[i][1], fmt.Sprintf("Test%d", i+1); g != w { 101 | t.Fatalf("COL2 value mismatch for row %d\nGot: %v\nWant: %v", i, g, w) 102 | } 103 | if g, w := values[i][2], 3.14*float64(i+1); g != w { 104 | t.Fatalf("COL3 value mismatch for row %d\nGot: %v\nWant: %v", i, g, w) 105 | } 106 | } else { 107 | if g, w := values[i][1], driver.Value(nil); g != w { 108 | t.Fatalf("COL2 value mismatch for row %d\nGot: %v\nWant: %v", i, g, w) 109 | } 110 | if g, w := values[i][2], driver.Value(nil); g != w { 111 | t.Fatalf("COL3 value mismatch for row %d\nGot: %v\nWant: %v", i, g, w) 112 | } 113 | } 114 | } 115 | } 116 | 117 | func TestRows_Next_Unsupported(t *testing.T) { 118 | unspecifiedType := &sppb.Type{Code: sppb.TypeCode_TYPE_CODE_UNSPECIFIED} 119 | 120 | it := testIterator{ 121 | metadata: &sppb.ResultSetMetadata{ 122 | RowType: &sppb.StructType{ 123 | Fields: []*sppb.StructType_Field{ 124 | {Name: "COL1", Type: unspecifiedType}, 125 | }, 126 | }, 127 | }, 128 | rows: []*spanner.Row{ 129 | newRow(t, []string{"COL1"}, []any{ 130 | spanner.GenericColumnValue{ 131 | Type: unspecifiedType, 132 | Value: &structpb.Value{}, 133 | }, 134 | }), 135 | }, 136 | } 137 | 138 | rows := rows{it: &it} 139 | 140 | dest := make([]driver.Value, 1) 141 | err := rows.Next(dest) 142 | if err == nil { 143 | t.Fatal("expected an error, but got nil") 144 | } 145 | const expectedError = "unsupported type TYPE_CODE_UNSPECIFIED, use spannerdriver.ExecOptions{DecodeOption: spannerdriver.DecodeOptionProto} to return the underlying protobuf value" 146 | if err.Error() != expectedError { 147 | t.Fatalf("expected error %q, but got %q", expectedError, err.Error()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /snippets/getting_started_guide.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "io" 22 | "os" 23 | "time" 24 | 25 | "github.com/googleapis/go-sql-spanner/examples/samples" 26 | ) 27 | 28 | type command func(ctx context.Context, w io.Writer, databaseName string) error 29 | 30 | var ( 31 | commands = map[string]command{ 32 | "createconnection": samples.CreateConnection, 33 | "createtables": samples.CreateTables, 34 | "dmlwrite": samples.WriteDataWithDml, 35 | "write": samples.WriteDataWithMutations, 36 | "query": samples.QueryData, 37 | "querywithparameter": samples.QueryDataWithParameter, 38 | "addcolumn": samples.AddColumn, 39 | "ddlbatch": samples.DdlBatch, 40 | "update": samples.UpdateDataWithMutations, 41 | "querymarketingbudget": samples.QueryNewColumn, 42 | "writewithtransactionusingdml": samples.WriteWithTransactionUsingDml, 43 | "tags": samples.Tags, 44 | "readonlytransaction": samples.ReadOnlyTransaction, 45 | "databoost": samples.DataBoost, 46 | "pdml": samples.PartitionedDml, 47 | } 48 | ) 49 | 50 | func run(ctx context.Context, w io.Writer, cmd string, db string) error { 51 | cmdFn := commands[cmd] 52 | if cmdFn == nil { 53 | flag.Usage() 54 | os.Exit(2) 55 | } 56 | err := cmdFn(ctx, w, db) 57 | if err != nil { 58 | fmt.Fprintf(w, "%s failed with %v", cmd, err) 59 | } 60 | return err 61 | } 62 | 63 | func main() { 64 | flag.Usage = func() { 65 | fmt.Fprintf(os.Stderr, `Usage: getting_started_guide 66 | Examples: 67 | spanner_snippets write projects/my-project/instances/my-instance/databases/example-db 68 | `) 69 | } 70 | 71 | flag.Parse() 72 | if len(flag.Args()) < 2 || len(flag.Args()) > 3 { 73 | flag.Usage() 74 | os.Exit(2) 75 | } 76 | 77 | cmd, db := flag.Arg(0), flag.Arg(1) 78 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 79 | defer cancel() 80 | if err := run(ctx, os.Stdout, cmd, db); err != nil { 81 | os.Exit(1) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /snippets/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/googleapis/go-sql-spanner/examples 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | replace github.com/googleapis/go-sql-spanner => ../ 8 | 9 | require ( 10 | cloud.google.com/go/spanner v1.82.0 11 | github.com/docker/docker v28.1.1+incompatible 12 | github.com/googleapis/go-sql-spanner v1.13.2 13 | github.com/testcontainers/testcontainers-go v0.37.0 14 | ) 15 | 16 | require ( 17 | cel.dev/expr v0.23.1 // indirect 18 | cloud.google.com/go v0.121.1 // indirect 19 | cloud.google.com/go/auth v0.16.1 // indirect 20 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 21 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 22 | cloud.google.com/go/iam v1.5.2 // indirect 23 | cloud.google.com/go/longrunning v0.6.7 // indirect 24 | cloud.google.com/go/monitoring v1.24.2 // indirect 25 | dario.cat/mergo v1.0.1 // indirect 26 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 27 | github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect 28 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 29 | github.com/Microsoft/go-winio v0.6.2 // indirect 30 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 33 | github.com/containerd/log v0.1.0 // indirect 34 | github.com/containerd/platforms v0.2.1 // indirect 35 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/distribution/reference v0.6.0 // indirect 38 | github.com/docker/go-connections v0.5.0 // indirect 39 | github.com/docker/go-units v0.5.0 // indirect 40 | github.com/ebitengine/purego v0.8.2 // indirect 41 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 42 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 43 | github.com/felixge/httpsnoop v1.0.4 // indirect 44 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 45 | github.com/go-logr/logr v1.4.2 // indirect 46 | github.com/go-logr/stdr v1.2.2 // indirect 47 | github.com/go-ole/go-ole v1.2.6 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 50 | github.com/google/s2a-go v0.1.9 // indirect 51 | github.com/google/uuid v1.6.0 // indirect 52 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 53 | github.com/googleapis/gax-go/v2 v2.14.2 // indirect 54 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 55 | github.com/klauspost/compress v1.18.0 // indirect 56 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 57 | github.com/magiconair/properties v1.8.10 // indirect 58 | github.com/moby/docker-image-spec v1.3.1 // indirect 59 | github.com/moby/go-archive v0.1.0 // indirect 60 | github.com/moby/patternmatcher v0.6.0 // indirect 61 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 62 | github.com/moby/sys/sequential v0.6.0 // indirect 63 | github.com/moby/sys/user v0.4.0 // indirect 64 | github.com/moby/sys/userns v0.1.0 // indirect 65 | github.com/moby/term v0.5.0 // indirect 66 | github.com/morikuni/aec v1.0.0 // indirect 67 | github.com/opencontainers/go-digest v1.0.0 // indirect 68 | github.com/opencontainers/image-spec v1.1.1 // indirect 69 | github.com/pkg/errors v0.9.1 // indirect 70 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 71 | github.com/pmezard/go-difflib v1.0.0 // indirect 72 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 73 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect 74 | github.com/sirupsen/logrus v1.9.3 // indirect 75 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 76 | github.com/stretchr/testify v1.10.0 // indirect 77 | github.com/tklauser/go-sysconf v0.3.12 // indirect 78 | github.com/tklauser/numcpus v0.6.1 // indirect 79 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 80 | github.com/zeebo/errs v1.4.0 // indirect 81 | go.opencensus.io v0.24.0 // indirect 82 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 83 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 84 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 85 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 86 | go.opentelemetry.io/otel v1.35.0 // indirect 87 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect 88 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 89 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 90 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 91 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 92 | golang.org/x/crypto v0.38.0 // indirect 93 | golang.org/x/net v0.40.0 // indirect 94 | golang.org/x/oauth2 v0.30.0 // indirect 95 | golang.org/x/sync v0.14.0 // indirect 96 | golang.org/x/sys v0.33.0 // indirect 97 | golang.org/x/text v0.25.0 // indirect 98 | golang.org/x/time v0.11.0 // indirect 99 | google.golang.org/api v0.233.0 // indirect 100 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 101 | google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect 102 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 103 | google.golang.org/grpc v1.72.1 // indirect 104 | google.golang.org/protobuf v1.36.6 // indirect 105 | gopkg.in/yaml.v3 v3.0.1 // indirect 106 | ) 107 | -------------------------------------------------------------------------------- /snippets/samples/add_column.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_add_column] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func AddColumn(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | _, err = db.ExecContext(ctx, 35 | `ALTER TABLE Albums 36 | ADD COLUMN MarketingBudget INT64`) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | fmt.Fprint(w, "Added MarketingBudget column\n") 42 | return nil 43 | } 44 | 45 | // [END spanner_add_column] 46 | -------------------------------------------------------------------------------- /snippets/samples/create_connection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_create_connection] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func CreateConnection(ctx context.Context, w io.Writer, databaseName string) error { 28 | // The dataSourceName should start with a fully qualified Spanner database name 29 | // in the format `projects/my-project/instances/my-instance/databases/my-database`. 30 | // Additional properties can be added after the database name by 31 | // adding one or more `;name=value` pairs. 32 | 33 | dsn := fmt.Sprintf("%s;numChannels=8", databaseName) 34 | db, err := sql.Open("spanner", dsn) 35 | if err != nil { 36 | return err 37 | } 38 | defer db.Close() 39 | 40 | row := db.QueryRowContext(ctx, "select 'Hello world!' as hello") 41 | var msg string 42 | if err := row.Scan(&msg); err != nil { 43 | return err 44 | } 45 | fmt.Fprintf(w, "Greeting from Spanner: %s\n", msg) 46 | return nil 47 | } 48 | 49 | // [END spanner_create_connection] 50 | -------------------------------------------------------------------------------- /snippets/samples/create_tables.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_create_database] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func CreateTables(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Create two tables in one batch on Spanner. 35 | conn, err := db.Conn(ctx) 36 | defer conn.Close() 37 | 38 | // Start a DDL batch on the connection. 39 | // This instructs the connection to buffer all DDL statements until the 40 | // command `run batch` is executed. 41 | if _, err := conn.ExecContext(ctx, "start batch ddl"); err != nil { 42 | return err 43 | } 44 | if _, err := conn.ExecContext(ctx, 45 | `CREATE TABLE Singers ( 46 | SingerId INT64 NOT NULL, 47 | FirstName STRING(1024), 48 | LastName STRING(1024), 49 | SingerInfo BYTES(MAX) 50 | ) PRIMARY KEY (SingerId)`); err != nil { 51 | return err 52 | } 53 | if _, err := conn.ExecContext(ctx, 54 | `CREATE TABLE Albums ( 55 | SingerId INT64 NOT NULL, 56 | AlbumId INT64 NOT NULL, 57 | AlbumTitle STRING(MAX) 58 | ) PRIMARY KEY (SingerId, AlbumId), 59 | INTERLEAVE IN PARENT Singers ON DELETE CASCADE`); err != nil { 60 | return err 61 | } 62 | // `run batch` sends the DDL statements to Spanner and blocks until 63 | // all statements have finished executing. 64 | if _, err := conn.ExecContext(ctx, "run batch"); err != nil { 65 | return err 66 | } 67 | 68 | fmt.Fprintf(w, "Created Singers & Albums tables in database: [%s]\n", databaseName) 69 | return nil 70 | } 71 | 72 | // [END spanner_create_database] 73 | -------------------------------------------------------------------------------- /snippets/samples/data_boost.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_data_boost] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | "slices" 24 | 25 | "cloud.google.com/go/spanner" 26 | spannerdriver "github.com/googleapis/go-sql-spanner" 27 | ) 28 | 29 | func DataBoost(ctx context.Context, w io.Writer, databaseName string) error { 30 | db, err := sql.Open("spanner", databaseName) 31 | if err != nil { 32 | return err 33 | } 34 | defer db.Close() 35 | 36 | // Run a partitioned query that uses Data Boost. 37 | rows, err := db.QueryContext(ctx, 38 | "SELECT SingerId, FirstName, LastName from Singers", 39 | spannerdriver.ExecOptions{ 40 | PartitionedQueryOptions: spannerdriver.PartitionedQueryOptions{ 41 | // AutoPartitionQuery instructs the Spanner database/sql driver to 42 | // automatically partition the query and execute each partition in parallel. 43 | // The rows are returned as one result set in undefined order. 44 | AutoPartitionQuery: true, 45 | }, 46 | QueryOptions: spanner.QueryOptions{ 47 | // Set DataBoostEnabled to true to enable DataBoost. 48 | // See https://cloud.google.com/spanner/docs/databoost/databoost-overview 49 | // for more information. 50 | DataBoostEnabled: true, 51 | }, 52 | }) 53 | defer rows.Close() 54 | if err != nil { 55 | return err 56 | } 57 | type Singer struct { 58 | SingerId int64 59 | FirstName string 60 | LastName string 61 | } 62 | var singers []Singer 63 | for rows.Next() { 64 | var singer Singer 65 | err = rows.Scan(&singer.SingerId, &singer.FirstName, &singer.LastName) 66 | if err != nil { 67 | return err 68 | } 69 | singers = append(singers, singer) 70 | } 71 | // Queries that use the AutoPartition option return rows in undefined order, 72 | // so we need to sort them in memory to guarantee the output order. 73 | slices.SortFunc(singers, func(a, b Singer) int { 74 | return int(a.SingerId - b.SingerId) 75 | }) 76 | for _, s := range singers { 77 | fmt.Fprintf(w, "%v %v %v\n", s.SingerId, s.FirstName, s.LastName) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // [END spanner_data_boost] 84 | -------------------------------------------------------------------------------- /snippets/samples/ddl_batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_ddl_batch] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func DdlBatch(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Executing multiple DDL statements as one batch is 35 | // more efficient than executing each statement 36 | // individually. 37 | conn, err := db.Conn(ctx) 38 | defer conn.Close() 39 | 40 | if _, err := conn.ExecContext(ctx, "start batch ddl"); err != nil { 41 | return err 42 | } 43 | if _, err := conn.ExecContext(ctx, 44 | `CREATE TABLE Venues ( 45 | VenueId INT64 NOT NULL, 46 | Name STRING(1024), 47 | Description JSON, 48 | ) PRIMARY KEY (VenueId)`); err != nil { 49 | return err 50 | } 51 | if _, err := conn.ExecContext(ctx, 52 | `CREATE TABLE Concerts ( 53 | ConcertId INT64 NOT NULL, 54 | VenueId INT64 NOT NULL, 55 | SingerId INT64 NOT NULL, 56 | StartTime TIMESTAMP, 57 | EndTime TIMESTAMP, 58 | CONSTRAINT Fk_Concerts_Venues FOREIGN KEY 59 | (VenueId) REFERENCES Venues (VenueId), 60 | CONSTRAINT Fk_Concerts_Singers FOREIGN KEY 61 | (SingerId) REFERENCES Singers (SingerId), 62 | ) PRIMARY KEY (ConcertId)`); err != nil { 63 | return err 64 | } 65 | // `run batch` sends the DDL statements to Spanner and blocks until 66 | // all statements have finished executing. 67 | if _, err := conn.ExecContext(ctx, "run batch"); err != nil { 68 | return err 69 | } 70 | 71 | fmt.Fprint(w, "Added Venues and Concerts tables\n") 72 | return nil 73 | } 74 | 75 | // [END spanner_ddl_batch] 76 | -------------------------------------------------------------------------------- /snippets/samples/pdml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_partitioned_dml] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func PartitionedDml(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | conn, err := db.Conn(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | // Enable Partitioned DML on this connection. 39 | if _, err := conn.ExecContext(ctx, "SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"); err != nil { 40 | return fmt.Errorf("failed to change DML mode to Partitioned_Non_Atomic: %v", err) 41 | } 42 | // Back-fill a default value for the MarketingBudget column. 43 | res, err := conn.ExecContext(ctx, "UPDATE Albums SET MarketingBudget=0 WHERE MarketingBudget IS NULL") 44 | if err != nil { 45 | return err 46 | } 47 | affected, err := res.RowsAffected() 48 | if err != nil { 49 | return fmt.Errorf("failed to get affected rows: %v", err) 50 | } 51 | 52 | // Partitioned DML returns the minimum number of records that were affected. 53 | fmt.Fprintf(w, "Updated at least %v albums\n", affected) 54 | 55 | // Closing the connection will return it to the connection pool. The DML mode will automatically be reset to the 56 | // default TRANSACTIONAL mode when the connection is returned to the pool, so we do not need to change it back 57 | // manually. 58 | _ = conn.Close() 59 | 60 | return nil 61 | } 62 | 63 | // [END spanner_partitioned_dml] 64 | -------------------------------------------------------------------------------- /snippets/samples/query_data.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_query_data] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func QueryData(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | rows, err := db.QueryContext(ctx, 35 | `SELECT SingerId, AlbumId, AlbumTitle 36 | FROM Albums 37 | ORDER BY SingerId, AlbumId`) 38 | defer rows.Close() 39 | if err != nil { 40 | return err 41 | } 42 | for rows.Next() { 43 | var singerId, albumId int64 44 | var title string 45 | err = rows.Scan(&singerId, &albumId, &title) 46 | if err != nil { 47 | return err 48 | } 49 | fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title) 50 | } 51 | if rows.Err() != nil { 52 | return rows.Err() 53 | } 54 | return rows.Close() 55 | } 56 | 57 | // [END spanner_query_data] 58 | -------------------------------------------------------------------------------- /snippets/samples/query_data_with_new_column.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_query_data_with_new_column] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func QueryNewColumn(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | rows, err := db.QueryContext(ctx, 35 | `SELECT SingerId, AlbumId, MarketingBudget 36 | FROM Albums 37 | ORDER BY SingerId, AlbumId`) 38 | defer rows.Close() 39 | if err != nil { 40 | return err 41 | } 42 | for rows.Next() { 43 | var singerId, albumId int64 44 | var marketingBudget sql.NullInt64 45 | err = rows.Scan(&singerId, &albumId, &marketingBudget) 46 | if err != nil { 47 | return err 48 | } 49 | budget := "NULL" 50 | if marketingBudget.Valid { 51 | budget = fmt.Sprintf("%v", marketingBudget.Int64) 52 | } 53 | fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, budget) 54 | } 55 | if rows.Err() != nil { 56 | return rows.Err() 57 | } 58 | return rows.Close() 59 | } 60 | 61 | // [END spanner_query_data_with_new_column] 62 | -------------------------------------------------------------------------------- /snippets/samples/query_data_with_parameter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_query_with_parameter] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func QueryDataWithParameter(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | rows, err := db.QueryContext(ctx, 35 | `SELECT SingerId, FirstName, LastName 36 | FROM Singers 37 | WHERE LastName = ?`, "Garcia") 38 | defer rows.Close() 39 | if err != nil { 40 | return err 41 | } 42 | for rows.Next() { 43 | var singerId int64 44 | var firstName, lastName string 45 | err = rows.Scan(&singerId, &firstName, &lastName) 46 | if err != nil { 47 | return err 48 | } 49 | fmt.Fprintf(w, "%v %v %v\n", singerId, firstName, lastName) 50 | } 51 | if rows.Err() != nil { 52 | return rows.Err() 53 | } 54 | return rows.Close() 55 | } 56 | 57 | // [END spanner_query_with_parameter] 58 | -------------------------------------------------------------------------------- /snippets/samples/query_data_with_timeout.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_statement_timeout] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | "time" 24 | 25 | _ "github.com/googleapis/go-sql-spanner" 26 | ) 27 | 28 | func QueryDataWithTimeout(ctx context.Context, w io.Writer, databaseName string) error { 29 | db, err := sql.Open("spanner", databaseName) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | 35 | // Use QueryContext and pass in a context with a timeout to execute 36 | // a query with a timeout. 37 | ctx, cancel := context.WithTimeout(ctx, time.Second*5) 38 | defer cancel() 39 | rows, err := db.QueryContext(ctx, 40 | `SELECT SingerId, AlbumId, AlbumTitle 41 | FROM Albums 42 | WHERE AlbumTitle in ( 43 | SELECT FirstName 44 | FROM Singers 45 | WHERE LastName LIKE '%a%' 46 | OR LastName LIKE '%m%' 47 | )`) 48 | defer rows.Close() 49 | if err != nil { 50 | return err 51 | } 52 | for rows.Next() { 53 | var singerId, albumId int64 54 | var title string 55 | err = rows.Scan(&singerId, &albumId, &title) 56 | if err != nil { 57 | return err 58 | } 59 | fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title) 60 | } 61 | if rows.Err() != nil { 62 | return rows.Err() 63 | } 64 | return rows.Close() 65 | } 66 | 67 | // [END spanner_statement_timeout] 68 | -------------------------------------------------------------------------------- /snippets/samples/read_only_transaction.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_read_only_transaction] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func ReadOnlyTransaction(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Start a read-only transaction by supplying additional transaction options. 35 | tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) 36 | 37 | albumsOrderedById, err := tx.QueryContext(ctx, 38 | `SELECT SingerId, AlbumId, AlbumTitle 39 | FROM Albums 40 | ORDER BY SingerId, AlbumId`) 41 | defer albumsOrderedById.Close() 42 | if err != nil { 43 | return err 44 | } 45 | for albumsOrderedById.Next() { 46 | var singerId, albumId int64 47 | var title string 48 | err = albumsOrderedById.Scan(&singerId, &albumId, &title) 49 | if err != nil { 50 | return err 51 | } 52 | fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title) 53 | } 54 | 55 | albumsOrderedTitle, err := tx.QueryContext(ctx, 56 | `SELECT SingerId, AlbumId, AlbumTitle 57 | FROM Albums 58 | ORDER BY AlbumTitle`) 59 | defer albumsOrderedTitle.Close() 60 | if err != nil { 61 | return err 62 | } 63 | for albumsOrderedTitle.Next() { 64 | var singerId, albumId int64 65 | var title string 66 | err = albumsOrderedTitle.Scan(&singerId, &albumId, &title) 67 | if err != nil { 68 | return err 69 | } 70 | fmt.Fprintf(w, "%v %v %v\n", singerId, albumId, title) 71 | } 72 | 73 | // End the read-only transaction by calling Commit. 74 | return tx.Commit() 75 | } 76 | 77 | // [END spanner_read_only_transaction] 78 | -------------------------------------------------------------------------------- /snippets/samples/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_transaction_and_statement_tag] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | "cloud.google.com/go/spanner" 25 | spannerdriver "github.com/googleapis/go-sql-spanner" 26 | ) 27 | 28 | func Tags(ctx context.Context, w io.Writer, databaseName string) error { 29 | db, err := sql.Open("spanner", databaseName) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | 35 | // Use the spannerdriver.BeginReadWriteTransaction function 36 | // to specify specific Spanner options, such as transaction tags. 37 | tx, err := spannerdriver.BeginReadWriteTransaction(ctx, db, 38 | spannerdriver.ReadWriteTransactionOptions{ 39 | TransactionOptions: spanner.TransactionOptions{ 40 | TransactionTag: "example-tx-tag", 41 | }, 42 | }) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Pass in an argument of type spannerdriver.ExecOptions to supply 48 | // additional options for a statement. 49 | row := tx.QueryRowContext(ctx, "SELECT MarketingBudget "+ 50 | "FROM Albums "+ 51 | "WHERE SingerId=? and AlbumId=?", 52 | spannerdriver.ExecOptions{ 53 | QueryOptions: spanner.QueryOptions{RequestTag: "query-marketing-budget"}, 54 | }, 1, 1) 55 | var budget int64 56 | if err := row.Scan(&budget); err != nil { 57 | tx.Rollback() 58 | return err 59 | } 60 | 61 | // Reduce the marketing budget by 10% if it is more than 1,000. 62 | if budget > 1000 { 63 | budget = int64(float64(budget) - float64(budget)*0.1) 64 | if _, err := tx.ExecContext(ctx, 65 | `UPDATE Albums SET MarketingBudget=@budget 66 | WHERE SingerId=@singerId AND AlbumId=@albumId`, 67 | spannerdriver.ExecOptions{ 68 | QueryOptions: spanner.QueryOptions{RequestTag: "reduce-marketing-budget"}, 69 | }, 70 | sql.Named("budget", budget), 71 | sql.Named("singerId", 1), 72 | sql.Named("albumId", 1)); err != nil { 73 | tx.Rollback() 74 | return err 75 | } 76 | } 77 | // Commit the current transaction. 78 | if err := tx.Commit(); err != nil { 79 | return err 80 | } 81 | fmt.Fprintln(w, "Reduced marketing budget") 82 | 83 | return nil 84 | } 85 | 86 | // [END spanner_transaction_and_statement_tag] 87 | -------------------------------------------------------------------------------- /snippets/samples/update_data_with_mutations.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_update_data] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | "cloud.google.com/go/spanner" 25 | spannerdriver "github.com/googleapis/go-sql-spanner" 26 | ) 27 | 28 | func UpdateDataWithMutations(ctx context.Context, w io.Writer, databaseName string) error { 29 | db, err := sql.Open("spanner", databaseName) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | 35 | // Get a connection so that we can get access to the Spanner specific 36 | // connection interface SpannerConn. 37 | conn, err := db.Conn(ctx) 38 | if err != nil { 39 | return err 40 | } 41 | defer conn.Close() 42 | 43 | cols := []string{"SingerId", "AlbumId", "MarketingBudget"} 44 | mutations := []*spanner.Mutation{ 45 | spanner.Update("Albums", cols, []interface{}{1, 1, 100000}), 46 | spanner.Update("Albums", cols, []interface{}{2, 2, 500000}), 47 | } 48 | if err := conn.Raw(func(driverConn interface{}) error { 49 | spannerConn, ok := driverConn.(spannerdriver.SpannerConn) 50 | if !ok { 51 | return fmt.Errorf("unexpected driver connection %v, "+ 52 | "expected SpannerConn", driverConn) 53 | } 54 | _, err = spannerConn.Apply(ctx, mutations) 55 | return err 56 | }); err != nil { 57 | return err 58 | } 59 | fmt.Fprintf(w, "Updated %v albums\n", len(mutations)) 60 | 61 | return nil 62 | } 63 | 64 | // [END spanner_update_data] 65 | -------------------------------------------------------------------------------- /snippets/samples/update_data_with_transaction.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_dml_getting_started_update] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func WriteWithTransactionUsingDml(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Transfer marketing budget from one album to another. We do it in a 35 | // transaction to ensure that the transfer is atomic. 36 | tx, err := db.BeginTx(ctx, &sql.TxOptions{}) 37 | if err != nil { 38 | return err 39 | } 40 | // The Spanner database/sql driver supports both positional and named 41 | // query parameters. This query uses named query parameters. 42 | const selectSql = "SELECT MarketingBudget " + 43 | "FROM Albums " + 44 | "WHERE SingerId = @singerId and AlbumId = @albumId" 45 | // Get the marketing_budget of singer 2 / album 2. 46 | row := tx.QueryRowContext(ctx, selectSql, 47 | sql.Named("singerId", 2), sql.Named("albumId", 2)) 48 | var budget2 int64 49 | if err := row.Scan(&budget2); err != nil { 50 | tx.Rollback() 51 | return err 52 | } 53 | const transfer = 20000 54 | // The transaction will only be committed if this condition still holds 55 | // at the time of commit. Otherwise, the transaction will be aborted. 56 | if budget2 >= transfer { 57 | // Get the marketing_budget of singer 1 / album 1. 58 | row := tx.QueryRowContext(ctx, selectSql, 59 | sql.Named("singerId", 1), sql.Named("albumId", 1)) 60 | var budget1 int64 61 | if err := row.Scan(&budget1); err != nil { 62 | tx.Rollback() 63 | return err 64 | } 65 | // Transfer part of the marketing budget of Album 2 to Album 1. 66 | budget1 += transfer 67 | budget2 -= transfer 68 | const updateSql = "UPDATE Albums " + 69 | "SET MarketingBudget = @budget " + 70 | "WHERE SingerId = @singerId and AlbumId = @albumId" 71 | // Start a DML batch and execute it as part of the current transaction. 72 | if _, err := tx.ExecContext(ctx, "start batch dml"); err != nil { 73 | tx.Rollback() 74 | return err 75 | } 76 | if _, err := tx.ExecContext(ctx, updateSql, 77 | sql.Named("singerId", 1), 78 | sql.Named("albumId", 1), 79 | sql.Named("budget", budget1)); err != nil { 80 | _, _ = tx.ExecContext(ctx, "abort batch") 81 | tx.Rollback() 82 | return err 83 | } 84 | if _, err := tx.ExecContext(ctx, updateSql, 85 | sql.Named("singerId", 2), 86 | sql.Named("albumId", 2), 87 | sql.Named("budget", budget2)); err != nil { 88 | _, _ = tx.ExecContext(ctx, "abort batch") 89 | tx.Rollback() 90 | return err 91 | } 92 | // `run batch` sends the DML statements to Spanner. 93 | // The result contains the total affected rows across the entire batch. 94 | result, err := tx.ExecContext(ctx, "run batch") 95 | if err != nil { 96 | tx.Rollback() 97 | return err 98 | } 99 | if affected, err := result.RowsAffected(); err != nil { 100 | tx.Rollback() 101 | return err 102 | } else if affected != 2 { 103 | // The batch should update 2 rows. 104 | tx.Rollback() 105 | return fmt.Errorf("unexpected number of rows affected: %v", affected) 106 | } 107 | } 108 | // Commit the current transaction. 109 | if err := tx.Commit(); err != nil { 110 | return err 111 | } 112 | 113 | fmt.Fprintln(w, "Transferred marketing budget from Album 2 to Album 1") 114 | 115 | return nil 116 | } 117 | 118 | // [END spanner_dml_getting_started_update] 119 | -------------------------------------------------------------------------------- /snippets/samples/write_data_with_dml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_dml_getting_started_insert] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func WriteDataWithDml(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Add 4 rows in one statement. 35 | // The database/sql driver supports positional query parameters. 36 | res, err := db.ExecContext(ctx, 37 | "INSERT INTO Singers (SingerId, FirstName, LastName) "+ 38 | "VALUES (?, ?, ?), (?, ?, ?), "+ 39 | " (?, ?, ?), (?, ?, ?)", 40 | 12, "Melissa", "Garcia", 41 | 13, "Russel", "Morales", 42 | 14, "Jacqueline", "Long", 43 | 15, "Dylan", "Shaw") 44 | if err != nil { 45 | return err 46 | } 47 | c, err := res.RowsAffected() 48 | if err != nil { 49 | return err 50 | } 51 | fmt.Fprintf(w, "%v records inserted\n", c) 52 | 53 | return nil 54 | } 55 | 56 | // [END spanner_dml_getting_started_insert] 57 | -------------------------------------------------------------------------------- /snippets/samples/write_data_with_dml_batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_dml_getting_started_insert] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | _ "github.com/googleapis/go-sql-spanner" 25 | ) 26 | 27 | func WriteDataWithDmlBatch(ctx context.Context, w io.Writer, databaseName string) error { 28 | db, err := sql.Open("spanner", databaseName) 29 | if err != nil { 30 | return err 31 | } 32 | defer db.Close() 33 | 34 | // Add multiple rows in one DML batch. 35 | // database/sql also supports named parameters. 36 | sql := "INSERT INTO Singers (SingerId, FirstName, LastName) " + 37 | "VALUES (@SingerId, @FirstName, @LastName)" 38 | 39 | // Get a connection from the pool, so we know that all statements 40 | // are executed on the same connection. 41 | conn, err := db.Conn(ctx) 42 | if err != nil { 43 | return err 44 | } 45 | defer conn.Close() 46 | // `start batch dml` starts a DML batch. 47 | // All following DML statements are buffered in the client until the 48 | // `run batch` statement is executed. 49 | if _, err := conn.ExecContext(ctx, "start batch dml"); err != nil { 50 | return err 51 | } 52 | if _, err := conn.ExecContext(ctx, sql, 16, "Sarah", "Wilson"); err != nil { 53 | return err 54 | } 55 | if _, err := conn.ExecContext(ctx, sql, 17, "Ethan", "Miller"); err != nil { 56 | return err 57 | } 58 | if _, err := conn.ExecContext(ctx, sql, 18, "Maya", "Patel"); err != nil { 59 | return err 60 | } 61 | // `run batch` sends all buffered DML statements in one batch to Spanner. 62 | res, err := conn.ExecContext(ctx, "run batch") 63 | if err != nil { 64 | return err 65 | } 66 | c, err := res.RowsAffected() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if err != nil { 72 | return err 73 | } 74 | fmt.Fprintf(w, "%v records inserted\n", c) 75 | 76 | return nil 77 | } 78 | 79 | // [END spanner_dml_getting_started_insert] 80 | -------------------------------------------------------------------------------- /snippets/samples/write_data_with_mutations.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package samples 16 | 17 | // [START spanner_insert_data] 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "io" 23 | 24 | "cloud.google.com/go/spanner" 25 | spannerdriver "github.com/googleapis/go-sql-spanner" 26 | ) 27 | 28 | func WriteDataWithMutations(ctx context.Context, w io.Writer, databaseName string) error { 29 | db, err := sql.Open("spanner", databaseName) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | 35 | // Get a connection so that we can get access to the Spanner specific 36 | // connection interface SpannerConn. 37 | conn, err := db.Conn(ctx) 38 | if err != nil { 39 | return err 40 | } 41 | defer conn.Close() 42 | 43 | singerColumns := []string{"SingerId", "FirstName", "LastName"} 44 | albumColumns := []string{"SingerId", "AlbumId", "AlbumTitle"} 45 | mutations := []*spanner.Mutation{ 46 | spanner.Insert("Singers", singerColumns, []interface{}{int64(1), "Marc", "Richards"}), 47 | spanner.Insert("Singers", singerColumns, []interface{}{int64(2), "Catalina", "Smith"}), 48 | spanner.Insert("Singers", singerColumns, []interface{}{int64(3), "Alice", "Trentor"}), 49 | spanner.Insert("Singers", singerColumns, []interface{}{int64(4), "Lea", "Martin"}), 50 | spanner.Insert("Singers", singerColumns, []interface{}{int64(5), "David", "Lomond"}), 51 | spanner.Insert("Albums", albumColumns, []interface{}{int64(1), int64(1), "Total Junk"}), 52 | spanner.Insert("Albums", albumColumns, []interface{}{int64(1), int64(2), "Go, Go, Go"}), 53 | spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(1), "Green"}), 54 | spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(2), "Forever Hold Your Peace"}), 55 | spanner.Insert("Albums", albumColumns, []interface{}{int64(2), int64(3), "Terrified"}), 56 | } 57 | // Mutations can be written outside an explicit transaction using SpannerConn#Apply. 58 | if err := conn.Raw(func(driverConn interface{}) error { 59 | spannerConn, ok := driverConn.(spannerdriver.SpannerConn) 60 | if !ok { 61 | return fmt.Errorf("unexpected driver connection %v, expected SpannerConn", driverConn) 62 | } 63 | _, err = spannerConn.Apply(ctx, mutations) 64 | return err 65 | }); err != nil { 66 | return err 67 | } 68 | fmt.Fprintf(w, "Inserted %v rows\n", len(mutations)) 69 | 70 | return nil 71 | } 72 | 73 | // [END spanner_insert_data] 74 | -------------------------------------------------------------------------------- /stmt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "context" 19 | "database/sql/driver" 20 | 21 | "cloud.google.com/go/spanner" 22 | "google.golang.org/grpc/codes" 23 | "google.golang.org/grpc/status" 24 | ) 25 | 26 | var _ driver.Stmt = &stmt{} 27 | var _ driver.StmtExecContext = &stmt{} 28 | var _ driver.StmtQueryContext = &stmt{} 29 | var _ driver.NamedValueChecker = &stmt{} 30 | 31 | type stmt struct { 32 | conn *conn 33 | numArgs int 34 | query string 35 | statementType statementType 36 | execOptions ExecOptions 37 | } 38 | 39 | func (s *stmt) Close() error { 40 | return nil 41 | } 42 | 43 | func (s *stmt) NumInput() int { 44 | return s.numArgs 45 | } 46 | 47 | func (s *stmt) Exec(_ []driver.Value) (driver.Result, error) { 48 | return nil, spanner.ToSpannerError(status.Errorf(codes.Unimplemented, "use ExecContext instead")) 49 | } 50 | 51 | func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { 52 | return s.conn.execContext(ctx, s.query, s.execOptions, args) 53 | } 54 | 55 | func (s *stmt) Query(_ []driver.Value) (driver.Rows, error) { 56 | return nil, spanner.ToSpannerError(status.Errorf(codes.Unimplemented, "use QueryContext instead")) 57 | } 58 | 59 | func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { 60 | return s.conn.queryContext(ctx, s.query, s.execOptions, args) 61 | } 62 | 63 | func (s *stmt) CheckNamedValue(value *driver.NamedValue) error { 64 | if value == nil { 65 | return nil 66 | } 67 | 68 | if execOptions, ok := value.Value.(ExecOptions); ok { 69 | s.execOptions = execOptions 70 | return driver.ErrRemoveArgument 71 | } 72 | return s.conn.CheckNamedValue(value) 73 | } 74 | 75 | func prepareSpannerStmt(q string, args []driver.NamedValue) (spanner.Statement, error) { 76 | q, names, err := parseParameters(q) 77 | if err != nil { 78 | return spanner.Statement{}, err 79 | } 80 | ss := spanner.NewStatement(q) 81 | for i, v := range args { 82 | name := args[i].Name 83 | if name == "" && len(names) > i { 84 | name = names[i] 85 | } 86 | if name != "" { 87 | ss.Params[name] = convertParam(v.Value) 88 | } 89 | } 90 | // Verify that all parameters have a value. 91 | for _, name := range names { 92 | if _, ok := ss.Params[name]; !ok { 93 | return spanner.Statement{}, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "missing value for query parameter %v", name)) 94 | } 95 | } 96 | return ss, nil 97 | } 98 | 99 | func convertParam(v driver.Value) driver.Value { 100 | switch v := v.(type) { 101 | default: 102 | return v 103 | case int: 104 | return int64(v) 105 | case []int: 106 | if v == nil { 107 | return []int64(nil) 108 | } 109 | res := make([]int64, len(v)) 110 | for i, val := range v { 111 | res[i] = int64(val) 112 | } 113 | return res 114 | case uint: 115 | return int64(v) 116 | case []uint: 117 | if v == nil { 118 | return []int64(nil) 119 | } 120 | res := make([]int64, len(v)) 121 | for i, val := range v { 122 | res[i] = int64(val) 123 | } 124 | return res 125 | case uint64: 126 | return int64(v) 127 | case []uint64: 128 | if v == nil { 129 | return []int64(nil) 130 | } 131 | res := make([]int64, len(v)) 132 | for i, val := range v { 133 | res[i] = int64(val) 134 | } 135 | return res 136 | case *uint64: 137 | if v == nil { 138 | return (*int64)(nil) 139 | } 140 | vi := int64(*v) 141 | return &vi 142 | case *int: 143 | if v == nil { 144 | return (*int64)(nil) 145 | } 146 | vi := int64(*v) 147 | return &vi 148 | case []*int: 149 | if v == nil { 150 | return []*int64(nil) 151 | } 152 | res := make([]*int64, len(v)) 153 | for i, val := range v { 154 | if val == nil { 155 | res[i] = nil 156 | } else { 157 | z := int64(*val) 158 | res[i] = &z 159 | } 160 | } 161 | return res 162 | case *[]int: 163 | if v == nil { 164 | return []int64(nil) 165 | } 166 | res := make([]int64, len(*v)) 167 | for i, val := range *v { 168 | res[i] = int64(val) 169 | } 170 | return res 171 | case *uint: 172 | if v == nil { 173 | return (*int64)(nil) 174 | } 175 | vi := int64(*v) 176 | return &vi 177 | case []*uint: 178 | if v == nil { 179 | return []*int64(nil) 180 | } 181 | res := make([]*int64, len(v)) 182 | for i, val := range v { 183 | if val == nil { 184 | res[i] = nil 185 | } else { 186 | z := int64(*val) 187 | res[i] = &z 188 | } 189 | } 190 | return res 191 | case *[]uint: 192 | if v == nil { 193 | return []int64(nil) 194 | } 195 | res := make([]int64, len(*v)) 196 | for i, val := range *v { 197 | res[i] = int64(val) 198 | } 199 | return res 200 | case []*uint64: 201 | if v == nil { 202 | return []*int64(nil) 203 | } 204 | res := make([]*int64, len(v)) 205 | for i, val := range v { 206 | if val == nil { 207 | res[i] = nil 208 | } else { 209 | z := int64(*val) 210 | res[i] = &z 211 | } 212 | } 213 | return res 214 | case *[]uint64: 215 | if v == nil { 216 | return []int64(nil) 217 | } 218 | res := make([]int64, len(*v)) 219 | for i, val := range *v { 220 | res[i] = int64(val) 221 | } 222 | return res 223 | } 224 | } 225 | 226 | type result struct { 227 | rowsAffected int64 228 | lastInsertId int64 229 | hasLastInsertId bool 230 | batchUpdateCounts []int64 231 | } 232 | 233 | var errNoLastInsertId = spanner.ToSpannerError( 234 | status.Errorf(codes.FailedPrecondition, 235 | "LastInsertId is only supported for INSERT statements that use a THEN RETURN clause "+ 236 | "and that return exactly one row and one column "+ 237 | "(e.g. `INSERT INTO MyTable (Val) values ('val1') THEN RETURN Id`)")) 238 | 239 | func (r *result) LastInsertId() (int64, error) { 240 | if r.hasLastInsertId { 241 | return r.lastInsertId, nil 242 | } 243 | return 0, errNoLastInsertId 244 | } 245 | 246 | func (r *result) RowsAffected() (int64, error) { 247 | return r.rowsAffected, nil 248 | } 249 | -------------------------------------------------------------------------------- /stmt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "database/sql/driver" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestConvertParam(t *testing.T) { 24 | check := func(in, want driver.Value) { 25 | t.Helper() 26 | got := convertParam(in) 27 | if !reflect.DeepEqual(got, want) { 28 | t.Errorf("in:%#v want:%#v got:%#v", in, want, got) 29 | } 30 | } 31 | 32 | check(uint(197), int64(197)) 33 | check(pointerTo(uint(197)), pointerTo(int64(197))) 34 | check((*uint)(nil), (*int64)(nil)) 35 | 36 | check([]uint{197}, []int64{197}) 37 | check(pointerTo([]uint{197}), []int64{197}) 38 | check([]*uint{pointerTo(uint(197))}, []*int64{pointerTo(int64(197))}) 39 | check(([]*uint)(nil), ([]*int64)(nil)) 40 | check((*[]uint)(nil), ([]int64)(nil)) 41 | 42 | check([]int{197}, []int64{197}) 43 | check([]*int{pointerTo(int(197))}, []*int64{pointerTo(int64(197))}) 44 | check(pointerTo([]int{197}), []int64{197}) 45 | check(([]*int)(nil), ([]*int64)(nil)) 46 | check((*[]int)(nil), ([]int64)(nil)) 47 | 48 | check(uint64(197), int64(197)) 49 | check(pointerTo(uint64(197)), pointerTo(int64(197))) 50 | check((*uint64)(nil), (*int64)(nil)) 51 | 52 | check([]uint64{197}, []int64{197}) 53 | check(pointerTo([]uint64{197}), []int64{197}) 54 | check([]*uint64{pointerTo(uint64(197))}, []*int64{pointerTo(int64(197))}) 55 | check(([]*uint64)(nil), ([]*int64)(nil)) 56 | check((*[]uint64)(nil), ([]int64)(nil)) 57 | } 58 | 59 | func pointerTo[T any](v T) *T { return &v } 60 | -------------------------------------------------------------------------------- /stmt_with_mockserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "context" 19 | "encoding/base64" 20 | "reflect" 21 | "strings" 22 | "testing" 23 | 24 | "cloud.google.com/go/spanner/apiv1/spannerpb" 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/google/go-cmp/cmp/cmpopts" 27 | "github.com/googleapis/go-sql-spanner/testdata/protos/concertspb" 28 | "github.com/googleapis/go-sql-spanner/testutil" 29 | "google.golang.org/protobuf/proto" 30 | "google.golang.org/protobuf/types/known/structpb" 31 | ) 32 | 33 | func TestPrepareQuery(t *testing.T) { 34 | t.Parallel() 35 | 36 | db, server, teardown := setupTestDBConnection(t) 37 | defer teardown() 38 | ctx := context.Background() 39 | 40 | query := "select value from test where id=?" 41 | if err := server.TestSpanner.PutStatementResult( 42 | strings.Replace(query, "?", "@p1", 1), 43 | &testutil.StatementResult{ 44 | Type: testutil.StatementResultResultSet, 45 | ResultSet: testutil.CreateSelect1ResultSet(), 46 | }, 47 | ); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | stmt, err := db.PrepareContext(ctx, query) 52 | if err != nil { 53 | t.Fatalf("failed to prepare query: %v", err) 54 | } 55 | 56 | rows, err := stmt.QueryContext(ctx, 1) 57 | if err != nil { 58 | t.Fatalf("failed to execute query: %v", err) 59 | } 60 | defer rows.Close() 61 | 62 | var values []int64 63 | var value int64 64 | for rows.Next() { 65 | if err := rows.Scan(&value); err != nil { 66 | t.Fatalf("failed to scan row value: %v", err) 67 | } 68 | values = append(values, value) 69 | } 70 | if g, w := values, []int64{1}; !cmp.Equal(g, w) { 71 | t.Fatalf("returned values mismatch\n Got: %v\nWant: %v", g, w) 72 | } 73 | } 74 | 75 | func TestPrepareDml(t *testing.T) { 76 | t.Parallel() 77 | 78 | db, server, teardown := setupTestDBConnection(t) 79 | defer teardown() 80 | ctx := context.Background() 81 | 82 | query := "insert into test (id, value) values (?, ?)" 83 | if err := server.TestSpanner.PutStatementResult( 84 | "insert into test (id, value) values (@p1, @p2)", 85 | &testutil.StatementResult{ 86 | Type: testutil.StatementResultUpdateCount, 87 | UpdateCount: 1, 88 | }, 89 | ); err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | stmt, err := db.PrepareContext(ctx, query) 94 | if err != nil { 95 | t.Fatalf("failed to prepare dml: %v", err) 96 | } 97 | 98 | res, err := stmt.ExecContext(ctx, 1, "One") 99 | if err != nil { 100 | t.Fatalf("failed to execute dml: %v", err) 101 | } 102 | if rowsAffected, err := res.RowsAffected(); err != nil { 103 | t.Fatalf("failed to get rows affected: %v", err) 104 | } else if g, w := rowsAffected, int64(1); g != w { 105 | t.Fatalf("rows affected mismatch\n Got: %v\nWant: %v", g, w) 106 | } 107 | } 108 | 109 | func TestPrepareWithValuerScanner(t *testing.T) { 110 | t.Parallel() 111 | 112 | db, server, teardown := setupTestDBConnection(t) 113 | defer teardown() 114 | ctx := context.Background() 115 | 116 | items := []*concertspb.Item{concertspb.CreateItem("test-product", 2)} 117 | shippingAddress := concertspb.CreateAddress("Test Street 1", "Bentheim", "NDS", "DE") 118 | ticketOrder := concertspb.CreateTicketOrder("123", 1, shippingAddress, items) 119 | bytes, err := proto.Marshal(ticketOrder) 120 | if err != nil { 121 | t.Fatalf("failed to marshal proto: %v", err) 122 | } 123 | query := "select TicketOrder from test where TicketOrder=?" 124 | if err := server.TestSpanner.PutStatementResult( 125 | strings.Replace(query, "?", "@p1", 1), 126 | &testutil.StatementResult{ 127 | Type: testutil.StatementResultResultSet, 128 | ResultSet: testutil.CreateSingleColumnProtoResultSet([][]byte{bytes}, "TicketOrder"), 129 | }, 130 | ); err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | stmt, err := db.PrepareContext(ctx, query) 135 | if err != nil { 136 | t.Fatalf("failed to prepare query: %v", err) 137 | } 138 | 139 | rows, err := stmt.QueryContext(ctx, ticketOrder) 140 | if err != nil { 141 | t.Fatalf("failed to execute query: %v", err) 142 | } 143 | defer rows.Close() 144 | 145 | var value concertspb.TicketOrder 146 | if rows.Next() { 147 | if err := rows.Scan(&value); err != nil { 148 | t.Fatalf("failed to scan row value: %v", err) 149 | } 150 | if g, w := &value, ticketOrder; !cmp.Equal(g, w, cmpopts.IgnoreUnexported(concertspb.TicketOrder{}, concertspb.Address{}, concertspb.Item{})) { 151 | t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) 152 | } 153 | if rows.Next() { 154 | t.Fatal("found more than one row") 155 | } 156 | } else { 157 | t.Fatal("no rows found") 158 | } 159 | if err := rows.Err(); err != nil { 160 | t.Fatalf("failed to read rows: %v", err) 161 | } 162 | if err := rows.Close(); err != nil { 163 | t.Fatalf("failed to close rows: %v", err) 164 | } 165 | 166 | requests := drainRequestsFromServer(server.TestSpanner) 167 | executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) 168 | if g, w := len(executeRequests), 1; g != w { 169 | t.Fatalf("number of execute requests mismatch\n Got: %v\nWant: %v", g, w) 170 | } 171 | params := executeRequests[0].(*spannerpb.ExecuteSqlRequest).Params 172 | if g, w := len(params.Fields), 1; g != w { 173 | t.Fatalf("params length mismatch\n Got: %v\nWant: %v", g, w) 174 | } 175 | wantParamValue := &structpb.Value{ 176 | Kind: &structpb.Value_StringValue{StringValue: base64.StdEncoding.EncodeToString(bytes)}, 177 | } 178 | if g, w := params.Fields["p1"], wantParamValue; !cmp.Equal(g, w, cmpopts.IgnoreUnexported(structpb.Value{})) { 179 | t.Fatalf("param value mismatch\n Got: %v\nWant: %v", g, w) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /testdata/protos/README.md: -------------------------------------------------------------------------------- 1 | ### Proto Descriptor Generation 2 | 3 | The order.pb file was generated with the following command: 4 | 5 | ```shell 6 | protoc --include_imports --descriptor_set_out=order.pb --go_out=. order.proto 7 | ``` 8 | -------------------------------------------------------------------------------- /testdata/protos/concertspb/order.go: -------------------------------------------------------------------------------- 1 | package concertspb 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | 7 | "github.com/golang/protobuf/proto" 8 | ) 9 | 10 | func CreateTicketOrder(orderNumber string, date int64, shippingAddress *Address, items []*Item) *TicketOrder { 11 | return &TicketOrder{ 12 | OrderNumber: &orderNumber, 13 | Date: &date, 14 | ShippingAddress: shippingAddress, 15 | LineItem: items, 16 | } 17 | } 18 | 19 | func CreateAddress(street, city, state, country string) *Address { 20 | return &Address{ 21 | Street: &street, 22 | City: &city, 23 | State: &state, 24 | Country: &country, 25 | } 26 | } 27 | 28 | func CreateItem(productName string, quantity int32) *Item { 29 | return &Item{ 30 | ProductName: &productName, 31 | Quantity: &quantity, 32 | } 33 | } 34 | 35 | func (x *TicketOrder) Value() (driver.Value, error) { 36 | return proto.Marshal(x) 37 | } 38 | 39 | func (x *TicketOrder) Scan(dest interface{}) error { 40 | b, ok := dest.([]byte) 41 | if !ok { 42 | return fmt.Errorf("invalid type for protobuf value: %v", dest) 43 | } 44 | return proto.Unmarshal(b, x) 45 | } 46 | -------------------------------------------------------------------------------- /testdata/protos/order.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/go-sql-spanner/c68065d5eb44e80ef93d02bddad759e0cc1993ac/testdata/protos/order.pb -------------------------------------------------------------------------------- /testdata/protos/order.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "concertspb/"; 4 | package examples.concerts; 5 | 6 | message Address { 7 | optional string street = 1; 8 | optional string city = 2; 9 | optional string state = 3; 10 | optional string country = 4; 11 | } 12 | 13 | message Item { 14 | optional string product_name = 1; 15 | optional int32 quantity = 2; 16 | } 17 | 18 | message TicketOrder { 19 | optional string order_number = 1; 20 | optional int64 date = 2; 21 | optional Address shipping_address = 3; 22 | repeated Item line_item = 4; 23 | } 24 | 25 | message TicketOrderHistory { 26 | optional string order_number = 1; 27 | optional int64 date = 2; 28 | } 29 | -------------------------------------------------------------------------------- /testutil/inmem_database_admin_server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutil 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | "cloud.google.com/go/longrunning/autogen/longrunningpb" 23 | databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" 24 | "google.golang.org/grpc/metadata" 25 | "google.golang.org/protobuf/proto" 26 | ) 27 | 28 | // InMemDatabaseAdminServer contains the DatabaseAdminServer interface plus a couple 29 | // of specific methods for setting mocked results. 30 | type InMemDatabaseAdminServer interface { 31 | databasepb.DatabaseAdminServer 32 | Stop() 33 | Resps() []proto.Message 34 | SetResps([]proto.Message) 35 | Reqs() []proto.Message 36 | SetReqs([]proto.Message) 37 | SetErr(error) 38 | } 39 | 40 | // inMemDatabaseAdminServer implements InMemDatabaseAdminServer interface. Note that 41 | // there is no mutex protecting the data structures, so it is not safe for 42 | // concurrent use. 43 | type inMemDatabaseAdminServer struct { 44 | databasepb.DatabaseAdminServer 45 | reqs []proto.Message 46 | // If set, all calls return this error 47 | err error 48 | // responses to return if err == nil 49 | resps []proto.Message 50 | } 51 | 52 | // NewInMemDatabaseAdminServer creates a new in-mem test server. 53 | func NewInMemDatabaseAdminServer() InMemDatabaseAdminServer { 54 | res := &inMemDatabaseAdminServer{} 55 | return res 56 | } 57 | 58 | func (s *inMemDatabaseAdminServer) UpdateDatabaseDdl(ctx context.Context, req *databasepb.UpdateDatabaseDdlRequest) (*longrunningpb.Operation, error) { 59 | md, _ := metadata.FromIncomingContext(ctx) 60 | if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { 61 | return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) 62 | } 63 | s.reqs = append(s.reqs, req) 64 | if s.err != nil { 65 | return nil, s.err 66 | } 67 | return s.resps[0].(*longrunningpb.Operation), nil 68 | } 69 | 70 | func (s *inMemDatabaseAdminServer) Stop() { 71 | // do nothing 72 | } 73 | 74 | func (s *inMemDatabaseAdminServer) Resps() []proto.Message { 75 | return s.resps 76 | } 77 | 78 | func (s *inMemDatabaseAdminServer) SetResps(resps []proto.Message) { 79 | s.resps = resps 80 | } 81 | 82 | func (s *inMemDatabaseAdminServer) Reqs() []proto.Message { 83 | return s.reqs 84 | } 85 | 86 | func (s *inMemDatabaseAdminServer) SetReqs(reqs []proto.Message) { 87 | s.reqs = reqs 88 | } 89 | 90 | func (s *inMemDatabaseAdminServer) SetErr(err error) { 91 | s.err = err 92 | } 93 | -------------------------------------------------------------------------------- /testutil/inmem_instance_admin_server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutil 16 | 17 | import ( 18 | "context" 19 | 20 | instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" 21 | "google.golang.org/protobuf/proto" 22 | ) 23 | 24 | // InMemInstanceAdminServer contains the InstanceAdminServer interface plus a couple 25 | // of specific methods for setting mocked results. 26 | type InMemInstanceAdminServer interface { 27 | instancepb.InstanceAdminServer 28 | Stop() 29 | Resps() []proto.Message 30 | SetResps([]proto.Message) 31 | Reqs() []proto.Message 32 | SetReqs([]proto.Message) 33 | SetErr(error) 34 | } 35 | 36 | // inMemInstanceAdminServer implements InMemInstanceAdminServer interface. Note that 37 | // there is no mutex protecting the data structures, so it is not safe for 38 | // concurrent use. 39 | type inMemInstanceAdminServer struct { 40 | instancepb.InstanceAdminServer 41 | reqs []proto.Message 42 | // If set, all calls return this error 43 | err error 44 | // responses to return if err == nil 45 | resps []proto.Message 46 | } 47 | 48 | // NewInMemInstanceAdminServer creates a new in-mem test server. 49 | func NewInMemInstanceAdminServer() InMemInstanceAdminServer { 50 | res := &inMemInstanceAdminServer{} 51 | return res 52 | } 53 | 54 | // GetInstance returns the metadata of a spanner instance. 55 | func (s *inMemInstanceAdminServer) GetInstance(ctx context.Context, req *instancepb.GetInstanceRequest) (*instancepb.Instance, error) { 56 | s.reqs = append(s.reqs, req) 57 | if s.err != nil { 58 | defer func() { s.err = nil }() 59 | return nil, s.err 60 | } 61 | return s.resps[0].(*instancepb.Instance), nil 62 | } 63 | 64 | func (s *inMemInstanceAdminServer) Stop() { 65 | // do nothing 66 | } 67 | 68 | func (s *inMemInstanceAdminServer) Resps() []proto.Message { 69 | return s.resps 70 | } 71 | 72 | func (s *inMemInstanceAdminServer) SetResps(resps []proto.Message) { 73 | s.resps = resps 74 | } 75 | 76 | func (s *inMemInstanceAdminServer) Reqs() []proto.Message { 77 | return s.reqs 78 | } 79 | 80 | func (s *inMemInstanceAdminServer) SetReqs(reqs []proto.Message) { 81 | s.reqs = reqs 82 | } 83 | 84 | func (s *inMemInstanceAdminServer) SetErr(err error) { 85 | s.err = err 86 | } 87 | -------------------------------------------------------------------------------- /testutil/inmem_instance_admin_server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "log" 22 | "net" 23 | "testing" 24 | 25 | instance "cloud.google.com/go/spanner/admin/instance/apiv1" 26 | instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" 27 | "github.com/googleapis/go-sql-spanner/testutil" 28 | "google.golang.org/api/option" 29 | "google.golang.org/grpc" 30 | "google.golang.org/grpc/credentials/insecure" 31 | "google.golang.org/protobuf/proto" 32 | ) 33 | 34 | var instanceClientOpt option.ClientOption 35 | 36 | var ( 37 | mockInstanceAdmin = testutil.NewInMemInstanceAdminServer() 38 | ) 39 | 40 | func setupInstanceAdminServer() { 41 | flag.Parse() 42 | 43 | serv := grpc.NewServer() 44 | instancepb.RegisterInstanceAdminServer(serv, mockInstanceAdmin) 45 | 46 | lis, err := net.Listen("tcp", "localhost:0") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | go serv.Serve(lis) 51 | 52 | conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | instanceClientOpt = option.WithGRPCConn(conn) 57 | } 58 | 59 | func TestInstanceAdminGetInstance(t *testing.T) { 60 | setupInstanceAdminServer() 61 | var expectedResponse = &instancepb.Instance{ 62 | Name: "name2-1052831874", 63 | Config: "config-1354792126", 64 | DisplayName: "displayName1615086568", 65 | NodeCount: 1539922066, 66 | } 67 | 68 | mockInstanceAdmin.SetErr(nil) 69 | mockInstanceAdmin.SetReqs(nil) 70 | 71 | mockInstanceAdmin.SetResps(append(mockInstanceAdmin.Resps()[:0], expectedResponse)) 72 | 73 | formattedName := fmt.Sprintf("projects/%s/instances/%s", "[PROJECT]", "[INSTANCE]") 74 | request := &instancepb.GetInstanceRequest{ 75 | Name: formattedName, 76 | } 77 | 78 | c, err := instance.NewInstanceAdminClient(context.Background(), instanceClientOpt) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | resp, err := c.GetInstance(context.Background(), request) 84 | 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | if want, got := request, mockInstanceAdmin.Reqs()[0]; !proto.Equal(want, got) { 90 | t.Errorf("wrong request %q, want %q", got, want) 91 | } 92 | 93 | if want, got := expectedResponse, resp; !proto.Equal(want, got) { 94 | t.Errorf("wrong response %q, want %q)", got, want) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "database/sql/driver" 19 | "reflect" 20 | 21 | "google.golang.org/protobuf/proto" 22 | "google.golang.org/protobuf/reflect/protoreflect" 23 | ) 24 | 25 | /* Copied from types.go in database/sql */ 26 | var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem() 27 | 28 | func callValuerValue(vr driver.Valuer) (v driver.Value, err error) { 29 | if rv := reflect.ValueOf(vr); rv.Kind() == reflect.Pointer && 30 | rv.IsNil() && 31 | rv.Type().Elem().Implements(valuerReflectType) { 32 | return nil, nil 33 | } 34 | return vr.Value() 35 | } 36 | 37 | /* The following is the same implementation as in google-cloud-go/spanner */ 38 | 39 | func isStructOrArrayOfStructValue(v interface{}) bool { 40 | typ := reflect.TypeOf(v) 41 | if typ.Kind() == reflect.Slice { 42 | typ = typ.Elem() 43 | } 44 | if typ.Kind() == reflect.Ptr { 45 | typ = typ.Elem() 46 | } 47 | return typ.Kind() == reflect.Struct 48 | } 49 | 50 | func isAnArrayOfProtoColumn(v interface{}) bool { 51 | typ := reflect.TypeOf(v) 52 | if typ.Kind() == reflect.Ptr { 53 | typ = typ.Elem() 54 | } 55 | if typ.Kind() == reflect.Slice { 56 | typ = typ.Elem() 57 | } 58 | return typ.Implements(protoMsgReflectType) || typ.Implements(protoEnumReflectType) 59 | } 60 | 61 | var ( 62 | protoMsgReflectType = reflect.TypeOf((*proto.Message)(nil)).Elem() 63 | protoEnumReflectType = reflect.TypeOf((*protoreflect.Enum)(nil)).Elem() 64 | ) 65 | -------------------------------------------------------------------------------- /wrapped_row_iterator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spannerdriver 16 | 17 | import ( 18 | "cloud.google.com/go/spanner" 19 | "cloud.google.com/go/spanner/apiv1/spannerpb" 20 | "google.golang.org/api/iterator" 21 | ) 22 | 23 | type wrappedRowIterator struct { 24 | *spanner.RowIterator 25 | 26 | noRows bool 27 | firstRow *spanner.Row 28 | } 29 | 30 | func (ri *wrappedRowIterator) Next() (*spanner.Row, error) { 31 | if ri.noRows { 32 | return nil, iterator.Done 33 | } 34 | if ri.firstRow != nil { 35 | defer func() { ri.firstRow = nil }() 36 | return ri.firstRow, nil 37 | } 38 | return ri.RowIterator.Next() 39 | } 40 | 41 | func (ri *wrappedRowIterator) Stop() { 42 | ri.RowIterator.Stop() 43 | } 44 | 45 | func (ri *wrappedRowIterator) Metadata() (*spannerpb.ResultSetMetadata, error) { 46 | return ri.RowIterator.Metadata, nil 47 | } 48 | --------------------------------------------------------------------------------