Skip to content

Commit 6a396d3

Browse files
authored
feat: parse SET TRANSACTION statements (#549)
* feat: parse SET TRANSACTION statements Parse SET TRANSACTION statements and translate these to SET LOCAL statements. SET TRANSACTION may only be executed in a transaction block, and can only be used for a specific, limited set of connection properties. The syntax is specified by the SQL standard and PostgreSQL. See also https://www.postgresql.org/docs/current/sql-set-transaction.html This change only adds partial support. The following features will be added in future changes: 1. SET TRANSACTION READ {WRITE | ONLY} is not picked up by the driver, as the type of transaction is set directly when BeginTx is called. A refactor of this transaction handling is needed to be able to pick up SET TRANSACTION READ ONLY / SET TRANSACTION READ WRITE statements that are executed after BeginTx has been called. 2. PostgreSQL allows multiple transaction modes to be set in a single SET TRANSACTION statement. E.g. the following is allowed: SET TRANSACTION READ WRITE, ISOLATION LEVEL SERIALIZABLE The current implementation only supports one transaction mode per SET statement. * feat: support multiple transaction options in one statement
1 parent 8893903 commit 6a396d3

File tree

9 files changed

+522
-59
lines changed

9 files changed

+522
-59
lines changed

conn.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,18 @@ func (c *conn) showConnectionVariable(identifier parser.Identifier) (any, bool,
327327
return c.state.GetValue(extension, name)
328328
}
329329

330-
func (c *conn) setConnectionVariable(identifier parser.Identifier, value string, local bool) error {
330+
func (c *conn) setConnectionVariable(identifier parser.Identifier, value string, local bool, transaction bool) error {
331+
if transaction && !local {
332+
// When transaction == true, then local must also be true.
333+
// We should never hit this condition, as this is an indication of a bug in the driver code.
334+
return status.Errorf(codes.FailedPrecondition, "transaction properties must be set as a local value")
335+
}
331336
extension, name, err := toExtensionAndName(identifier)
332337
if err != nil {
333338
return err
334339
}
335340
if local {
336-
return c.state.SetLocalValue(extension, name, value)
341+
return c.state.SetLocalValue(extension, name, value, transaction)
337342
}
338343
return c.state.SetValue(extension, name, value, connectionstate.ContextUser)
339344
}
@@ -1188,6 +1193,8 @@ func (c *conn) beginTx(ctx context.Context, driverOpts driver.TxOptions, closeFu
11881193
}
11891194
}()
11901195

1196+
// TODO: Delay the actual determination of the transaction type until the first query.
1197+
// This is required in order to support SET TRANSACTION READ {ONLY | WRITE}
11911198
readOnlyTxOpts := c.getReadOnlyTransactionOptions()
11921199
batchReadOnlyTxOpts := c.getBatchReadOnlyTransactionOptions()
11931200
if c.inTransaction() {

connection_properties.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ var propertyDecodeToNativeArrays = createConnectionProperty(
228228
// Transaction connection properties.
229229
// ------------------------------------------------------------------------------------------------
230230

231+
var propertyTransactionReadOnly = createConnectionProperty(
232+
"transaction_read_only",
233+
"transaction_read_only is the default read-only mode for transactions on this connection.",
234+
false,
235+
false,
236+
nil,
237+
connectionstate.ContextUser,
238+
connectionstate.ConvertBool,
239+
)
240+
var propertyTransactionDeferrable = createConnectionProperty(
241+
"transaction_deferrable",
242+
"transaction_deferrable is a no-op on Spanner. It is defined in this driver for compatibility with PostgreSQL.",
243+
false,
244+
false,
245+
nil,
246+
connectionstate.ContextUser,
247+
connectionstate.ConvertBool,
248+
)
231249
var propertyExcludeTxnFromChangeStreams = createConnectionProperty(
232250
"exclude_txn_from_change_streams",
233251
"exclude_txn_from_change_streams determines whether transactions on this connection should be excluded from "+

connectionstate/connection_state.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ func (cs *ConnectionState) SetValue(extension, name, value string, context Conte
131131
return cs.setValue(extension, name, value, context, false)
132132
}
133133

134-
func (cs *ConnectionState) SetLocalValue(extension, name, value string) error {
134+
func (cs *ConnectionState) SetLocalValue(extension, name, value string, isSetTransaction bool) error {
135+
if isSetTransaction && !cs.inTransaction {
136+
return status.Error(codes.FailedPrecondition, "SET TRANSACTION can only be used in transaction blocks")
137+
}
135138
return cs.setValue(extension, name, value, ContextUser, true)
136139
}
137140

parser/simple_parser.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ func (p *simpleParser) eatKeywords(keywords []string) bool {
290290
return true
291291
}
292292

293+
// peekKeyword checks if the next keyword is the given keyword.
294+
// The position of the parser is not updated.
295+
func (p *simpleParser) peekKeyword(keyword string) bool {
296+
pos := p.pos
297+
defer func() {
298+
p.pos = pos
299+
}()
300+
return p.eatKeyword(keyword)
301+
}
302+
293303
// eatKeyword eats the given keyword at the current position of the parser if it exists
294304
// and returns true if the keyword was found. Otherwise, it returns false.
295305
func (p *simpleParser) eatKeyword(keyword string) bool {
@@ -323,8 +333,8 @@ func (p *simpleParser) readKeyword() string {
323333
if isSpace(p.sql[p.pos]) {
324334
break
325335
}
326-
// Only upper/lower-case letters are allowed in keywords.
327-
if !((p.sql[p.pos] >= 'A' && p.sql[p.pos] <= 'Z') || (p.sql[p.pos] >= 'a' && p.sql[p.pos] <= 'z')) {
336+
// Only upper/lower-case letters and underscores are allowed in keywords.
337+
if !((p.sql[p.pos] >= 'A' && p.sql[p.pos] <= 'Z') || (p.sql[p.pos] >= 'a' && p.sql[p.pos] <= 'z')) && p.sql[p.pos] != '_' {
328338
break
329339
}
330340
}

parser/statement_parser_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,10 @@ func TestReadKeyword(t *testing.T) {
21352135
input: "Select from my_table",
21362136
want: "Select",
21372137
},
2138+
{
2139+
input: "statement_tag",
2140+
want: "statement_tag",
2141+
},
21382142
}
21392143
statementParser, err := NewStatementParser(databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, 1000)
21402144
if err != nil {
@@ -2404,6 +2408,36 @@ func TestCachedParamsAreImmutable(t *testing.T) {
24042408
}
24052409
}
24062410

2411+
func TestPeekKeyword(t *testing.T) {
2412+
t.Parallel()
2413+
2414+
parser, err := NewStatementParser(databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, 1000)
2415+
if err != nil {
2416+
t.Fatal(err)
2417+
}
2418+
sp := &simpleParser{sql: []byte("select * from foo"), statementParser: parser}
2419+
if !sp.peekKeyword("select") {
2420+
t.Fatal("peekKeyword should have returned true")
2421+
}
2422+
if g, w := sp.pos, 0; g != w {
2423+
t.Fatalf("position mismatch\n Got: %v\nWant: %v", g, w)
2424+
}
2425+
2426+
if !sp.eatKeyword("select") {
2427+
t.Fatal("eatKeyword should have returned true")
2428+
}
2429+
if !sp.eatToken('*') {
2430+
t.Fatal("eatToken should have returned true")
2431+
}
2432+
pos := sp.pos
2433+
if !sp.peekKeyword("from") {
2434+
t.Fatal("peekKeyword should have returned true")
2435+
}
2436+
if g, w := sp.pos, pos; g != w {
2437+
t.Fatalf("position mismatch\n Got: %v\nWant: %v", g, w)
2438+
}
2439+
}
2440+
24072441
func TestEatKeyword(t *testing.T) {
24082442
t.Parallel()
24092443

parser/statements.go

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package parser
1616

1717
import (
18+
"fmt"
19+
1820
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
1921
"google.golang.org/grpc/codes"
2022
"google.golang.org/grpc/status"
@@ -142,11 +144,27 @@ func (s *ParsedShowStatement) parse(parser *StatementParser, query string) error
142144

143145
// ParsedSetStatement is a statement of the form
144146
// SET [SESSION | LOCAL] [my_extension.]my_property {=|to} <value>
147+
//
148+
// It also covers statements of the form SET TRANSACTION. This is a
149+
// synonym for SET LOCAL, but is only supported for a specific set of
150+
// properties, and may only be executed before a transaction has been
151+
// activated. Examples include:
152+
// SET TRANSACTION READ ONLY
153+
// SET TRANSACTION ISOLATION LEVEL [SERIALIZABLE | REPEATABLE READ]
154+
//
155+
// One SET statement can set more than one property.
145156
type ParsedSetStatement struct {
146-
query string
147-
Identifier Identifier
148-
Literal Literal
149-
IsLocal bool
157+
query string
158+
// Identifiers contains the properties that are being set. The number of elements in this slice
159+
// must be equal to the number of Literals.
160+
Identifiers []Identifier
161+
// Literals contains the values that should be set for the properties.
162+
Literals []Literal
163+
// IsLocal indicates whether this is a SET LOCAL statement or not.
164+
IsLocal bool
165+
// IsTransaction indicates whether this is a SET TRANSACTION statement or not.
166+
// IsTransaction automatically also implies IsLocal.
167+
IsTransaction bool
150168
}
151169

152170
func (s *ParsedSetStatement) Name() string {
@@ -165,10 +183,17 @@ func (s *ParsedSetStatement) parse(parser *StatementParser, query string) error
165183
return status.Errorf(codes.InvalidArgument, "syntax error: expected SET")
166184
}
167185
isLocal := sp.eatKeyword("LOCAL")
168-
if !isLocal && parser.Dialect == databasepb.DatabaseDialect_POSTGRESQL {
186+
isTransaction := false
187+
if !isLocal {
188+
isTransaction = sp.eatKeyword("TRANSACTION")
189+
}
190+
if !isLocal && !isTransaction && parser.Dialect == databasepb.DatabaseDialect_POSTGRESQL {
169191
// Just eat and ignore the SESSION keyword if it exists, as SESSION is the default.
170192
_ = sp.eatKeyword("SESSION")
171193
}
194+
if isTransaction {
195+
return s.parseSetTransaction(sp, query)
196+
}
172197
identifier, err := sp.eatIdentifier()
173198
if err != nil {
174199
return err
@@ -191,12 +216,93 @@ func (s *ParsedSetStatement) parse(parser *StatementParser, query string) error
191216
return status.Errorf(codes.InvalidArgument, "unexpected tokens at position %d in %q", sp.pos, sp.sql)
192217
}
193218
s.query = query
194-
s.Identifier = identifier
195-
s.Literal = literalValue
219+
s.Identifiers = []Identifier{identifier}
220+
s.Literals = []Literal{literalValue}
196221
s.IsLocal = isLocal
197222
return nil
198223
}
199224

225+
func (s *ParsedSetStatement) parseSetTransaction(sp *simpleParser, query string) error {
226+
if !sp.hasMoreTokens() {
227+
return status.Errorf(codes.InvalidArgument, "syntax error: missing TRANSACTION OPTION, expected one of ISOLATION LEVEL, READ WRITE, or READ ONLY")
228+
}
229+
s.query = query
230+
s.IsLocal = true
231+
s.IsTransaction = true
232+
233+
for {
234+
if sp.peekKeyword("ISOLATION") {
235+
if err := s.parseSetTransactionIsolationLevel(sp, query); err != nil {
236+
return err
237+
}
238+
} else if sp.peekKeyword("READ") {
239+
if err := s.parseSetTransactionMode(sp, query); err != nil {
240+
return err
241+
}
242+
} else if sp.statementParser.Dialect == databasepb.DatabaseDialect_POSTGRESQL && (sp.peekKeyword("DEFERRABLE") || sp.peekKeyword("NOT")) {
243+
// https://www.postgresql.org/docs/current/sql-set-transaction.html
244+
if err := s.parseSetTransactionDeferrable(sp, query); err != nil {
245+
return err
246+
}
247+
} else {
248+
return status.Error(codes.InvalidArgument, "invalid TRANSACTION option, expected one of ISOLATION LEVEL, READ WRITE, or READ ONLY")
249+
}
250+
if !sp.hasMoreTokens() {
251+
return nil
252+
}
253+
// Eat and ignore any commas separating the various options.
254+
sp.eatToken(',')
255+
}
256+
}
257+
258+
func (s *ParsedSetStatement) parseSetTransactionIsolationLevel(sp *simpleParser, query string) error {
259+
if !sp.eatKeywords([]string{"ISOLATION", "LEVEL"}) {
260+
return status.Errorf(codes.InvalidArgument, "syntax error: expected ISOLATION LEVEL")
261+
}
262+
var value Literal
263+
if sp.eatKeyword("SERIALIZABLE") {
264+
value = Literal{Value: "serializable"}
265+
} else if sp.eatKeywords([]string{"REPEATABLE", "READ"}) {
266+
value = Literal{Value: "repeatable_read"}
267+
} else {
268+
return status.Errorf(codes.InvalidArgument, "syntax error: expected SERIALIZABLE OR REPETABLE READ")
269+
}
270+
271+
s.Identifiers = append(s.Identifiers, Identifier{Parts: []string{"isolation_level"}})
272+
s.Literals = append(s.Literals, value)
273+
return nil
274+
}
275+
276+
func (s *ParsedSetStatement) parseSetTransactionMode(sp *simpleParser, query string) error {
277+
readOnly := false
278+
if sp.eatKeywords([]string{"READ", "ONLY"}) {
279+
readOnly = true
280+
} else if sp.eatKeywords([]string{"READ", "WRITE"}) {
281+
readOnly = false
282+
} else {
283+
return status.Errorf(codes.InvalidArgument, "syntax error: expected READ ONLY or READ WRITE")
284+
}
285+
286+
s.Identifiers = append(s.Identifiers, Identifier{Parts: []string{"transaction_read_only"}})
287+
s.Literals = append(s.Literals, Literal{Value: fmt.Sprintf("%v", readOnly)})
288+
return nil
289+
}
290+
291+
func (s *ParsedSetStatement) parseSetTransactionDeferrable(sp *simpleParser, query string) error {
292+
deferrable := false
293+
if sp.eatKeywords([]string{"NOT", "DEFERRABLE"}) {
294+
deferrable = false
295+
} else if sp.eatKeyword("DEFERRABLE") {
296+
deferrable = true
297+
} else {
298+
return status.Errorf(codes.InvalidArgument, "syntax error: expected [NOT] DEFERRABLE")
299+
}
300+
301+
s.Identifiers = append(s.Identifiers, Identifier{Parts: []string{"transaction_deferrable"}})
302+
s.Literals = append(s.Literals, Literal{Value: fmt.Sprintf("%v", deferrable)})
303+
return nil
304+
}
305+
200306
// ParsedResetStatement is a statement of the form
201307
// RESET [my_extension.]my_property
202308
type ParsedResetStatement struct {
@@ -404,6 +510,7 @@ func (s *ParsedBeginStatement) parse(parser *StatementParser, query string) erro
404510
// Parse a statement of the form
405511
// GoogleSQL: BEGIN [TRANSACTION]
406512
// PostgreSQL: {START | BEGIN} [{TRANSACTION | WORK}] (https://www.postgresql.org/docs/current/sql-begin.html)
513+
// TODO: Support transaction modes in the BEGIN / START statement.
407514
sp := &simpleParser{sql: []byte(query), statementParser: parser}
408515
if sp.statementParser.Dialect == databasepb.DatabaseDialect_POSTGRESQL {
409516
if !sp.eatKeyword("START") && !sp.eatKeyword("BEGIN") {

0 commit comments

Comments
 (0)