From f1db3487e67d178e39e4328291be9a333b2a96f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 17 May 2024 14:21:11 +0200 Subject: [PATCH] docs: add transaction samples --- .../query_data_with_new_column.go | 11 ++- .../snippets/golang-snippets/samples_test.go | 6 ++ .../update_data_with_transaction.go | 86 +++++++++++++++++++ .../src/update_data_with_transaction.ts | 65 ++++++++++++++ .../nodejs-snippets/test/sample_tests.ts | 5 ++ 5 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 samples/snippets/golang-snippets/update_data_with_transaction.go create mode 100644 samples/snippets/nodejs-snippets/src/update_data_with_transaction.ts diff --git a/samples/snippets/golang-snippets/query_data_with_new_column.go b/samples/snippets/golang-snippets/query_data_with_new_column.go index 8b64a499e..22eb22e02 100644 --- a/samples/snippets/golang-snippets/query_data_with_new_column.go +++ b/samples/snippets/golang-snippets/query_data_with_new_column.go @@ -16,6 +16,7 @@ package golang_snippets // [START spanner_query_data_with_new_column] import ( "context" + "database/sql" "fmt" "github.com/jackc/pgx/v5" @@ -41,12 +42,18 @@ func queryDataWithNewColumn(host string, port int, database string) error { } for rows.Next() { var singerId, albumId int64 - var marketingBudget string + var marketingBudget sql.NullString err = rows.Scan(&singerId, &albumId, &marketingBudget) if err != nil { return err } - fmt.Printf("%v %v %v\n", singerId, albumId, marketingBudget) + var budget string + if marketingBudget.Valid { + budget = marketingBudget.String + } else { + budget = "NULL" + } + fmt.Printf("%v %v %v\n", singerId, albumId, budget) } return rows.Err() diff --git a/samples/snippets/golang-snippets/samples_test.go b/samples/snippets/golang-snippets/samples_test.go index 267d2f266..89b4f03c0 100644 --- a/samples/snippets/golang-snippets/samples_test.go +++ b/samples/snippets/golang-snippets/samples_test.go @@ -72,4 +72,10 @@ func TestSamples(t *testing.T) { if err := updateDataWithCopy(host, port, db); err != nil { t.Fatalf("update data with copy failed: %v", err) } + if err := queryDataWithNewColumn(host, port, db); err != nil { + t.Fatalf("query data with with new column failed: %v", err) + } + if err := writeWithTransactionUsingDml(host, port, db); err != nil { + t.Fatalf("update data using a transaction failed: %v", err) + } } diff --git a/samples/snippets/golang-snippets/update_data_with_transaction.go b/samples/snippets/golang-snippets/update_data_with_transaction.go new file mode 100644 index 000000000..1dff724bd --- /dev/null +++ b/samples/snippets/golang-snippets/update_data_with_transaction.go @@ -0,0 +1,86 @@ +/* +Copyright 2024 Google LLC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package golang_snippets + +// [START spanner_dml_getting_started_update] +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5" +) + +func writeWithTransactionUsingDml(host string, port int, database string) error { + ctx := context.Background() + connString := fmt.Sprintf( + "postgres://uid:pwd@%s:%d/%s?sslmode=disable", + host, port, database) + conn, err := pgx.Connect(ctx, connString) + if err != nil { + return err + } + defer conn.Close(ctx) + + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. + tx, err := conn.Begin(ctx) + if err != nil { + return err + } + const selectSql = "SELECT marketing_budget " + + "from albums " + + "WHERE singer_id = $1 and album_id = $2" + // Get the marketing_budget of singer 2 / album 2. + row := tx.QueryRow(ctx, selectSql, 2, 2) + var budget2 int64 + if err := row.Scan(&budget2); err != nil { + tx.Rollback(ctx) + return err + } + const transfer = 20000 + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + if budget2 >= transfer { + // Get the marketing_budget of singer 1 / album 1. + row := tx.QueryRow(ctx, selectSql, 1, 1) + var budget1 int64 + if err := row.Scan(&budget1); err != nil { + tx.Rollback(ctx) + return err + } + // Transfer part of the marketing budget of Album 2 to Album 1. + budget1 += transfer + budget2 -= transfer + const updateSql = "UPDATE albums " + + "SET marketing_budget = $1 " + + "WHERE singer_id = $2 and album_id = $3" + // Start a DML batch and execute it as part of the current transaction. + batch := &pgx.Batch{} + batch.Queue(updateSql, budget1, 1, 1) + batch.Queue(updateSql, budget2, 2, 2) + br := tx.SendBatch(ctx, batch) + _, err = br.Exec() + if err := br.Close(); err != nil { + tx.Rollback(ctx) + return err + } + } + // Commit the current transaction. + tx.Commit(ctx) + fmt.Println("Transferred marketing budget from Album 2 to Album 1") + + return nil +} + +// [END spanner_dml_getting_started_update] diff --git a/samples/snippets/nodejs-snippets/src/update_data_with_transaction.ts b/samples/snippets/nodejs-snippets/src/update_data_with_transaction.ts new file mode 100644 index 000000000..a691226f2 --- /dev/null +++ b/samples/snippets/nodejs-snippets/src/update_data_with_transaction.ts @@ -0,0 +1,65 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// [START spanner_dml_getting_started_update] +import { Client } from 'pg'; + +async function writeWithTransactionUsingDml(host: string, port: number, database: string): Promise { + const connection = new Client({ + host: host, + port: port, + database: database, + }); + await connection.connect(); + + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. node-postgres + // requires you to explicitly start the transaction by executing 'begin'. + await connection.query("begin"); + const selectMarketingBudgetSql = "SELECT marketing_budget " + + "from albums " + + "WHERE singer_id = $1 and album_id = $2"; + // Get the marketing_budget of singer 2 / album 2. + const album2BudgetResult = await connection.query(selectMarketingBudgetSql, [2, 2]); + let album2Budget = album2BudgetResult.rows[0]["marketing_budget"]; + const transfer = 200000; + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + if (album2Budget >= transfer) { + // Get the marketing budget of singer 1 / album 1. + const album1BudgetResult = await connection.query(selectMarketingBudgetSql, [1, 1]); + let album1Budget = album1BudgetResult.rows[0]["marketing_budget"]; + // Transfer part of the marketing budget of Album 2 to Album 1. + album1Budget += transfer; + album2Budget -= transfer; + const updateSql = "UPDATE albums " + + "SET marketing_budget = $1 " + + "WHERE singer_id = $2 and album_id = $3"; + // Start a DML batch. This batch will become part of the current transaction. + await connection.query("start batch dml"); + // Update the marketing budget of both albums. + await connection.query(updateSql, [album1Budget, 1, 1]); + await connection.query(updateSql, [album2Budget, 2, 2]); + await connection.query("run batch"); + } + // Commit the current transaction. + await connection.query("commit"); + console.log("Transferred marketing budget from Album 2 to Album 1"); + + // Close the connection. + await connection.end(); +} +// [END spanner_dml_getting_started_update] + +export = writeWithTransactionUsingDml; diff --git a/samples/snippets/nodejs-snippets/test/sample_tests.ts b/samples/snippets/nodejs-snippets/test/sample_tests.ts index ec598abd1..abb3997d0 100644 --- a/samples/snippets/nodejs-snippets/test/sample_tests.ts +++ b/samples/snippets/nodejs-snippets/test/sample_tests.ts @@ -29,6 +29,7 @@ import addColumn from "../src/add_column"; import ddlBatch from "../src/ddl_batch"; import updateDataWithCopy from "../src/update_data_with_copy"; import queryDataWithNewColumn from "../src/query_data_with_new_column"; +import writeWithTransactionUsingDml from "../src/update_data_with_transaction"; const container: TestContainer = new GenericContainer("gcr.io/cloud-spanner-pg-adapter/pgadapter-emulator") .withExposedPorts(5432) @@ -103,4 +104,8 @@ describe('running samples', () => { expect(console.log).toHaveBeenCalledWith("2 2 500000"); expect(console.log).toHaveBeenCalledWith("2 3 null"); }, 30000); + test('update data with transaction', async () => { + await writeWithTransactionUsingDml(startedTestContainer.getHost(), startedTestContainer.getMappedPort(5432), "example-db"); + expect(console.log).toHaveBeenCalledWith("Transferred marketing budget from Album 2 to Album 1"); + }, 30000); });