Skip to content

Commit 1622267

Browse files
authored
chore: allow query parameters starting with underscore (#516)
The database/sql package does not allow query parameter names to start with an underscore. Spanner however allows this. Query parameters that start with an underscore can be wrapped in a SpannerNamedArg to work around this limitation. This feature is primarily intended for use with other programming languages than Go, as frameworks written in Go will know about the limitation in database/sql that query parameters may not start with an underscore.
1 parent aa00055 commit 1622267

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

driver_with_mockserver_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,39 @@ func TestQueryWithDuplicateNamedParameter(t *testing.T) {
20992099
}
21002100
}
21012101

2102+
func TestQueryWithDuplicateNamedParameterStartingWithUnderscore(t *testing.T) {
2103+
t.Parallel()
2104+
2105+
db, server, teardown := setupTestDBConnection(t)
2106+
defer teardown()
2107+
2108+
// database/sql does not allow named arguments to start with an underscore.
2109+
// The Spanner database/sql driver allows a workaround for this by specifying those named arguments with a
2110+
// SpannerNamedArg.
2111+
s := "insert into users (id, name) values (@__name, @__name)"
2112+
_ = server.TestSpanner.PutStatementResult(s, &testutil.StatementResult{
2113+
Type: testutil.StatementResultUpdateCount,
2114+
UpdateCount: 1,
2115+
})
2116+
_, err := db.Exec(s, sql.Named("p__name", SpannerNamedArg{NameInQuery: "__name", Value: "foo"}), sql.Named("p__name", SpannerNamedArg{NameInQuery: "__name", Value: "bar"}))
2117+
if err != nil {
2118+
t.Fatal(err)
2119+
}
2120+
// Verify that 'bar' is used for both instances of the parameter @__name.
2121+
requests := drainRequestsFromServer(server.TestSpanner)
2122+
sqlRequests := requestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{}))
2123+
if len(sqlRequests) != 1 {
2124+
t.Fatalf("sql requests count mismatch\nGot: %v\nWant: %v", len(sqlRequests), 1)
2125+
}
2126+
req := sqlRequests[0].(*sppb.ExecuteSqlRequest)
2127+
if g, w := len(req.Params.Fields), 1; g != w {
2128+
t.Fatalf("params count mismatch\n Got: %v\nWant: %v", g, w)
2129+
}
2130+
if g, w := req.Params.Fields["__name"].GetStringValue(), "bar"; g != w {
2131+
t.Fatalf("param value mismatch\n Got: %v\nWant: %v", g, w)
2132+
}
2133+
}
2134+
21022135
func TestQueryWithReusedNamedParameter(t *testing.T) {
21032136
t.Parallel()
21042137

@@ -2129,6 +2162,36 @@ func TestQueryWithReusedNamedParameter(t *testing.T) {
21292162
}
21302163
}
21312164

2165+
func TestQueryWithReusedNamedParameterStartingWithUnderscore(t *testing.T) {
2166+
t.Parallel()
2167+
2168+
db, server, teardown := setupTestDBConnection(t)
2169+
defer teardown()
2170+
2171+
s := "insert into users (id, name) values (@__name, @__name)"
2172+
_ = server.TestSpanner.PutStatementResult(s, &testutil.StatementResult{
2173+
Type: testutil.StatementResultUpdateCount,
2174+
UpdateCount: 1,
2175+
})
2176+
_, err := db.Exec(s, sql.Named("p__name", SpannerNamedArg{NameInQuery: "__name", Value: "foo"}))
2177+
if err != nil {
2178+
t.Fatal(err)
2179+
}
2180+
// Verify that 'foo' is used for both instances of the parameter @__name.
2181+
requests := drainRequestsFromServer(server.TestSpanner)
2182+
sqlRequests := requestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{}))
2183+
if len(sqlRequests) != 1 {
2184+
t.Fatalf("sql requests count mismatch\nGot: %v\nWant: %v", len(sqlRequests), 1)
2185+
}
2186+
req := sqlRequests[0].(*sppb.ExecuteSqlRequest)
2187+
if g, w := len(req.Params.Fields), 1; g != w {
2188+
t.Fatalf("params count mismatch\n Got: %v\nWant: %v", g, w)
2189+
}
2190+
if g, w := req.Params.Fields["__name"].GetStringValue(), "foo"; g != w {
2191+
t.Fatalf("param value mismatch\n Got: %v\nWant: %v", g, w)
2192+
}
2193+
}
2194+
21322195
func TestQueryWithReusedPositionalParameter(t *testing.T) {
21332196
t.Parallel()
21342197

stmt.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ import (
2424
"google.golang.org/grpc/status"
2525
)
2626

27+
// SpannerNamedArg can be used for query parameters with a name that (might) start
28+
// with an underscore. The generic database/sql package does not allow query parameters
29+
// to start with an underscore, but Spanner allows this, and this struct can be used to
30+
// work around the limitation in database/sql.
31+
type SpannerNamedArg struct {
32+
NameInQuery string
33+
Value any
34+
}
35+
2736
var _ driver.Stmt = &stmt{}
2837
var _ driver.StmtExecContext = &stmt{}
2938
var _ driver.StmtQueryContext = &stmt{}
@@ -80,12 +89,17 @@ func prepareSpannerStmt(parser *parser.StatementParser, q string, args []driver.
8089
}
8190
ss := spanner.NewStatement(q)
8291
for i, v := range args {
92+
value := v.Value
8393
name := args[i].Name
94+
if sa, ok := args[i].Value.(SpannerNamedArg); ok {
95+
name = sa.NameInQuery
96+
value = sa.Value
97+
}
8498
if name == "" && len(names) > i {
8599
name = names[i]
86100
}
87101
if name != "" {
88-
ss.Params[name] = convertParam(v.Value)
102+
ss.Params[name] = convertParam(value)
89103
}
90104
}
91105
// Verify that all parameters have a value.

0 commit comments

Comments
 (0)