Skip to content

Commit 4cd9b07

Browse files
author
Maciej Zimnoch
committed
Support transaction isolation level in BeginTx
1 parent af2da39 commit 4cd9b07

File tree

5 files changed

+181
-4
lines changed

5 files changed

+181
-4
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Xiangyu Hu <xiangyu.hu at outlook.com>
6161
Xiaobing Jiang <s7v7nislands at gmail.com>
6262
Xiuming Chen <cc at cxm.cc>
6363
Zhenye Xie <xiezhenye at gmail.com>
64+
Maciej Zimnoch <maciej.zimnoch@codilime.com>
6465

6566
# Organizations
6667

connection_go18.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,23 @@ func (mc *mysqlConn) Ping(ctx context.Context) error {
4141

4242
// BeginTx implements driver.ConnBeginTx interface
4343
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
44-
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
45-
// TODO: support isolation levels
46-
return nil, errors.New("mysql: isolation levels not supported")
47-
}
4844
if opts.ReadOnly {
4945
// TODO: support read-only transactions
5046
return nil, errors.New("mysql: read-only transactions not supported")
5147
}
5248

49+
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
50+
level, err := mapIsolationLevel(opts.Isolation)
51+
if err != nil {
52+
return nil, err
53+
}
54+
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
55+
if err != nil {
56+
return nil, err
57+
}
58+
mc.finish()
59+
}
60+
5361
if err := mc.watchCancel(ctx); err != nil {
5462
return nil, err
5563
}

driver_go18_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"database/sql/driver"
1717
"fmt"
1818
"reflect"
19+
"sync"
1920
"testing"
2021
"time"
2122
)
@@ -468,3 +469,100 @@ func TestContextCancelBegin(t *testing.T) {
468469
}
469470
})
470471
}
472+
473+
func TestContextBeginIsolationLevel(t *testing.T) {
474+
runTests(t, dsn, func(dbt *DBTest) {
475+
dbt.mustExec("CREATE TABLE test (v INTEGER)")
476+
ctx, cancel := context.WithCancel(context.Background())
477+
defer cancel()
478+
479+
// Waitgroup syncing BeginTx
480+
beginWg := sync.WaitGroup{}
481+
beginWg.Add(2)
482+
483+
// Waitgroup syncing insert in writer transaction
484+
insertWg := sync.WaitGroup{}
485+
insertWg.Add(1)
486+
487+
// Waitgroup syncing writer transaction commit before reader reads
488+
readWg := sync.WaitGroup{}
489+
readWg.Add(1)
490+
491+
// Waitgroup syncing commit in writer transaction
492+
commitWg := sync.WaitGroup{}
493+
commitWg.Add(1)
494+
495+
// Waitgroup syncing end of both goroutines
496+
testDoneWg := sync.WaitGroup{}
497+
testDoneWg.Add(2)
498+
499+
repeatableReadGoroutine := func() {
500+
tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
501+
Isolation: sql.LevelRepeatableRead,
502+
})
503+
if err != nil {
504+
dbt.Fatal(err)
505+
}
506+
beginWg.Done()
507+
// Wait until other session will begin it's transaction
508+
beginWg.Wait()
509+
510+
_, err = tx.ExecContext(ctx, "INSERT INTO test VALUES (1)")
511+
if err != nil {
512+
dbt.Fatal(err)
513+
}
514+
insertWg.Done()
515+
516+
// Wait until reader transaction finish reading
517+
readWg.Wait()
518+
519+
err = tx.Commit()
520+
if err != nil {
521+
dbt.Fatal(err)
522+
}
523+
commitWg.Done()
524+
525+
testDoneWg.Done()
526+
}
527+
528+
readCommitedGoroutine := func() {
529+
tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
530+
Isolation: sql.LevelReadCommitted,
531+
})
532+
if err != nil {
533+
dbt.Fatal(err)
534+
}
535+
beginWg.Done()
536+
// Wait until writer transaction will begin
537+
beginWg.Wait()
538+
// Wait until writer transaction will insert value
539+
insertWg.Wait()
540+
var v int
541+
row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
542+
if err := row.Scan(&v); err != nil {
543+
dbt.Fatal(err)
544+
}
545+
// Because writer transaction wasn't commited yet, it should be available
546+
if v != 0 {
547+
dbt.Errorf("expected val to be 0, got %d", v)
548+
}
549+
readWg.Done()
550+
// Wait until writer transaction will commit
551+
commitWg.Wait()
552+
row = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
553+
if err := row.Scan(&v); err != nil {
554+
dbt.Fatal(err)
555+
}
556+
// Data written by writer transaction is already commited, it should be selectable
557+
if v != 1 {
558+
dbt.Errorf("expected val to be 1, got %d", v)
559+
}
560+
tx.Commit()
561+
testDoneWg.Done()
562+
}
563+
564+
go repeatableReadGoroutine()
565+
go readCommitedGoroutine()
566+
testDoneWg.Wait()
567+
})
568+
}

utils_go18.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package mysql
1212

1313
import (
1414
"crypto/tls"
15+
"database/sql"
1516
"database/sql/driver"
1617
"errors"
1718
)
@@ -31,3 +32,18 @@ func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
3132
}
3233
return dargs, nil
3334
}
35+
36+
func mapIsolationLevel(level driver.IsolationLevel) (string, error) {
37+
switch sql.IsolationLevel(level) {
38+
case sql.LevelRepeatableRead:
39+
return "REPEATABLE READ", nil
40+
case sql.LevelReadCommitted:
41+
return "READ COMMITTED", nil
42+
case sql.LevelReadUncommitted:
43+
return "READ UNCOMMITTED", nil
44+
case sql.LevelSerializable:
45+
return "SERIALIZABLE", nil
46+
default:
47+
return "", errors.New("mysql: unsupported isolation level: " + string(level))
48+
}
49+
}

utils_go18_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2+
//
3+
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
4+
//
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7+
// You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
// +build go1.8
10+
11+
package mysql
12+
13+
import (
14+
"database/sql"
15+
"database/sql/driver"
16+
"testing"
17+
)
18+
19+
func TestIsolationLevelMapping(t *testing.T) {
20+
21+
data := []struct {
22+
level driver.IsolationLevel
23+
expected string
24+
}{
25+
{
26+
level: driver.IsolationLevel(sql.LevelReadCommitted),
27+
expected: "READ COMMITTED",
28+
},
29+
{
30+
level: driver.IsolationLevel(sql.LevelRepeatableRead),
31+
expected: "REPEATABLE READ",
32+
},
33+
{
34+
level: driver.IsolationLevel(sql.LevelReadUncommitted),
35+
expected: "READ UNCOMMITTED",
36+
},
37+
{
38+
level: driver.IsolationLevel(sql.LevelSerializable),
39+
expected: "SERIALIZABLE",
40+
},
41+
}
42+
43+
for i, td := range data {
44+
if actual, err := mapIsolationLevel(td.level); actual != td.expected || err != nil {
45+
t.Fatal(i, td.expected, actual, err)
46+
}
47+
}
48+
49+
// check unsupported mapping
50+
if actual, err := mapIsolationLevel(driver.IsolationLevel(sql.LevelLinearizable)); actual != "" || err == nil {
51+
t.Fatal("Expected error on unsupported isolation level")
52+
}
53+
54+
}

0 commit comments

Comments
 (0)