sql: add DB.Close, fix bugs, remove Execer on Driver (only Conn)
R=rsc
CC=golang-dev
https://golang.org/cl/5372099
diff --git a/src/pkg/exp/sql/convert.go b/src/pkg/exp/sql/convert.go
index e46cebe9..48e2812 100644
--- a/src/pkg/exp/sql/convert.go
+++ b/src/pkg/exp/sql/convert.go
@@ -14,6 +14,21 @@
"strconv"
)
+// subsetTypeArgs takes a slice of arguments from callers of the sql
+// package and converts them into a slice of the driver package's
+// "subset types".
+func subsetTypeArgs(args []interface{}) ([]interface{}, error) {
+ out := make([]interface{}, len(args))
+ for n, arg := range args {
+ var err error
+ out[n], err = driver.DefaultParameterConverter.ConvertValue(arg)
+ if err != nil {
+ return nil, fmt.Errorf("sql: converting argument #%d's type: %v", n+1, err)
+ }
+ }
+ return out, nil
+}
+
// convertAssign copies to dest the value in src, converting it if possible.
// An error is returned if the copy would result in loss of information.
// dest should be a pointer type.
diff --git a/src/pkg/exp/sql/driver/driver.go b/src/pkg/exp/sql/driver/driver.go
index 6a51c34..35fc6ae 100644
--- a/src/pkg/exp/sql/driver/driver.go
+++ b/src/pkg/exp/sql/driver/driver.go
@@ -36,19 +36,22 @@
Open(name string) (Conn, error)
}
-// Execer is an optional interface that may be implemented by a Driver
-// or a Conn.
-//
-// If a Driver does not implement Execer, the sql package's DB.Exec
-// method first obtains a free connection from its free pool or from
-// the driver's Open method. Execer should only be implemented by
-// drivers that can provide a more efficient implementation.
+// ErrSkip may be returned by some optional interfaces' methods to
+// indicate at runtime that the fast path is unavailable and the sql
+// package should continue as if the optional interface was not
+// implemented. ErrSkip is only supported where explicitly
+// documented.
+var ErrSkip = errors.New("driver: skip fast-path; continue as if unimplemented")
+
+// Execer is an optional interface that may be implemented by a Conn.
//
// If a Conn does not implement Execer, the db package's DB.Exec will
// first prepare a query, execute the statement, and then close the
// statement.
//
// All arguments are of a subset type as defined in the package docs.
+//
+// Exec may return ErrSkip.
type Execer interface {
Exec(query string, args []interface{}) (Result, error)
}
diff --git a/src/pkg/exp/sql/fakedb_test.go b/src/pkg/exp/sql/fakedb_test.go
index c8a1997..17028e2 100644
--- a/src/pkg/exp/sql/fakedb_test.go
+++ b/src/pkg/exp/sql/fakedb_test.go
@@ -195,6 +195,29 @@
return nil
}
+func checkSubsetTypes(args []interface{}) error {
+ for n, arg := range args {
+ switch arg.(type) {
+ case int64, float64, bool, nil, []byte, string:
+ default:
+ return fmt.Errorf("fakedb_test: invalid argument #%d: %v, type %T", n+1, arg, arg)
+ }
+ }
+ return nil
+}
+
+func (c *fakeConn) Exec(query string, args []interface{}) (driver.Result, error) {
+ // This is an optional interface, but it's implemented here
+ // just to check that all the args of of the proper types.
+ // ErrSkip is returned so the caller acts as if we didn't
+ // implement this at all.
+ err := checkSubsetTypes(args)
+ if err != nil {
+ return nil, err
+ }
+ return nil, driver.ErrSkip
+}
+
func errf(msg string, args ...interface{}) error {
return errors.New("fakedb: " + fmt.Sprintf(msg, args...))
}
@@ -323,6 +346,11 @@
}
func (s *fakeStmt) Exec(args []interface{}) (driver.Result, error) {
+ err := checkSubsetTypes(args)
+ if err != nil {
+ return nil, err
+ }
+
db := s.c.db
switch s.cmd {
case "WIPE":
@@ -377,6 +405,11 @@
}
func (s *fakeStmt) Query(args []interface{}) (driver.Rows, error) {
+ err := checkSubsetTypes(args)
+ if err != nil {
+ return nil, err
+ }
+
db := s.c.db
if len(args) != s.placeholders {
panic("error in pkg db; should only get here if size is correct")
diff --git a/src/pkg/exp/sql/sql.go b/src/pkg/exp/sql/sql.go
index 291af7f..d3677af 100644
--- a/src/pkg/exp/sql/sql.go
+++ b/src/pkg/exp/sql/sql.go
@@ -88,8 +88,9 @@
driver driver.Driver
dsn string
- mu sync.Mutex
+ mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
+ closed bool
}
// Open opens a database specified by its database driver name and a
@@ -106,6 +107,22 @@
return &DB{driver: driver, dsn: dataSourceName}, nil
}
+// Close closes the database, releasing any open resources.
+func (db *DB) Close() error {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+ var err error
+ for _, c := range db.freeConn {
+ err1 := c.Close()
+ if err1 != nil {
+ err = err1
+ }
+ }
+ db.freeConn = nil
+ db.closed = true
+ return err
+}
+
func (db *DB) maxIdleConns() int {
const defaultMaxIdleConns = 2
// TODO(bradfitz): ask driver, if supported, for its default preference
@@ -116,6 +133,9 @@
// conn returns a newly-opened or cached driver.Conn
func (db *DB) conn() (driver.Conn, error) {
db.mu.Lock()
+ if db.closed {
+ return nil, errors.New("sql: database is closed")
+ }
if n := len(db.freeConn); n > 0 {
conn := db.freeConn[n-1]
db.freeConn = db.freeConn[:n-1]
@@ -140,11 +160,13 @@
}
func (db *DB) putConn(c driver.Conn) {
- if n := len(db.freeConn); n < db.maxIdleConns() {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+ if n := len(db.freeConn); !db.closed && n < db.maxIdleConns() {
db.freeConn = append(db.freeConn, c)
return
}
- db.closeConn(c)
+ db.closeConn(c) // TODO(bradfitz): release lock before calling this?
}
func (db *DB) closeConn(c driver.Conn) {
@@ -180,17 +202,11 @@
// Exec executes a query without returning any rows.
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
- // Optional fast path, if the driver implements driver.Execer.
- if execer, ok := db.driver.(driver.Execer); ok {
- resi, err := execer.Exec(query, args)
- if err != nil {
- return nil, err
- }
- return result{resi}, nil
+ sargs, err := subsetTypeArgs(args)
+ if err != nil {
+ return nil, err
}
- // If the driver does not implement driver.Execer, we need
- // a connection.
ci, err := db.conn()
if err != nil {
return nil, err
@@ -198,11 +214,13 @@
defer db.putConn(ci)
if execer, ok := ci.(driver.Execer); ok {
- resi, err := execer.Exec(query, args)
- if err != nil {
- return nil, err
+ resi, err := execer.Exec(query, sargs)
+ if err != driver.ErrSkip {
+ if err != nil {
+ return nil, err
+ }
+ return result{resi}, nil
}
- return result{resi}, nil
}
sti, err := ci.Prepare(query)
@@ -210,7 +228,8 @@
return nil, err
}
defer sti.Close()
- resi, err := sti.Exec(args)
+
+ resi, err := sti.Exec(sargs)
if err != nil {
return nil, err
}
@@ -386,7 +405,13 @@
return nil, err
}
defer sti.Close()
- resi, err := sti.Exec(args)
+
+ sargs, err := subsetTypeArgs(args)
+ if err != nil {
+ return nil, err
+ }
+
+ resi, err := sti.Exec(sargs)
if err != nil {
return nil, err
}
@@ -548,7 +573,11 @@
if len(args) != si.NumInput() {
return nil, fmt.Errorf("db: statement expects %d inputs; got %d", si.NumInput(), len(args))
}
- rowsi, err := si.Query(args)
+ sargs, err := subsetTypeArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ rowsi, err := si.Query(sargs)
if err != nil {
s.db.putConn(ci)
return nil, err
diff --git a/src/pkg/exp/sql/sql_test.go b/src/pkg/exp/sql/sql_test.go
index eb1bb58..d365f6b 100644
--- a/src/pkg/exp/sql/sql_test.go
+++ b/src/pkg/exp/sql/sql_test.go
@@ -34,8 +34,16 @@
}
}
+func closeDB(t *testing.T, db *DB) {
+ err := db.Close()
+ if err != nil {
+ t.Fatalf("error closing DB: %v", err)
+ }
+}
+
func TestQuery(t *testing.T) {
db := newTestDB(t, "people")
+ defer closeDB(t, db)
var name string
var age int
@@ -69,6 +77,7 @@
func TestStatementQueryRow(t *testing.T) {
db := newTestDB(t, "people")
+ defer closeDB(t, db)
stmt, err := db.Prepare("SELECT|people|age|name=?")
if err != nil {
t.Fatalf("Prepare: %v", err)
@@ -94,6 +103,7 @@
// just a test of fakedb itself
func TestBogusPreboundParameters(t *testing.T) {
db := newTestDB(t, "foo")
+ defer closeDB(t, db)
exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool")
_, err := db.Prepare("INSERT|t1|name=?,age=bogusconversion")
if err == nil {
@@ -106,6 +116,7 @@
func TestDb(t *testing.T) {
db := newTestDB(t, "foo")
+ defer closeDB(t, db)
exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool")
stmt, err := db.Prepare("INSERT|t1|name=?,age=?")
if err != nil {