Skip to content

Commit d68fc12

Browse files
committed
chore: add ExecuteBatch to SpannerLib
Adds an ExecuteBatch function to SpannerLib that supports executing DML or DDL statements as a single batch. The function accepts an ExecuteBatchDml request for both types of batches. The type of batch that is actually being executed is determined based on the statements in the batch. Mixing DML and DDL in the same batch is not supported. Queries are also not supported in batches.
1 parent 860b1cc commit d68fc12

File tree

10 files changed

+680
-0
lines changed

10 files changed

+680
-0
lines changed

conn.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ type SpannerConn interface {
222222
// return the same Spanner client.
223223
UnderlyingClient() (client *spanner.Client, err error)
224224

225+
// DetectStatementType returns the type of SQL statement.
226+
DetectStatementType(query string) parser.StatementType
227+
225228
// resetTransactionForRetry resets the current transaction after it has
226229
// been aborted by Spanner. Calling this function on a transaction that
227230
// has not been aborted is not supported and will cause an error to be
@@ -286,6 +289,11 @@ func (c *conn) UnderlyingClient() (*spanner.Client, error) {
286289
return c.client, nil
287290
}
288291

292+
func (c *conn) DetectStatementType(query string) parser.StatementType {
293+
info := c.parser.DetectStatementType(query)
294+
return info.StatementType
295+
}
296+
289297
func (c *conn) CommitTimestamp() (time.Time, error) {
290298
ts := propertyCommitTimestamp.GetValueOrDefault(c.state)
291299
if ts == nil {

spannerlib/api/batch_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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 api
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"reflect"
21+
"testing"
22+
23+
"cloud.google.com/go/longrunning/autogen/longrunningpb"
24+
"cloud.google.com/go/spanner"
25+
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
26+
"cloud.google.com/go/spanner/apiv1/spannerpb"
27+
"github.com/google/go-cmp/cmp"
28+
"github.com/google/go-cmp/cmp/cmpopts"
29+
"github.com/googleapis/go-sql-spanner/testutil"
30+
"google.golang.org/grpc/codes"
31+
"google.golang.org/protobuf/proto"
32+
"google.golang.org/protobuf/types/known/anypb"
33+
"google.golang.org/protobuf/types/known/emptypb"
34+
)
35+
36+
func TestExecuteDmlBatch(t *testing.T) {
37+
t.Parallel()
38+
39+
ctx := context.Background()
40+
server, teardown := setupMockServer(t)
41+
defer teardown()
42+
dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address)
43+
44+
poolId, err := CreatePool(ctx, dsn)
45+
if err != nil {
46+
t.Fatalf("CreatePool returned unexpected error: %v", err)
47+
}
48+
connId, err := CreateConnection(ctx, poolId)
49+
if err != nil {
50+
t.Fatalf("CreateConnection returned unexpected error: %v", err)
51+
}
52+
53+
// Execute a DML batch.
54+
request := &spannerpb.ExecuteBatchDmlRequest{Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{
55+
{Sql: testutil.UpdateBarSetFoo},
56+
{Sql: testutil.UpdateBarSetFoo},
57+
}}
58+
resp, err := ExecuteBatch(ctx, poolId, connId, request)
59+
if err != nil {
60+
t.Fatalf("ExecuteBatch returned unexpected error: %v", err)
61+
}
62+
if g, w := len(resp.ResultSets), 2; g != w {
63+
t.Fatalf("num results mismatch\n Got: %d\nWant: %d", g, w)
64+
}
65+
for i, result := range resp.ResultSets {
66+
if g, w := result.Stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w {
67+
t.Fatalf("%d: update count mismatch\n Got: %d\nWant: %d", i, g, w)
68+
}
69+
}
70+
71+
requests := server.TestSpanner.DrainRequestsFromServer()
72+
// There should be no ExecuteSql requests.
73+
executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
74+
if g, w := len(executeRequests), 0; g != w {
75+
t.Fatalf("Execute request count mismatch\n Got: %v\nWant: %v", g, w)
76+
}
77+
batchRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteBatchDmlRequest{}))
78+
if g, w := len(batchRequests), 1; g != w {
79+
t.Fatalf("Execute batch request count mismatch\n Got: %v\nWant: %v", g, w)
80+
}
81+
82+
if err := CloseConnection(ctx, poolId, connId); err != nil {
83+
t.Fatalf("CloseConnection returned unexpected error: %v", err)
84+
}
85+
if err := ClosePool(ctx, poolId); err != nil {
86+
t.Fatalf("ClosePool returned unexpected error: %v", err)
87+
}
88+
}
89+
90+
func TestExecuteDdlBatch(t *testing.T) {
91+
t.Parallel()
92+
93+
ctx := context.Background()
94+
server, teardown := setupMockServer(t)
95+
defer teardown()
96+
dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address)
97+
// Set up a result for a DDL statement on the mock server.
98+
var expectedResponse = &emptypb.Empty{}
99+
anyMsg, _ := anypb.New(expectedResponse)
100+
server.TestDatabaseAdmin.SetResps([]proto.Message{
101+
&longrunningpb.Operation{
102+
Done: true,
103+
Result: &longrunningpb.Operation_Response{Response: anyMsg},
104+
Name: "test-operation",
105+
},
106+
})
107+
108+
poolId, err := CreatePool(ctx, dsn)
109+
if err != nil {
110+
t.Fatalf("CreatePool returned unexpected error: %v", err)
111+
}
112+
connId, err := CreateConnection(ctx, poolId)
113+
if err != nil {
114+
t.Fatalf("CreateConnection returned unexpected error: %v", err)
115+
}
116+
117+
// Execute a DDL batch. This also uses a DML batch request.
118+
request := &spannerpb.ExecuteBatchDmlRequest{Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{
119+
{Sql: "create table my_table (id int64 primary key, value string(100))"},
120+
{Sql: "create index my_index on my_table (value)"},
121+
}}
122+
resp, err := ExecuteBatch(ctx, poolId, connId, request)
123+
if err != nil {
124+
t.Fatalf("ExecuteBatch returned unexpected error: %v", err)
125+
}
126+
// The response should contain an 'update count' per DDL statement.
127+
if g, w := len(resp.ResultSets), 2; g != w {
128+
t.Fatalf("num results mismatch\n Got: %d\nWant: %d", g, w)
129+
}
130+
// There is no update count for DDL statements.
131+
for i, result := range resp.ResultSets {
132+
emptyStats := &spannerpb.ResultSetStats{}
133+
if g, w := result.Stats, emptyStats; !cmp.Equal(g, w, cmpopts.IgnoreUnexported(spannerpb.ResultSetStats{})) {
134+
t.Fatalf("%d: ResultSetStats mismatch\n Got: %v\nWant: %v", i, g, w)
135+
}
136+
}
137+
138+
requests := server.TestSpanner.DrainRequestsFromServer()
139+
// There should be no ExecuteSql requests.
140+
executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
141+
if g, w := len(executeRequests), 0; g != w {
142+
t.Fatalf("Execute request count mismatch\n Got: %v\nWant: %v", g, w)
143+
}
144+
// There should also be no ExecuteBatchDml requests.
145+
batchDmlRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteBatchDmlRequest{}))
146+
if g, w := len(batchDmlRequests), 0; g != w {
147+
t.Fatalf("ExecuteBatchDmlRequest count mismatch\n Got: %v\nWant: %v", g, w)
148+
}
149+
150+
adminRequests := server.TestDatabaseAdmin.Reqs()
151+
if g, w := len(adminRequests), 1; g != w {
152+
t.Fatalf("admin request count mismatch\n Got: %v\nWant: %v", g, w)
153+
}
154+
ddlRequest := adminRequests[0].(*databasepb.UpdateDatabaseDdlRequest)
155+
if g, w := len(ddlRequest.Statements), 2; g != w {
156+
t.Fatalf("DDL statement count mismatch\n Got: %v\nWant: %v", g, w)
157+
}
158+
159+
if err := CloseConnection(ctx, poolId, connId); err != nil {
160+
t.Fatalf("CloseConnection returned unexpected error: %v", err)
161+
}
162+
if err := ClosePool(ctx, poolId); err != nil {
163+
t.Fatalf("ClosePool returned unexpected error: %v", err)
164+
}
165+
}
166+
167+
func TestExecuteMixedBatch(t *testing.T) {
168+
t.Parallel()
169+
170+
ctx := context.Background()
171+
server, teardown := setupMockServer(t)
172+
defer teardown()
173+
dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address)
174+
175+
poolId, err := CreatePool(ctx, dsn)
176+
if err != nil {
177+
t.Fatalf("CreatePool returned unexpected error: %v", err)
178+
}
179+
connId, err := CreateConnection(ctx, poolId)
180+
if err != nil {
181+
t.Fatalf("CreateConnection returned unexpected error: %v", err)
182+
}
183+
184+
// Try to execute a batch with mixed DML and DDL statements. This should fail.
185+
request := &spannerpb.ExecuteBatchDmlRequest{Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{
186+
{Sql: "create table my_table (id int64 primary key, value string(100))"},
187+
{Sql: "update my_table set value = 100 where true"},
188+
}}
189+
_, err = ExecuteBatch(ctx, poolId, connId, request)
190+
if g, w := spanner.ErrCode(err), codes.InvalidArgument; g != w {
191+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
192+
}
193+
194+
if err := CloseConnection(ctx, poolId, connId); err != nil {
195+
t.Fatalf("CloseConnection returned unexpected error: %v", err)
196+
}
197+
if err := ClosePool(ctx, poolId); err != nil {
198+
t.Fatalf("ClosePool returned unexpected error: %v", err)
199+
}
200+
}
201+
202+
func TestExecuteDdlBatchInTransaction(t *testing.T) {
203+
t.Parallel()
204+
205+
ctx := context.Background()
206+
server, teardown := setupMockServer(t)
207+
defer teardown()
208+
dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address)
209+
210+
poolId, err := CreatePool(ctx, dsn)
211+
if err != nil {
212+
t.Fatalf("CreatePool returned unexpected error: %v", err)
213+
}
214+
connId, err := CreateConnection(ctx, poolId)
215+
if err != nil {
216+
t.Fatalf("CreateConnection returned unexpected error: %v", err)
217+
}
218+
if err := BeginTransaction(ctx, poolId, connId, &spannerpb.TransactionOptions{}); err != nil {
219+
t.Fatalf("BeginTransaction returned unexpected error: %v", err)
220+
}
221+
222+
// Try to execute a DDL batch in a transaction. This should fail.
223+
request := &spannerpb.ExecuteBatchDmlRequest{Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{
224+
{Sql: "create table my_table (id int64 primary key, value string(100))"},
225+
{Sql: "create index my_index on my_table (value)"},
226+
}}
227+
_, err = ExecuteBatch(ctx, poolId, connId, request)
228+
if g, w := spanner.ErrCode(err), codes.FailedPrecondition; g != w {
229+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
230+
}
231+
232+
if err := CloseConnection(ctx, poolId, connId); err != nil {
233+
t.Fatalf("CloseConnection returned unexpected error: %v", err)
234+
}
235+
if err := ClosePool(ctx, poolId); err != nil {
236+
t.Fatalf("ClosePool returned unexpected error: %v", err)
237+
}
238+
}

0 commit comments

Comments
 (0)