Skip to content

Commit 6a6999e

Browse files
committed
Merge branch 'main' into spanner-lib
2 parents 9a33505 + d676f34 commit 6a6999e

File tree

8 files changed

+223
-13
lines changed

8 files changed

+223
-13
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "1.15.0"
2+
".": "1.16.0"
33
}

CHANGES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## [1.16.0](https://github.com/googleapis/go-sql-spanner/compare/v1.15.0...v1.16.0) (2025-07-03)
4+
5+
6+
### Features
7+
8+
* Add BeginTransactionOption to configure how to begin a transaction ([21ed1e3](https://github.com/googleapis/go-sql-spanner/commit/21ed1e3a744d7052781e6f28aa7f25dc4ed891df))
9+
* Add RunDmlBatch function with typed return value ([#471](https://github.com/googleapis/go-sql-spanner/issues/471)) ([12ead7c](https://github.com/googleapis/go-sql-spanner/commit/12ead7c5df87813b167ab9dec025cf78fee520a2))
10+
* Return metadata and stats for client-side statements ([#470](https://github.com/googleapis/go-sql-spanner/issues/470)) ([19238ce](https://github.com/googleapis/go-sql-spanner/commit/19238cec2f77998c0e8052794be2c25167057b04))
11+
12+
13+
### Bug Fixes
14+
15+
* Update all dependencies ([#464](https://github.com/googleapis/go-sql-spanner/issues/464)) ([574d6fe](https://github.com/googleapis/go-sql-spanner/commit/574d6fe6fa42a138b0123ac0bcf756413c87ea25))
16+
* Use client opts to auto configure emulator instead of environment variable ([#468](https://github.com/googleapis/go-sql-spanner/issues/468)) ([a518727](https://github.com/googleapis/go-sql-spanner/commit/a5187277ae02be0e3935d746751aadd76beb0c8e))
17+
18+
19+
### Performance Improvements
20+
21+
* Inline BeginTransaction with first statement ([21ed1e3](https://github.com/googleapis/go-sql-spanner/commit/21ed1e3a744d7052781e6f28aa7f25dc4ed891df))
22+
323
## [1.15.0](https://github.com/googleapis/go-sql-spanner/compare/v1.14.0...v1.15.0) (2025-06-27)
424

525

client_side_statement_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import (
2222
"time"
2323

2424
"cloud.google.com/go/spanner"
25+
"cloud.google.com/go/spanner/apiv1/spannerpb"
2526
"github.com/google/go-cmp/cmp"
27+
"github.com/google/go-cmp/cmp/cmpopts"
2628
"google.golang.org/grpc/codes"
29+
"google.golang.org/protobuf/types/known/structpb"
2730
)
2831

2932
func TestStatementExecutor_StartBatchDdl(t *testing.T) {
@@ -507,3 +510,100 @@ func TestStatementExecutor_SetTransactionTag(t *testing.T) {
507510

508511
}
509512
}
513+
514+
func TestStatementExecutor_UsesExecOptions(t *testing.T) {
515+
ctx := context.Background()
516+
c := &conn{retryAborts: true, logger: noopLogger}
517+
s := &statementExecutor{}
518+
519+
it, err := s.ShowTransactionTag(ctx, c, "", ExecOptions{DecodeOption: DecodeOptionProto, ReturnResultSetMetadata: true, ReturnResultSetStats: true}, nil)
520+
if err != nil {
521+
t.Fatalf("could not get current transaction tag value from connection: %v", err)
522+
}
523+
rows, ok := it.(driver.RowsNextResultSet)
524+
if !ok {
525+
t.Fatal("did not get RowsNextResultSet")
526+
}
527+
// The first result set contains the metadata.
528+
cols := rows.Columns()
529+
wantCols := []string{"metadata"}
530+
if !cmp.Equal(cols, wantCols) {
531+
t.Fatalf("column names mismatch\nGot: %v\nWant: %v", cols, wantCols)
532+
}
533+
wantValues := []driver.Value{&spannerpb.ResultSetMetadata{
534+
RowType: &spannerpb.StructType{
535+
Fields: []*spannerpb.StructType_Field{
536+
{Name: "TransactionTag", Type: &spannerpb.Type{Code: spannerpb.TypeCode_STRING}},
537+
},
538+
},
539+
}}
540+
values := make([]driver.Value, len(cols))
541+
if err := rows.Next(values); err != nil {
542+
t.Fatalf("failed to get first row: %v", err)
543+
}
544+
if !cmp.Equal(values, wantValues, cmpopts.IgnoreUnexported(spannerpb.ResultSetMetadata{}, spannerpb.StructType{}, spannerpb.StructType_Field{}, spannerpb.Type{})) {
545+
t.Fatalf("default transaction tag mismatch\nGot: %v\nWant: %v", values, wantValues)
546+
}
547+
if err := rows.Next(values); err != io.EOF {
548+
t.Fatalf("error mismatch\nGot: %v\nWant: %v", err, io.EOF)
549+
}
550+
551+
// Move to the next result set, which should contain the data.
552+
if !rows.HasNextResultSet() {
553+
t.Fatal("missing next result set")
554+
}
555+
if err := rows.NextResultSet(); err != nil {
556+
t.Fatalf("error mismatch\n Got: %v\nWant: %v", err, nil)
557+
}
558+
559+
cols = rows.Columns()
560+
wantCols = []string{"TransactionTag"}
561+
if !cmp.Equal(cols, wantCols) {
562+
t.Fatalf("column names mismatch\nGot: %v\nWant: %v", cols, wantCols)
563+
}
564+
values = make([]driver.Value, len(cols))
565+
if err := rows.Next(values); err != nil {
566+
t.Fatalf("failed to get first row: %v", err)
567+
}
568+
// The value that we get should be the raw protobuf value.
569+
wantValues = []driver.Value{spanner.GenericColumnValue{
570+
Type: &spannerpb.Type{Code: spannerpb.TypeCode_STRING},
571+
Value: &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: ""}},
572+
}}
573+
if !cmp.Equal(values, wantValues, cmpopts.IgnoreUnexported(spannerpb.Type{}, structpb.Value{})) {
574+
t.Fatalf("default transaction tag mismatch\nGot: %v\nWant: %v", values, wantValues)
575+
}
576+
if err := rows.Next(values); err != io.EOF {
577+
t.Fatalf("error mismatch\nGot: %v\nWant: %v", err, io.EOF)
578+
}
579+
580+
// Move to the next result set, which should contain the ResultSetStats.
581+
if !rows.HasNextResultSet() {
582+
t.Fatal("missing next result set")
583+
}
584+
if err := rows.NextResultSet(); err != nil {
585+
t.Fatalf("error mismatch\n Got: %v\nWant: %v", err, nil)
586+
}
587+
cols = rows.Columns()
588+
wantCols = []string{"stats"}
589+
if !cmp.Equal(cols, wantCols) {
590+
t.Fatalf("column names mismatch\nGot: %v\nWant: %v", cols, wantCols)
591+
}
592+
wantValues = []driver.Value{&spannerpb.ResultSetStats{}}
593+
values = make([]driver.Value, len(cols))
594+
if err := rows.Next(values); err != nil {
595+
t.Fatalf("failed to get first row: %v", err)
596+
}
597+
if !cmp.Equal(values, wantValues, cmpopts.IgnoreUnexported(spannerpb.ResultSetStats{})) {
598+
t.Fatalf("ResultSetStats mismatch\nGot: %v\nWant: %v", values, wantValues)
599+
}
600+
if err := rows.Next(values); err != io.EOF {
601+
t.Fatalf("error mismatch\nGot: %v\nWant: %v", err, io.EOF)
602+
}
603+
604+
// There should be no more result sets.
605+
if rows.HasNextResultSet() {
606+
t.Fatal("got unexpected next result set")
607+
}
608+
609+
}

conn.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ func (c *conn) startBatchDDL() (driver.Result, error) {
513513
}
514514

515515
func (c *conn) startBatchDML(automatic bool) (driver.Result, error) {
516-
execOptions := c.options( /* reset = */ true)
516+
execOptions := c.options( /*reset = */ true)
517517

518518
if c.inTransaction() {
519519
return c.tx.StartBatchDML(execOptions.QueryOptions, automatic)

conn_with_mockserver_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,82 @@ func TestDDLUsingQueryContextInReadWriteTransaction(t *testing.T) {
389389
t.Fatalf("error mismatch\n Got: %v\nWant: %v", g, w)
390390
}
391391
}
392+
393+
func TestRunDmlBatch(t *testing.T) {
394+
t.Parallel()
395+
396+
db, _, teardown := setupTestDBConnection(t)
397+
defer teardown()
398+
ctx := context.Background()
399+
400+
conn, err := db.Conn(ctx)
401+
if err != nil {
402+
t.Fatal(err)
403+
}
404+
defer silentClose(conn)
405+
if err := conn.Raw(func(driverConn interface{}) error {
406+
spannerConn, _ := driverConn.(SpannerConn)
407+
return spannerConn.StartBatchDML()
408+
}); err != nil {
409+
t.Fatal(err)
410+
}
411+
// Buffer two DML statements.
412+
for range 2 {
413+
if _, err := conn.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
414+
t.Fatal(err)
415+
}
416+
}
417+
var res SpannerResult
418+
if err := conn.Raw(func(driverConn interface{}) (err error) {
419+
spannerConn, _ := driverConn.(SpannerConn)
420+
res, err = spannerConn.RunDmlBatch(ctx)
421+
return err
422+
}); err != nil {
423+
t.Fatal(err)
424+
}
425+
affected, err := res.BatchRowsAffected()
426+
if err != nil {
427+
t.Fatal(err)
428+
}
429+
if g, w := affected, []int64{testutil.UpdateBarSetFooRowCount, testutil.UpdateBarSetFooRowCount}; !reflect.DeepEqual(g, w) {
430+
t.Fatalf("affected mismatch\n Got: %v\nWant: %v", g, w)
431+
}
432+
}
433+
434+
func TestSetRetryAbortsInternallyInInactiveTransaction(t *testing.T) {
435+
t.Parallel()
436+
437+
db, _, teardown := setupTestDBConnection(t)
438+
defer teardown()
439+
ctx := context.Background()
440+
441+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
442+
if err != nil {
443+
t.Fatal(err)
444+
}
445+
if _, err := tx.ExecContext(ctx, "set retry_aborts_internally = false"); err != nil {
446+
t.Fatal(err)
447+
}
448+
_ = tx.Rollback()
449+
}
450+
451+
func TestSetRetryAbortsInternallyInActiveTransaction(t *testing.T) {
452+
t.Parallel()
453+
454+
db, _, teardown := setupTestDBConnection(t)
455+
defer teardown()
456+
ctx := context.Background()
457+
458+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
459+
if err != nil {
460+
t.Fatal(err)
461+
}
462+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
463+
t.Fatal(err)
464+
}
465+
_, err = tx.ExecContext(ctx, "set retry_aborts_internally = false")
466+
if g, w := err.Error(), "spanner: code = \"FailedPrecondition\", desc = \"cannot change retry mode while a transaction is active\""; g != w {
467+
t.Fatalf("error mismatch\n Got: %v\nWant: %v", g, w)
468+
}
469+
_ = tx.Rollback()
470+
}

driver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import (
4646
"google.golang.org/grpc/status"
4747
)
4848

49-
const userAgent = "go-sql-spanner/1.15.0" // x-release-please-version
49+
const userAgent = "go-sql-spanner/1.16.0" // x-release-please-version
5050

5151
const gormModule = "github.com/googleapis/go-gorm-spanner"
5252
const gormUserAgent = "go-gorm-spanner"

driver_with_mockserver_test.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,20 +2673,25 @@ func TestShowAndSetVariableRetryAbortsInternally(t *testing.T) {
26732673
}
26742674
}
26752675

2676-
tx, _ := c.BeginTx(ctx, nil)
2677-
defer func() { _ = tx.Rollback() }()
2678-
// Verify that the value can be set before the transaction has been activated.
2679-
if _, err = c.ExecContext(ctx, "SET RETRY_ABORTS_INTERNALLY = TRUE"); err != nil {
2680-
t.Fatalf("failed to set value for retry_aborts_internally for an inactive transaction: %v", err)
2681-
}
26822676
// Verify that the value cannot be set during an active transaction.
2683-
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
2684-
t.Fatalf("failed to execute test update statement: %v", err)
2677+
tx, _ := c.BeginTx(ctx, nil)
2678+
// Execute a statement to activate the transaction.
2679+
if _, err := c.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
2680+
t.Fatal(err)
26852681
}
26862682
_, err = c.ExecContext(ctx, "SET RETRY_ABORTS_INTERNALLY = TRUE")
26872683
if g, w := spanner.ErrCode(err), codes.FailedPrecondition; g != w {
26882684
t.Fatalf("error code mismatch for setting retry_aborts_internally during a transaction\nGot: %v\nWant: %v", g, w)
26892685
}
2686+
_ = tx.Rollback()
2687+
2688+
// Verify that the value can be set at the start of a transaction
2689+
// before any statements have been executed.
2690+
tx, _ = c.BeginTx(ctx, nil)
2691+
if _, err = c.ExecContext(ctx, "SET RETRY_ABORTS_INTERNALLY = TRUE"); err != nil {
2692+
t.Fatal(err)
2693+
}
2694+
_ = tx.Rollback()
26902695
}
26912696

26922697
func TestPartitionedDml(t *testing.T) {

examples/dml-batches/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,17 @@ func dmlBatch(projectId, instanceId, databaseId string) error {
106106
return fmt.Errorf("failed to insert: %v", err)
107107
}
108108
// 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)
109+
var res spannerdriver.SpannerResult
110+
if err := conn.Raw(func(driverConn interface{}) (err error) {
111+
res, err = driverConn.(spannerdriver.SpannerConn).RunDmlBatch(ctx)
112+
return err
111113
}); err != nil {
112114
return fmt.Errorf("failed to run DML batch: %v", err)
113115
}
116+
// BatchRowsAffected returns a slice with the affected rows per DML statement in the batch.
117+
affected, _ := res.BatchRowsAffected()
118+
fmt.Printf("Affected rows: %v\n", affected)
119+
114120
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM Singers").Scan(&c); err != nil {
115121
return fmt.Errorf("failed to get singers count: %v", err)
116122
}

0 commit comments

Comments
 (0)