diff --git a/.gitignore b/.gitignore index ba8e0cb3a..2de28da16 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Icon? ehthumbs.db Thumbs.db +.idea diff --git a/AUTHORS b/AUTHORS index 58c749e29..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é diff --git a/connection_go18.go b/connection_go18.go index c4ca4c33f..3ff6ff24f 100644 --- a/connection_go18.go +++ b/connection_go18.go @@ -41,10 +41,6 @@ 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") @@ -54,19 +50,20 @@ func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver return nil, err } - tx, err := mc.Begin() - mc.finish() - if err != nil { - return nil, err - } + defer mc.finish() - select { - default: - case <-ctx.Done(): - tx.Rollback() - return nil, ctx.Err() + 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 + } } - return tx, err + + return mc.Begin() } func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { diff --git a/driver_go18_test.go b/driver_go18_test.go index 69d0a2b7d..f2184add0 100644 --- a/driver_go18_test.go +++ b/driver_go18_test.go @@ -468,3 +468,55 @@ 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() + + tx1, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + }) + if err != nil { + dbt.Fatal(err) + } + + tx2, err := dbt.db.BeginTx(ctx, &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + }) + if err != nil { + dbt.Fatal(err) + } + + _, err = tx1.ExecContext(ctx, "INSERT INTO test VALUES (1)") + if err != nil { + dbt.Fatal(err) + } + + 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) + } + + err = tx1.Commit() + if err != nil { + dbt.Fatal(err) + } + + 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() + }) +} 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") + } + +}