Skip to content

Commit aa00055

Browse files
authored
feat: allow DDL statements with QueryContext (#515)
Allow DDL statements to be used with the QueryContext function. This makes the function a 'catch-all' function that can be used for all types of statements, without having to worry beforehand what the type of statement is. The driver returns an empty row iterator for DDL statements.
1 parent 04d384e commit aa00055

File tree

4 files changed

+189
-12
lines changed

4 files changed

+189
-12
lines changed

conn.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,9 +836,13 @@ func (c *conn) queryContext(ctx context.Context, query string, execOptions *Exec
836836
return nil, err
837837
}
838838
statementType := c.parser.DetectStatementType(query)
839-
// DDL statements are not supported in QueryContext so fail early.
839+
// DDL statements are not supported in QueryContext so use the execContext method for the execution.
840840
if statementType.StatementType == parser.StatementTypeDdl {
841-
return nil, spanner.ToSpannerError(status.Errorf(codes.FailedPrecondition, "QueryContext does not support DDL statements, use ExecContext instead"))
841+
res, err := c.execContext(ctx, query, execOptions, args)
842+
if err != nil {
843+
return nil, err
844+
}
845+
return createDriverResultRows(res, execOptions), nil
842846
}
843847
var iter rowIterator
844848
if c.tx == nil {

conn_with_mockserver_test.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -567,17 +567,26 @@ func TestDropDatabase(t *testing.T) {
567567
func TestDDLUsingQueryContext(t *testing.T) {
568568
t.Parallel()
569569

570-
db, _, teardown := setupTestDBConnection(t)
570+
db, server, teardown := setupTestDBConnection(t)
571571
defer teardown()
572+
var expectedResponse = &emptypb.Empty{}
573+
anyMsg, _ := anypb.New(expectedResponse)
574+
server.TestDatabaseAdmin.SetResps([]proto.Message{
575+
&longrunningpb.Operation{
576+
Done: true,
577+
Result: &longrunningpb.Operation_Response{Response: anyMsg},
578+
Name: "test-operation",
579+
},
580+
})
572581
ctx := context.Background()
573582

574-
// DDL statements should not use the query context.
575-
_, err := db.QueryContext(ctx, "CREATE TABLE Foo (Bar STRING(100))")
576-
if err == nil {
577-
t.Fatal("expected error for DDL statement using QueryContext, got nil")
578-
}
579-
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "QueryContext does not support DDL statements, use ExecContext instead"`; g != w {
580-
t.Fatalf("error mismatch\n Got: %v\nWant: %v", g, w)
583+
// DDL statements should be able to use QueryContext.
584+
if it, err := db.QueryContext(ctx, "CREATE TABLE Foo (Bar STRING(100))"); err != nil {
585+
t.Fatal(err)
586+
} else {
587+
if it.Next() {
588+
t.Fatalf("DDL should not return any rows")
589+
}
581590
}
582591
}
583592

@@ -598,7 +607,7 @@ func TestDDLUsingQueryContextInReadOnlyTx(t *testing.T) {
598607
if err == nil {
599608
t.Fatal("expected error for DDL statement using QueryContext in read-only transaction, got nil")
600609
}
601-
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "QueryContext does not support DDL statements, use ExecContext instead"`; g != w {
610+
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "cannot execute DDL as part of a transaction"`; g != w {
602611
t.Fatalf("error mismatch\n Got: %v\nWant: %v", g, w)
603612
}
604613
}
@@ -621,7 +630,7 @@ func TestDDLUsingQueryContextInReadWriteTransaction(t *testing.T) {
621630
if err == nil {
622631
t.Fatal("expected error for DDL statement using QueryContext in read-write transaction, got nil")
623632
}
624-
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "QueryContext does not support DDL statements, use ExecContext instead"`; g != w {
633+
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "cannot execute DDL as part of a transaction"`; g != w {
625634
t.Fatalf("error mismatch\n Got: %v\nWant: %v", g, w)
626635
}
627636
}

rows.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,97 @@ func (r *rows) nextStats(dest []driver.Value) error {
476476
dest[0] = r.it.ResultSetStats()
477477
return nil
478478
}
479+
480+
var _ driver.Rows = (*emptyRows)(nil)
481+
var _ driver.RowsNextResultSet = (*emptyRows)(nil)
482+
var emptyRowsMetadata = &sppb.ResultSetMetadata{
483+
RowType: &sppb.StructType{
484+
Fields: []*sppb.StructType_Field{{Name: "affected_rows", Type: &sppb.Type{Code: sppb.TypeCode_INT64}}},
485+
},
486+
}
487+
var emptyRowsStats = &sppb.ResultSetStats{}
488+
489+
type emptyRows struct {
490+
currentResultSetType resultSetType
491+
returnResultSetMetadata bool
492+
returnResultSetStats bool
493+
494+
hasReturnedResultSetMetadata bool
495+
hasReturnedResultSetStats bool
496+
}
497+
498+
func createDriverResultRows(_ driver.Result, opts *ExecOptions) *emptyRows {
499+
res := &emptyRows{
500+
returnResultSetMetadata: opts.ReturnResultSetMetadata,
501+
returnResultSetStats: opts.ReturnResultSetStats,
502+
}
503+
if !opts.ReturnResultSetMetadata {
504+
res.currentResultSetType = resultSetTypeResults
505+
}
506+
return res
507+
}
508+
509+
func (e *emptyRows) HasNextResultSet() bool {
510+
if e.currentResultSetType == resultSetTypeMetadata && e.returnResultSetMetadata {
511+
return true
512+
}
513+
if e.currentResultSetType == resultSetTypeResults && e.returnResultSetStats {
514+
return true
515+
}
516+
return false
517+
}
518+
519+
func (e *emptyRows) NextResultSet() error {
520+
if !e.HasNextResultSet() {
521+
return io.EOF
522+
}
523+
e.currentResultSetType++
524+
return nil
525+
}
526+
527+
func (e *emptyRows) Columns() []string {
528+
switch e.currentResultSetType {
529+
case resultSetTypeMetadata:
530+
return []string{"metadata"}
531+
case resultSetTypeResults:
532+
return []string{"affected_rows"}
533+
case resultSetTypeStats:
534+
return []string{"stats"}
535+
case resultSetTypeNoMoreResults:
536+
return []string{}
537+
}
538+
return []string{}
539+
}
540+
541+
func (e *emptyRows) Close() error {
542+
return nil
543+
}
544+
545+
func (e *emptyRows) Next(dest []driver.Value) error {
546+
if e.currentResultSetType == resultSetTypeMetadata {
547+
return e.nextMetadata(dest)
548+
}
549+
if e.currentResultSetType == resultSetTypeStats {
550+
return e.nextStats(dest)
551+
}
552+
553+
return io.EOF
554+
}
555+
556+
func (e *emptyRows) nextMetadata(dest []driver.Value) error {
557+
if e.hasReturnedResultSetMetadata {
558+
return io.EOF
559+
}
560+
e.hasReturnedResultSetMetadata = true
561+
dest[0] = emptyRowsMetadata
562+
return nil
563+
}
564+
565+
func (e *emptyRows) nextStats(dest []driver.Value) error {
566+
if e.hasReturnedResultSetStats {
567+
return io.EOF
568+
}
569+
e.hasReturnedResultSetStats = true
570+
dest[0] = emptyRowsStats
571+
return nil
572+
}

rows_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ package spannerdriver
1616

1717
import (
1818
"database/sql/driver"
19+
"errors"
1920
"fmt"
2021
"io"
22+
"reflect"
2123
"testing"
2224

2325
"cloud.google.com/go/spanner"
2426
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
27+
"github.com/google/go-cmp/cmp"
2528
"google.golang.org/protobuf/types/known/structpb"
2629
)
2730

@@ -153,3 +156,70 @@ func TestRows_Next_Unsupported(t *testing.T) {
153156
t.Fatalf("expected error %q, but got %q", expectedError, err.Error())
154157
}
155158
}
159+
160+
func TestEmptyRows(t *testing.T) {
161+
r := createDriverResultRows(&result{}, &ExecOptions{})
162+
163+
if g, w := r.Columns(), []string{"affected_rows"}; !cmp.Equal(g, w) {
164+
t.Fatalf("columns mismatch\n Got: %v\nWant: %v", g, w)
165+
}
166+
if r.HasNextResultSet() {
167+
t.Fatalf("unexpected next result set available")
168+
}
169+
}
170+
171+
func TestEmptyRowsWithMetadataAndStats(t *testing.T) {
172+
r := createDriverResultRows(&result{}, &ExecOptions{ReturnResultSetMetadata: true, ReturnResultSetStats: true})
173+
174+
// The first result set should contain ResultSetMetadata.
175+
if g, w := r.Columns(), []string{"metadata"}; !cmp.Equal(g, w) {
176+
t.Fatalf("columns mismatch\n Got: %v\nWant: %v", g, w)
177+
}
178+
values := make([]driver.Value, 1)
179+
if err := r.Next(values); err != nil {
180+
t.Fatalf("unexpected error from Next: %v", err)
181+
}
182+
if g, w := reflect.TypeOf(values[0]), reflect.TypeOf(&sppb.ResultSetMetadata{}); g != w {
183+
t.Fatalf("result set metadata type mismatch\n Got: %v\nWant: %v", g, w)
184+
}
185+
if g, w := r.Next(values), io.EOF; !errors.Is(g, w) {
186+
t.Fatalf("next result set mismatch\n Got: %v\nWant: %v", g, w)
187+
}
188+
189+
// The second result set should contain the actual data (which is empty).
190+
if !r.HasNextResultSet() {
191+
t.Fatalf("missing next result set")
192+
}
193+
if err := r.NextResultSet(); err != nil {
194+
t.Fatalf("unexpected error from NextResultSet: %v", err)
195+
}
196+
if g, w := r.Columns(), []string{"affected_rows"}; !cmp.Equal(g, w) {
197+
t.Fatalf("columns mismatch\n Got: %v\nWant: %v", g, w)
198+
}
199+
// There should be no data.
200+
if g, w := r.Next(values), io.EOF; !errors.Is(g, w) {
201+
t.Fatalf("next result set mismatch\n Got: %v\nWant: %v", g, w)
202+
}
203+
204+
// The third result set should contain ResultSetStats.
205+
if !r.HasNextResultSet() {
206+
t.Fatalf("missing next result set")
207+
}
208+
if err := r.NextResultSet(); err != nil {
209+
t.Fatalf("unexpected error from NextResultSet: %v", err)
210+
}
211+
if err := r.Next(values); err != nil {
212+
t.Fatalf("unexpected error from Next: %v", err)
213+
}
214+
if g, w := reflect.TypeOf(values[0]), reflect.TypeOf(&sppb.ResultSetStats{}); g != w {
215+
t.Fatalf("result set stats type mismatch\n Got: %v\nWant: %v", g, w)
216+
}
217+
if g, w := r.Next(values), io.EOF; !errors.Is(g, w) {
218+
t.Fatalf("next result set mismatch\n Got: %v\nWant: %v", g, w)
219+
}
220+
221+
// There should be no more result sets.
222+
if r.HasNextResultSet() {
223+
t.Fatalf("unexpected next result set available")
224+
}
225+
}

0 commit comments

Comments
 (0)