database/sql: do not store Tx options in Context

Drivers which previously supported tip will need to update to this
revision before release.

Fixes #18284

Change-Id: I70b8e7afff1558a8b5348885ce9f50e067c72ee9
Reviewed-on: https://go-review.googlesource.com/34330
Run-TryBot: Daniel Theophanes <kardianos@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/src/database/sql/ctxutil.go b/src/database/sql/ctxutil.go
index 7c05ce2..1071446 100644
--- a/src/database/sql/ctxutil.go
+++ b/src/database/sql/ctxutil.go
@@ -111,25 +111,32 @@
 
 var errLevelNotSupported = errors.New("sql: selected isolation level is not supported")
 
-func ctxDriverBegin(ctx context.Context, ci driver.Conn) (driver.Tx, error) {
-	if ciCtx, is := ci.(driver.ConnBeginContext); is {
-		return ciCtx.BeginContext(ctx)
+func ctxDriverBegin(ctx context.Context, opts *TxOptions, ci driver.Conn) (driver.Tx, error) {
+	if ciCtx, is := ci.(driver.ConnBeginTx); is {
+		dopts := driver.TxOptions{}
+		if opts != nil {
+			dopts.Isolation = driver.IsolationLevel(opts.Isolation)
+			dopts.ReadOnly = opts.ReadOnly
+		}
+		return ciCtx.BeginTx(ctx, dopts)
 	}
 
 	if ctx.Done() == context.Background().Done() {
 		return ci.Begin()
 	}
 
-	// Check the transaction level in ctx. If set and non-default
-	// then return an error here as the BeginContext driver value is not supported.
-	if level, ok := driver.IsolationFromContext(ctx); ok && level != driver.IsolationLevel(LevelDefault) {
-		return nil, errors.New("sql: driver does not support non-default isolation level")
-	}
+	if opts != nil {
+		// Check the transaction level. If the transaction level is non-default
+		// then return an error here as the BeginTx driver value is not supported.
+		if opts.Isolation != LevelDefault {
+			return nil, errors.New("sql: driver does not support non-default isolation level")
+		}
 
-	// Check for a read-only parameter in ctx. If a read-only transaction is
-	// requested return an error as the BeginContext driver value is not supported.
-	if ro := driver.ReadOnlyFromContext(ctx); ro {
-		return nil, errors.New("sql: driver does not support read-only transactions")
+		// If a read-only transaction is requested return an error as the
+		// BeginTx driver value is not supported.
+		if opts.ReadOnly {
+			return nil, errors.New("sql: driver does not support read-only transactions")
+		}
 	}
 
 	txi, err := ci.Begin()
diff --git a/src/database/sql/driver/driver.go b/src/database/sql/driver/driver.go
index 2e47cd9..d66196f 100644
--- a/src/database/sql/driver/driver.go
+++ b/src/database/sql/driver/driver.go
@@ -10,7 +10,6 @@
 
 import (
 	"context"
-	"database/sql/internal"
 	"errors"
 	"reflect"
 )
@@ -157,7 +156,7 @@
 
 	// Begin starts and returns a new transaction.
 	//
-	// Deprecated: Drivers should implement ConnBeginContext instead (or additionally).
+	// Deprecated: Drivers should implement ConnBeginTx instead (or additionally).
 	Begin() (Tx, error)
 }
 
@@ -169,41 +168,35 @@
 	PrepareContext(ctx context.Context, query string) (Stmt, error)
 }
 
-// IsolationLevel is the transaction isolation level stored in Context.
+// IsolationLevel is the transaction isolation level stored in TxOptions.
 //
 // This type should be considered identical to sql.IsolationLevel along
 // with any values defined on it.
 type IsolationLevel int
 
-// IsolationFromContext extracts the isolation level from a Context.
-func IsolationFromContext(ctx context.Context) (level IsolationLevel, ok bool) {
-	level, ok = ctx.Value(internal.IsolationLevelKey{}).(IsolationLevel)
-	return level, ok
+// TxOptions holds the transaction options.
+//
+// This type should be considered identical to sql.TxOptions.
+type TxOptions struct {
+	Isolation IsolationLevel
+	ReadOnly  bool
 }
 
-// ReadOnlyFromContext extracts the read-only property from a Context.
-// When readonly is true the transaction must be set to read-only
-// or return an error.
-func ReadOnlyFromContext(ctx context.Context) (readonly bool) {
-	readonly, _ = ctx.Value(internal.ReadOnlyKey{}).(bool)
-	return readonly
-}
-
-// ConnBeginContext enhances the Conn interface with context.
-type ConnBeginContext interface {
-	// BeginContext starts and returns a new transaction.
+// ConnBeginTx enhances the Conn interface with context and TxOptions.
+type ConnBeginTx interface {
+	// BeginTx starts and returns a new transaction.
 	// If the context is canceled by the user the sql package will
 	// call Tx.Rollback before discarding and closing the connection.
 	//
-	// This must call IsolationFromContext to determine if there is a set
-	// isolation level. If the driver does not support setting the isolation
-	// level and one is set or if there is a set isolation level
-	// but the set level is not supported, an error must be returned.
+	// This must check opts.Isolation to determine if there is a set
+	// isolation level. If the driver does not support a non-default
+	// level and one is set or if there is a non-default isolation level
+	// that is not supported, an error must be returned.
 	//
-	// This must also call ReadOnlyFromContext to determine if the read-only
+	// This must also check opts.ReadOnly to determine if the read-only
 	// value is true to either set the read-only transaction property if supported
 	// or return an error if it is not supported.
-	BeginContext(ctx context.Context) (Tx, error)
+	BeginTx(ctx context.Context, opts TxOptions) (Tx, error)
 }
 
 // Result is the result of a query execution.
diff --git a/src/database/sql/internal/types.go b/src/database/sql/internal/types.go
deleted file mode 100644
index 1895144..0000000
--- a/src/database/sql/internal/types.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2016 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package internal
-
-// Context keys that set transaction properties for sql.BeginContext.
-type (
-	IsolationLevelKey struct{} // context value is driver.IsolationLevel
-	ReadOnlyKey       struct{} // context value is bool
-)
diff --git a/src/database/sql/sql.go b/src/database/sql/sql.go
index 9f056c5..9602450 100644
--- a/src/database/sql/sql.go
+++ b/src/database/sql/sql.go
@@ -18,7 +18,6 @@
 import (
 	"context"
 	"database/sql/driver"
-	"database/sql/internal"
 	"errors"
 	"fmt"
 	"io"
@@ -115,12 +114,10 @@
 	return NamedArg{Name: name, Value: value}
 }
 
-// IsolationLevel is the transaction isolation level stored in Context.
-// The IsolationLevel is set with IsolationContext and the context
-// should be passed to BeginContext.
+// IsolationLevel is the transaction isolation level used in TxOptions.
 type IsolationLevel int
 
-// Various isolation levels that drivers may support in BeginContext.
+// Various isolation levels that drivers may support in BeginTx.
 // If a driver does not support a given isolation level an error may be returned.
 //
 // See https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels.
@@ -135,18 +132,12 @@
 	LevelLinearizable
 )
 
-// IsolationContext returns a new Context that carries the provided isolation level.
-// The context must contain the isolation level before beginning the transaction
-// with BeginContext.
-func IsolationContext(ctx context.Context, level IsolationLevel) context.Context {
-	return context.WithValue(ctx, internal.IsolationLevelKey{}, driver.IsolationLevel(level))
-}
-
-// ReadOnlyWithContext returns a new Context that carries the provided
-// read-only transaction property. The context must contain the read-only property
-// before beginning the transaction with BeginContext.
-func ReadOnlyContext(ctx context.Context) context.Context {
-	return context.WithValue(ctx, internal.ReadOnlyKey{}, true)
+// TxOptions holds the transaction options to be used in DB.BeginTx.
+type TxOptions struct {
+	// Isolation is the transaction isolation level.
+	// If zero, the driver or database's default level is used.
+	Isolation IsolationLevel
+	ReadOnly  bool
 }
 
 // RawBytes is a byte slice that holds a reference to memory owned by
@@ -1311,28 +1302,27 @@
 	return db.QueryRowContext(context.Background(), query, args...)
 }
 
-// BeginContext starts a transaction.
+// BeginTx starts a transaction.
 //
 // The provided context is used until the transaction is committed or rolled back.
 // If the context is canceled, the sql package will roll back
 // the transaction. Tx.Commit will return an error if the context provided to
-// BeginContext is canceled.
+// BeginTx is canceled.
 //
-// An isolation level may be set by setting the value in the context
-// before calling this. If a non-default isolation level is used
-// that the driver doesn't support an error will be returned. Different drivers
-// may have slightly different meanings for the same isolation level.
-func (db *DB) BeginContext(ctx context.Context) (*Tx, error) {
+// The provided TxOptions is optional and may be nil if defaults should be used.
+// If a non-default isolation level is used that the driver doesn't support,
+// an error will be returned.
+func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error) {
 	var tx *Tx
 	var err error
 	for i := 0; i < maxBadConnRetries; i++ {
-		tx, err = db.begin(ctx, cachedOrNewConn)
+		tx, err = db.begin(ctx, opts, cachedOrNewConn)
 		if err != driver.ErrBadConn {
 			break
 		}
 	}
 	if err == driver.ErrBadConn {
-		return db.begin(ctx, alwaysNewConn)
+		return db.begin(ctx, opts, alwaysNewConn)
 	}
 	return tx, err
 }
@@ -1340,17 +1330,17 @@
 // Begin starts a transaction. The default isolation level is dependent on
 // the driver.
 func (db *DB) Begin() (*Tx, error) {
-	return db.BeginContext(context.Background())
+	return db.BeginTx(context.Background(), nil)
 }
 
-func (db *DB) begin(ctx context.Context, strategy connReuseStrategy) (tx *Tx, err error) {
+func (db *DB) begin(ctx context.Context, opts *TxOptions, strategy connReuseStrategy) (tx *Tx, err error) {
 	dc, err := db.conn(ctx, strategy)
 	if err != nil {
 		return nil, err
 	}
 	var txi driver.Tx
 	withLock(dc, func() {
-		txi, err = ctxDriverBegin(ctx, dc.ci)
+		txi, err = ctxDriverBegin(ctx, opts, dc.ci)
 	})
 	if err != nil {
 		db.putConn(dc, err)
diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go
index 1ec6217..422d219 100644
--- a/src/database/sql/sql_test.go
+++ b/src/database/sql/sql_test.go
@@ -375,7 +375,7 @@
 
 	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*15)
 
-	tx, err := db.BeginContext(ctx)
+	tx, err := db.BeginTx(ctx, nil)
 	if err != nil {
 		t.Fatal(err)
 	}