Skip to content

Commit 687e912

Browse files
authored
Add transaction-level isolation level setting (#206)
Enables optionally setting the isolation level at a transaction-level to either `SERIALIZABLE` or `REPEATABLE READ`. If the isolation level is not set, Spanner will default to `SERIALIZABLE` isolation.
1 parent 9ff408f commit 687e912

5 files changed

Lines changed: 164 additions & 11 deletions

File tree

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ and `{}` for a mutually exclusive keyword.
237237
| Show Query Result Shape | `DESCRIBE SELECT ...;` | |
238238
| Show DML Result Shape | `DESCRIBE {INSERT\|UPDATE\|DELETE} ... THEN RETURN ...;` | |
239239
| Start a new query optimizer statistics package construction | `ANALYZE;` | |
240-
| Start Read-Write Transaction | `BEGIN [RW] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG <tag>];` | See [Request Priority](#request-priority) for details on the priority. The tag you set is used as both transaction tag and request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).|
240+
| Start Read-Write Transaction | `BEGIN [RW] [ISOLATION LEVEL {SERIALIZABLE\|REPEATABLE READ}] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG <tag>];` | See [Isolation Level](#isolation-level) for details on the isolation level. See [Request Priority](#request-priority) for details on the priority. The tag you set is used as both transaction tag and request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).|
241241
| Commit Read-Write Transaction | `COMMIT;` | |
242242
| Rollback Read-Write Transaction | `ROLLBACK;` | |
243243
| Start Read-Only Transaction | `BEGIN RO [{<seconds>\|<RFC3339-formatted time>}] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG <tag>];` | `<seconds>` and `<RFC3339-formatted time>` is used for stale read. See [Request Priority](#request-priority) for details on the priority. The tag you set is used as request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).|
@@ -300,14 +300,32 @@ prompt = "[\\p:\\i:\\d]\\t> "
300300
3. `.spanner_cli.cnf` in current directory
301301
4. `.spanner_cli.cnf` in home directory(lowest)
302302

303+
## Isolation Level
304+
305+
You can set the isolation level at a transaction level in read-write transactions. By default `SERIALIZABLE` isolation is used for every request.
306+
307+
To set the isolation level for a transaction, you can use the `ISOLATION LEVEL {SERIALIZABLE|REPEATABLE READ}` keyword.
308+
309+
Here are some examples for transaction-level isolation.
310+
311+
```
312+
# Read-write transaction with serializable isolation
313+
BEGIN RW ISOLATION LEVEL SERIALIZABLE
314+
315+
# Read-write transaction with repeatable read isolation
316+
BEGIN RW ISOLATION LEVEL REPEATABLE READ
317+
```
318+
319+
Note that the transaction-level isolation level cannot be set on read-only transactions.
320+
303321
## Request Priority
304322

305323
You can set [request priority](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions#Priority) for command level or transaction level.
306324
By default `MEDIUM` priority is used for every request.
307325

308326
To set a priority for command line level, you can use `--priority={HIGH|MEDIUM|LOW}` command line option.
309327

310-
To set a priority for transaction level, you can use `PRIORITY {HIGH|MEDIUM|LOW}` keyword.
328+
To set a priority for transaction level, you can use the `PRIORITY {HIGH|MEDIUM|LOW}` keyword.
311329

312330
Here are some examples for transaction-level priority.
313331

session.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (s *Session) InReadOnlyTransaction() bool {
119119
}
120120

121121
// BeginReadWriteTransaction starts read-write transaction.
122-
func (s *Session) BeginReadWriteTransaction(ctx context.Context, priority pb.RequestOptions_Priority, tag string) error {
122+
func (s *Session) BeginReadWriteTransaction(ctx context.Context, isolation_level pb.TransactionOptions_IsolationLevel, priority pb.RequestOptions_Priority, tag string) error {
123123
if s.InReadWriteTransaction() {
124124
return errors.New("read-write transaction is already running")
125125
}
@@ -132,6 +132,7 @@ func (s *Session) BeginReadWriteTransaction(ctx context.Context, priority pb.Req
132132
opts := spanner.TransactionOptions{
133133
CommitOptions: spanner.CommitOptions{ReturnCommitStats: true},
134134
CommitPriority: priority,
135+
IsolationLevel: isolation_level,
135136
TransactionTag: tag,
136137
}
137138
txn, err := spanner.NewReadWriteStmtBasedTransactionWithOptions(ctx, s.client, opts)

session_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestRequestPriority(t *testing.T) {
7272
}
7373

7474
// Read-Write Transaction.
75-
if err := session.BeginReadWriteTransaction(ctx, test.transactionPriority, ""); err != nil {
75+
if err := session.BeginReadWriteTransaction(ctx, pb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED, test.transactionPriority, ""); err != nil {
7676
t.Fatalf("failed to begin read write transaction: %v", err)
7777
}
7878
iter, _ := session.RunQuery(ctx, spanner.NewStatement("SELECT * FROM t1"))
@@ -191,6 +191,81 @@ func TestParseDirectedReadOption(t *testing.T) {
191191
}
192192
}
193193

194+
func TestIsolationLevel(t *testing.T) {
195+
server := setupTestServer(t)
196+
197+
var recorder requestRecorder
198+
unaryInterceptor, streamInterceptor := recordRequestsInterceptors(&recorder)
199+
opts := []grpc.DialOption{
200+
grpc.WithInsecure(),
201+
grpc.WithUnaryInterceptor(unaryInterceptor),
202+
grpc.WithStreamInterceptor(streamInterceptor),
203+
}
204+
ctx := context.Background()
205+
conn, err := grpc.DialContext(ctx, server.Addr, opts...)
206+
if err != nil {
207+
t.Fatalf("failed to dial: %v", err)
208+
}
209+
210+
for _, test := range []struct {
211+
desc string
212+
isolationLevel pb.TransactionOptions_IsolationLevel
213+
want pb.TransactionOptions_IsolationLevel
214+
}{
215+
{
216+
desc: "use default unspecified isolation level",
217+
isolationLevel: pb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED,
218+
want: pb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED,
219+
},
220+
{
221+
desc: "use SERIALIZABLE isolation level",
222+
isolationLevel: pb.TransactionOptions_SERIALIZABLE,
223+
want: pb.TransactionOptions_SERIALIZABLE,
224+
},
225+
{
226+
desc: "use REPEATABLE READ isolation level",
227+
isolationLevel: pb.TransactionOptions_REPEATABLE_READ,
228+
want: pb.TransactionOptions_REPEATABLE_READ,
229+
},
230+
} {
231+
t.Run(test.desc, func(t *testing.T) {
232+
defer recorder.flush()
233+
234+
session, err := NewSession("project", "instance", "database", pb.RequestOptions_PRIORITY_UNSPECIFIED, "role", nil, nil, option.WithGRPCConn(conn))
235+
if err != nil {
236+
t.Fatalf("failed to create spanner-cli session: %v", err)
237+
}
238+
239+
// Read-Write Transaction.
240+
if err := session.BeginReadWriteTransaction(ctx, test.isolationLevel, pb.RequestOptions_PRIORITY_UNSPECIFIED, ""); err != nil {
241+
t.Fatalf("failed to begin read write transaction: %v", err)
242+
}
243+
iter, _ := session.RunQuery(ctx, spanner.NewStatement("SELECT * FROM t1"))
244+
if err := iter.Do(func(r *spanner.Row) error {
245+
return nil
246+
}); err != nil {
247+
t.Fatalf("failed to run query: %v", err)
248+
}
249+
if _, _, _, _, err := session.RunUpdate(ctx, spanner.NewStatement("DELETE FROM t1 WHERE Id = 1"), true); err != nil {
250+
t.Fatalf("failed to run update: %v", err)
251+
}
252+
if _, err := session.CommitReadWriteTransaction(ctx); err != nil {
253+
t.Fatalf("failed to commit: %v", err)
254+
}
255+
256+
// Check request priority.
257+
for _, r := range recorder.requests {
258+
switch v := r.(type) {
259+
case *pb.BeginTransactionRequest:
260+
if got := v.GetOptions().GetIsolationLevel(); got != test.want {
261+
t.Errorf("isolation level mismatch: got = %v, want = %v", got, test.want)
262+
}
263+
}
264+
}
265+
})
266+
}
267+
}
268+
194269
func setupTestServer(t *testing.T) *spannertest.Server {
195270
server, err := spannertest.NewServer("localhost:0")
196271
if err != nil {

statement.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ var (
110110
pdmlRe = regexp.MustCompile(`(?is)^PARTITIONED\s+((?:INSERT|UPDATE|DELETE)\s+.+$)`)
111111

112112
// Transaction
113-
beginRwRe = regexp.MustCompile(`(?is)^BEGIN(?:\s+RW)?(?:\s+PRIORITY\s+(HIGH|MEDIUM|LOW))?(?:\s+TAG\s+(.+))?$`)
113+
beginRwRe = regexp.MustCompile(`(?is)^BEGIN(?:\s+RW)?(?:\s+ISOLATION LEVEL\s+(SERIALIZABLE|REPEATABLE READ))?(?:\s+PRIORITY\s+(HIGH|MEDIUM|LOW))?(?:\s+TAG\s+(.+))?$`)
114114
beginRoRe = regexp.MustCompile(`(?is)^BEGIN\s+RO(?:\s+([^\s]+))?(?:\s+PRIORITY\s+(HIGH|MEDIUM|LOW))?(?:\s+TAG\s+(.+))?$`)
115115
commitRe = regexp.MustCompile(`(?is)^COMMIT$`)
116116
rollbackRe = regexp.MustCompile(`(?is)^ROLLBACK$`)
@@ -991,24 +991,33 @@ func runInNewOrExistRwTxForExplain(ctx context.Context, session *Session, f func
991991
}
992992

993993
type BeginRwStatement struct {
994-
Priority pb.RequestOptions_Priority
995-
Tag string
994+
IsolationLevel pb.TransactionOptions_IsolationLevel
995+
Priority pb.RequestOptions_Priority
996+
Tag string
996997
}
997998

998999
func newBeginRwStatement(input string) (*BeginRwStatement, error) {
9991000
matched := beginRwRe.FindStringSubmatch(input)
10001001
stmt := &BeginRwStatement{}
10011002

10021003
if matched[1] != "" {
1003-
priority, err := parsePriority(matched[1])
1004+
isolationLevel, err := parseIsolationLevel(matched[1])
10041005
if err != nil {
10051006
return nil, err
10061007
}
1007-
stmt.Priority = priority
1008+
stmt.IsolationLevel = isolationLevel
10081009
}
10091010

10101011
if matched[2] != "" {
1011-
stmt.Tag = matched[2]
1012+
priority, err := parsePriority(matched[2])
1013+
if err != nil {
1014+
return nil, err
1015+
}
1016+
stmt.Priority = priority
1017+
}
1018+
1019+
if matched[3] != "" {
1020+
stmt.Tag = matched[3]
10121021
}
10131022

10141023
return stmt, nil
@@ -1022,7 +1031,7 @@ func (s *BeginRwStatement) Execute(ctx context.Context, session *Session) (*Resu
10221031
return nil, errors.New("you're in read-only transaction. Please finish the transaction by 'CLOSE;'")
10231032
}
10241033

1025-
if err := session.BeginReadWriteTransaction(ctx, s.Priority, s.Tag); err != nil {
1034+
if err := session.BeginReadWriteTransaction(ctx, s.IsolationLevel, s.Priority, s.Tag); err != nil {
10261035
return nil, err
10271036
}
10281037

@@ -1190,3 +1199,14 @@ func parsePriority(priority string) (pb.RequestOptions_Priority, error) {
11901199
return pb.RequestOptions_PRIORITY_UNSPECIFIED, fmt.Errorf("invalid priority: %q", priority)
11911200
}
11921201
}
1202+
1203+
func parseIsolationLevel(isolationLevel string) (pb.TransactionOptions_IsolationLevel, error) {
1204+
switch strings.ToUpper(isolationLevel) {
1205+
case "SERIALIZABLE":
1206+
return pb.TransactionOptions_SERIALIZABLE, nil
1207+
case "REPEATABLE READ":
1208+
return pb.TransactionOptions_REPEATABLE_READ, nil
1209+
default:
1210+
return pb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED, fmt.Errorf("invalid isolation level: %q", isolationLevel)
1211+
}
1212+
}

statement_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,45 @@ func TestBuildStatement(t *testing.T) {
321321
Tag: "app=spanner-cli env=test",
322322
},
323323
},
324+
{
325+
desc: "BEGIN RW statement with ISOLATION LEVEL",
326+
input: "BEGIN RW ISOLATION LEVEL SERIALIZABLE",
327+
want: &BeginRwStatement{
328+
IsolationLevel: pb.TransactionOptions_SERIALIZABLE,
329+
},
330+
},
331+
{
332+
desc: "BEGIN RW statement with ISOLATION LEVEL",
333+
input: "BEGIN RW ISOLATION LEVEL REPEATABLE READ",
334+
want: &BeginRwStatement{
335+
IsolationLevel: pb.TransactionOptions_REPEATABLE_READ,
336+
},
337+
},
338+
{
339+
desc: "BEGIN RW statement with ISOLATION LEVEL and PRIORITY",
340+
input: "BEGIN RW ISOLATION LEVEL SERIALIZABLE PRIORITY MEDIUM",
341+
want: &BeginRwStatement{
342+
IsolationLevel: pb.TransactionOptions_SERIALIZABLE,
343+
Priority: pb.RequestOptions_PRIORITY_MEDIUM,
344+
},
345+
},
346+
{
347+
desc: "BEGIN RW statement with ISOLATION LEVEL and TAG",
348+
input: "BEGIN RW ISOLATION LEVEL SERIALIZABLE TAG app=spanner-cli",
349+
want: &BeginRwStatement{
350+
IsolationLevel: pb.TransactionOptions_SERIALIZABLE,
351+
Tag: "app=spanner-cli",
352+
},
353+
},
354+
{
355+
desc: "BEGIN RW statement with ISOLATION LEVEL, PRIORITY and TAG",
356+
input: "BEGIN RW ISOLATION LEVEL REPEATABLE READ PRIORITY MEDIUM TAG app=spanner-cli",
357+
want: &BeginRwStatement{
358+
IsolationLevel: pb.TransactionOptions_REPEATABLE_READ,
359+
Priority: pb.RequestOptions_PRIORITY_MEDIUM,
360+
Tag: "app=spanner-cli",
361+
},
362+
},
324363
{
325364
desc: "BEGIN PRIORITY statement with TAG whitespace",
326365
input: "BEGIN PRIORITY MEDIUM TAG app=spanner-cli env=test",

0 commit comments

Comments
 (0)