From af2da39b7d348da2dafe5156ce74484faf4dedb3 Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Tue, 13 Jun 2017 09:48:32 -0700 Subject: [PATCH 1/6] Added Gogland IDE internal directory to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ba8e0cb3a..2de28da16 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Icon? ehthumbs.db Thumbs.db +.idea From 4cd9b07f8f31ec005988cd3a64797c7ea9cdbede Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Tue, 13 Jun 2017 12:56:21 -0700 Subject: [PATCH 2/6] Support transaction isolation level in BeginTx --- AUTHORS | 1 + connection_go18.go | 16 ++++++-- driver_go18_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++ utils_go18.go | 16 ++++++++ utils_go18_test.go | 54 +++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 utils_go18_test.go diff --git a/AUTHORS b/AUTHORS index 58c749e29..45efbae09 100644 --- a/AUTHORS +++ b/AUTHORS @@ -61,6 +61,7 @@ Xiangyu Hu Xiaobing Jiang Xiuming Chen Zhenye Xie +Maciej Zimnoch # Organizations diff --git a/connection_go18.go b/connection_go18.go index c4ca4c33f..80c6f3cd1 100644 --- a/connection_go18.go +++ b/connection_go18.go @@ -41,15 +41,23 @@ func (mc *mysqlConn) Ping(ctx context.Context) error { // BeginTx implements driver.ConnBeginTx interface func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { - if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault { - // TODO: support isolation levels - return nil, errors.New("mysql: isolation levels not supported") - } if opts.ReadOnly { // TODO: support read-only transactions return nil, errors.New("mysql: read-only transactions not supported") } + if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault { + level, err := mapIsolationLevel(opts.Isolation) + if err != nil { + return nil, err + } + err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level) + if err != nil { + return nil, err + } + mc.finish() + } + if err := mc.watchCancel(ctx); err != nil { return nil, err } diff --git a/driver_go18_test.go b/driver_go18_test.go index 69d0a2b7d..306963d83 100644 --- a/driver_go18_test.go +++ b/driver_go18_test.go @@ -16,6 +16,7 @@ import ( "database/sql/driver" "fmt" "reflect" + "sync" "testing" "time" ) @@ -468,3 +469,100 @@ func TestContextCancelBegin(t *testing.T) { } }) } + +func TestContextBeginIsolationLevel(t *testing.T) { + runTests(t, dsn, func(dbt *DBTest) { + dbt.mustExec("CREATE TABLE test (v INTEGER)") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Waitgroup syncing BeginTx + beginWg := sync.WaitGroup{} + beginWg.Add(2) + + // Waitgroup syncing insert in writer transaction + insertWg := sync.WaitGroup{} + insertWg.Add(1) + + // Waitgroup syncing writer transaction commit before reader reads + readWg := sync.WaitGroup{} + readWg.Add(1) + + // Waitgroup syncing commit in writer transaction + commitWg := sync.WaitGroup{} + commitWg.Add(1) + + // Waitgroup syncing end of both goroutines + testDoneWg := sync.WaitGroup{} + testDoneWg.Add(2) + + repeatableReadGoroutine := func() { + tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + }) + if err != nil { + dbt.Fatal(err) + } + beginWg.Done() + // Wait until other session will begin it's transaction + beginWg.Wait() + + _, err = tx.ExecContext(ctx, "INSERT INTO test VALUES (1)") + if err != nil { + dbt.Fatal(err) + } + insertWg.Done() + + // Wait until reader transaction finish reading + readWg.Wait() + + err = tx.Commit() + if err != nil { + dbt.Fatal(err) + } + commitWg.Done() + + testDoneWg.Done() + } + + readCommitedGoroutine := func() { + tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + }) + if err != nil { + dbt.Fatal(err) + } + beginWg.Done() + // Wait until writer transaction will begin + beginWg.Wait() + // Wait until writer transaction will insert value + insertWg.Wait() + var v int + row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") + if err := row.Scan(&v); err != nil { + dbt.Fatal(err) + } + // Because writer transaction wasn't commited yet, it should be available + if v != 0 { + dbt.Errorf("expected val to be 0, got %d", v) + } + readWg.Done() + // Wait until writer transaction will commit + commitWg.Wait() + row = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") + if err := row.Scan(&v); err != nil { + dbt.Fatal(err) + } + // Data written by writer transaction is already commited, it should be selectable + if v != 1 { + dbt.Errorf("expected val to be 1, got %d", v) + } + tx.Commit() + testDoneWg.Done() + } + + go repeatableReadGoroutine() + go readCommitedGoroutine() + testDoneWg.Wait() + }) +} diff --git a/utils_go18.go b/utils_go18.go index eaeac4f84..7d8c9b16e 100644 --- a/utils_go18.go +++ b/utils_go18.go @@ -12,6 +12,7 @@ package mysql import ( "crypto/tls" + "database/sql" "database/sql/driver" "errors" ) @@ -31,3 +32,18 @@ func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) { } return dargs, nil } + +func mapIsolationLevel(level driver.IsolationLevel) (string, error) { + switch sql.IsolationLevel(level) { + case sql.LevelRepeatableRead: + return "REPEATABLE READ", nil + case sql.LevelReadCommitted: + return "READ COMMITTED", nil + case sql.LevelReadUncommitted: + return "READ UNCOMMITTED", nil + case sql.LevelSerializable: + return "SERIALIZABLE", nil + default: + return "", errors.New("mysql: unsupported isolation level: " + string(level)) + } +} diff --git a/utils_go18_test.go b/utils_go18_test.go new file mode 100644 index 000000000..856c25f56 --- /dev/null +++ b/utils_go18_test.go @@ -0,0 +1,54 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +// +build go1.8 + +package mysql + +import ( + "database/sql" + "database/sql/driver" + "testing" +) + +func TestIsolationLevelMapping(t *testing.T) { + + data := []struct { + level driver.IsolationLevel + expected string + }{ + { + level: driver.IsolationLevel(sql.LevelReadCommitted), + expected: "READ COMMITTED", + }, + { + level: driver.IsolationLevel(sql.LevelRepeatableRead), + expected: "REPEATABLE READ", + }, + { + level: driver.IsolationLevel(sql.LevelReadUncommitted), + expected: "READ UNCOMMITTED", + }, + { + level: driver.IsolationLevel(sql.LevelSerializable), + expected: "SERIALIZABLE", + }, + } + + for i, td := range data { + if actual, err := mapIsolationLevel(td.level); actual != td.expected || err != nil { + t.Fatal(i, td.expected, actual, err) + } + } + + // check unsupported mapping + if actual, err := mapIsolationLevel(driver.IsolationLevel(sql.LevelLinearizable)); actual != "" || err == nil { + t.Fatal("Expected error on unsupported isolation level") + } + +} From 0d8f878f988d0c99a59dc816ebee22fa64385b94 Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Tue, 13 Jun 2017 20:11:20 -0700 Subject: [PATCH 3/6] Review fixes --- connection_go18.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/connection_go18.go b/connection_go18.go index 80c6f3cd1..5053497d0 100644 --- a/connection_go18.go +++ b/connection_go18.go @@ -46,20 +46,21 @@ func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver return nil, errors.New("mysql: read-only transactions not supported") } + if err := mc.watchCancel(ctx); err != nil { + return nil, err + } + if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault { level, err := mapIsolationLevel(opts.Isolation) if err != nil { + mc.finish() return nil, err } err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level) if err != nil { + mc.finish() return nil, err } - mc.finish() - } - - if err := mc.watchCancel(ctx); err != nil { - return nil, err } tx, err := mc.Begin() From 74b7dc84dfda8162c22c4fa507e86e2a845b9f2e Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Wed, 14 Jun 2017 09:02:17 -0700 Subject: [PATCH 4/6] Simplyfied TestContextBeginIsolationLevel test --- driver_go18_test.go | 118 ++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 82 deletions(-) diff --git a/driver_go18_test.go b/driver_go18_test.go index 306963d83..f2184add0 100644 --- a/driver_go18_test.go +++ b/driver_go18_test.go @@ -16,7 +16,6 @@ import ( "database/sql/driver" "fmt" "reflect" - "sync" "testing" "time" ) @@ -476,93 +475,48 @@ func TestContextBeginIsolationLevel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // Waitgroup syncing BeginTx - beginWg := sync.WaitGroup{} - beginWg.Add(2) - - // Waitgroup syncing insert in writer transaction - insertWg := sync.WaitGroup{} - insertWg.Add(1) - - // Waitgroup syncing writer transaction commit before reader reads - readWg := sync.WaitGroup{} - readWg.Add(1) - - // Waitgroup syncing commit in writer transaction - commitWg := sync.WaitGroup{} - commitWg.Add(1) - - // Waitgroup syncing end of both goroutines - testDoneWg := sync.WaitGroup{} - testDoneWg.Add(2) - - repeatableReadGoroutine := func() { - tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ - Isolation: sql.LevelRepeatableRead, - }) - if err != nil { - dbt.Fatal(err) - } - beginWg.Done() - // Wait until other session will begin it's transaction - beginWg.Wait() - - _, err = tx.ExecContext(ctx, "INSERT INTO test VALUES (1)") - if err != nil { - dbt.Fatal(err) - } - insertWg.Done() + tx1, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + }) + if err != nil { + dbt.Fatal(err) + } - // Wait until reader transaction finish reading - readWg.Wait() + tx2, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + }) + if err != nil { + dbt.Fatal(err) + } - err = tx.Commit() - if err != nil { - dbt.Fatal(err) - } - commitWg.Done() + _, err = tx1.ExecContext(ctx, "INSERT INTO test VALUES (1)") + if err != nil { + dbt.Fatal(err) + } - testDoneWg.Done() + var v int + row := tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") + if err := row.Scan(&v); err != nil { + dbt.Fatal(err) + } + // Because writer transaction wasn't commited yet, it should be available + if v != 0 { + dbt.Errorf("expected val to be 0, got %d", v) } - readCommitedGoroutine := func() { - tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ - Isolation: sql.LevelReadCommitted, - }) - if err != nil { - dbt.Fatal(err) - } - beginWg.Done() - // Wait until writer transaction will begin - beginWg.Wait() - // Wait until writer transaction will insert value - insertWg.Wait() - var v int - row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") - if err := row.Scan(&v); err != nil { - dbt.Fatal(err) - } - // Because writer transaction wasn't commited yet, it should be available - if v != 0 { - dbt.Errorf("expected val to be 0, got %d", v) - } - readWg.Done() - // Wait until writer transaction will commit - commitWg.Wait() - row = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") - if err := row.Scan(&v); err != nil { - dbt.Fatal(err) - } - // Data written by writer transaction is already commited, it should be selectable - if v != 1 { - dbt.Errorf("expected val to be 1, got %d", v) - } - tx.Commit() - testDoneWg.Done() + err = tx1.Commit() + if err != nil { + dbt.Fatal(err) } - go repeatableReadGoroutine() - go readCommitedGoroutine() - testDoneWg.Wait() + row = tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") + if err := row.Scan(&v); err != nil { + dbt.Fatal(err) + } + // Data written by writer transaction is already commited, it should be selectable + if v != 1 { + dbt.Errorf("expected val to be 1, got %d", v) + } + tx2.Commit() }) } From 06cbc7a489892d35f2e83513eadf128fc3e2c274 Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Wed, 14 Jun 2017 09:34:03 -0700 Subject: [PATCH 5/6] Applied more review comments --- AUTHORS | 2 +- connection_go18.go | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/AUTHORS b/AUTHORS index 45efbae09..10e2ebb94 100644 --- a/AUTHORS +++ b/AUTHORS @@ -46,6 +46,7 @@ Lion Yang Luca Looz Lucas Liu Luke Scott +Maciej Zimnoch Michael Woolnough Nicola Peduzzi Olivier Mengué @@ -61,7 +62,6 @@ Xiangyu Hu Xiaobing Jiang Xiuming Chen Zhenye Xie -Maciej Zimnoch # Organizations diff --git a/connection_go18.go b/connection_go18.go index 5053497d0..5e802fc24 100644 --- a/connection_go18.go +++ b/connection_go18.go @@ -50,31 +50,24 @@ func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver return nil, err } + defer mc.finish() + if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault { level, err := mapIsolationLevel(opts.Isolation) if err != nil { - mc.finish() return nil, err } err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level) if err != nil { - mc.finish() return nil, err } } tx, err := mc.Begin() - mc.finish() if err != nil { return nil, err } - select { - default: - case <-ctx.Done(): - tx.Rollback() - return nil, ctx.Err() - } return tx, err } From 62836ef5c9de9f7125e4a7fc5f0a468df4e6016a Mon Sep 17 00:00:00 2001 From: Maciej Zimnoch Date: Thu, 15 Jun 2017 07:54:10 -0700 Subject: [PATCH 6/6] Applied review remarks --- connection_go18.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/connection_go18.go b/connection_go18.go index 5e802fc24..3ff6ff24f 100644 --- a/connection_go18.go +++ b/connection_go18.go @@ -63,12 +63,7 @@ func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver } } - tx, err := mc.Begin() - if err != nil { - return nil, err - } - - return tx, err + return mc.Begin() } func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {