diff options
Diffstat (limited to 'src/pkg/database/sql')
-rw-r--r-- | src/pkg/database/sql/convert.go | 50 | ||||
-rw-r--r-- | src/pkg/database/sql/convert_test.go | 56 | ||||
-rw-r--r-- | src/pkg/database/sql/driver/driver.go | 2 | ||||
-rw-r--r-- | src/pkg/database/sql/example_test.go | 1 | ||||
-rw-r--r-- | src/pkg/database/sql/fakedb_test.go | 32 | ||||
-rw-r--r-- | src/pkg/database/sql/sql.go | 166 | ||||
-rw-r--r-- | src/pkg/database/sql/sql_test.go | 137 |
7 files changed, 364 insertions, 80 deletions
diff --git a/src/pkg/database/sql/convert.go b/src/pkg/database/sql/convert.go index c04adde1f..c0b38a249 100644 --- a/src/pkg/database/sql/convert.go +++ b/src/pkg/database/sql/convert.go @@ -160,27 +160,19 @@ func convertAssign(dest, src interface{}) error { reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: - *d = fmt.Sprintf("%v", src) + *d = asString(src) return nil } case *[]byte: sv = reflect.ValueOf(src) - switch sv.Kind() { - case reflect.Bool, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64: - *d = []byte(fmt.Sprintf("%v", src)) + if b, ok := asBytes(nil, sv); ok { + *d = b return nil } case *RawBytes: sv = reflect.ValueOf(src) - switch sv.Kind() { - case reflect.Bool, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64: - *d = RawBytes(fmt.Sprintf("%v", src)) + if b, ok := asBytes([]byte(*d)[:0], sv); ok { + *d = RawBytes(b) return nil } case *bool: @@ -271,5 +263,37 @@ func asString(src interface{}) string { case []byte: return string(v) } + rv := reflect.ValueOf(src) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(rv.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(rv.Uint(), 10) + case reflect.Float64: + return strconv.FormatFloat(rv.Float(), 'g', -1, 64) + case reflect.Float32: + return strconv.FormatFloat(rv.Float(), 'g', -1, 32) + case reflect.Bool: + return strconv.FormatBool(rv.Bool()) + } return fmt.Sprintf("%v", src) } + +func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) { + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.AppendInt(buf, rv.Int(), 10), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.AppendUint(buf, rv.Uint(), 10), true + case reflect.Float32: + return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true + case reflect.Float64: + return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true + case reflect.Bool: + return strconv.AppendBool(buf, rv.Bool()), true + case reflect.String: + s := rv.String() + return append(buf, s...), true + } + return +} diff --git a/src/pkg/database/sql/convert_test.go b/src/pkg/database/sql/convert_test.go index a39c2c54f..6e2483012 100644 --- a/src/pkg/database/sql/convert_test.go +++ b/src/pkg/database/sql/convert_test.go @@ -8,6 +8,7 @@ import ( "database/sql/driver" "fmt" "reflect" + "runtime" "testing" "time" ) @@ -279,3 +280,58 @@ func TestValueConverters(t *testing.T) { } } } + +// Tests that assigning to RawBytes doesn't allocate (and also works). +func TestRawBytesAllocs(t *testing.T) { + buf := make(RawBytes, 10) + test := func(name string, in interface{}, want string) { + if err := convertAssign(&buf, in); err != nil { + t.Fatalf("%s: convertAssign = %v", name, err) + } + match := len(buf) == len(want) + if match { + for i, b := range buf { + if want[i] != b { + match = false + break + } + } + } + if !match { + t.Fatalf("%s: got %q (len %d); want %q (len %d)", name, buf, len(buf), want, len(want)) + } + } + n := testing.AllocsPerRun(100, func() { + test("uint64", uint64(12345678), "12345678") + test("uint32", uint32(1234), "1234") + test("uint16", uint16(12), "12") + test("uint8", uint8(1), "1") + test("uint", uint(123), "123") + test("int", int(123), "123") + test("int8", int8(1), "1") + test("int16", int16(12), "12") + test("int32", int32(1234), "1234") + test("int64", int64(12345678), "12345678") + test("float32", float32(1.5), "1.5") + test("float64", float64(64), "64") + test("bool", false, "false") + }) + + // The numbers below are only valid for 64-bit interface word sizes, + // and gc. With 32-bit words there are more convT2E allocs, and + // with gccgo, only pointers currently go in interface data. + // So only care on amd64 gc for now. + measureAllocs := runtime.GOARCH == "amd64" && runtime.Compiler == "gc" + + if n > 0.5 && measureAllocs { + t.Fatalf("allocs = %v; want 0", n) + } + + // This one involves a convT2E allocation, string -> interface{} + n = testing.AllocsPerRun(100, func() { + test("string", "foo", "foo") + }) + if n > 1.5 && measureAllocs { + t.Fatalf("allocs = %v; want max 1", n) + } +} diff --git a/src/pkg/database/sql/driver/driver.go b/src/pkg/database/sql/driver/driver.go index 0828e63c6..eca25f29a 100644 --- a/src/pkg/database/sql/driver/driver.go +++ b/src/pkg/database/sql/driver/driver.go @@ -134,7 +134,7 @@ type Stmt interface { // as an INSERT or UPDATE. Exec(args []Value) (Result, error) - // Exec executes a query that may return rows, such as a + // Query executes a query that may return rows, such as a // SELECT. Query(args []Value) (Rows, error) } diff --git a/src/pkg/database/sql/example_test.go b/src/pkg/database/sql/example_test.go index d47eed50c..dcb74e069 100644 --- a/src/pkg/database/sql/example_test.go +++ b/src/pkg/database/sql/example_test.go @@ -18,6 +18,7 @@ func ExampleDB_Query() { if err != nil { log.Fatal(err) } + defer rows.Close() for rows.Next() { var name string if err := rows.Scan(&name); err != nil { diff --git a/src/pkg/database/sql/fakedb_test.go b/src/pkg/database/sql/fakedb_test.go index a8adfdd94..c7db0dd77 100644 --- a/src/pkg/database/sql/fakedb_test.go +++ b/src/pkg/database/sql/fakedb_test.go @@ -23,7 +23,7 @@ var _ = log.Printf // interface, just for testing. // // It speaks a query language that's semantically similar to but -// syntantically different and simpler than SQL. The syntax is as +// syntactically different and simpler than SQL. The syntax is as // follows: // // WIPE @@ -433,11 +433,19 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e return stmt, nil } +// hook to simulate broken connections +var hookPrepareBadConn func() bool + func (c *fakeConn) Prepare(query string) (driver.Stmt, error) { c.numPrepare++ if c.db == nil { panic("nil c.db; conn = " + fmt.Sprintf("%#v", c)) } + + if hookPrepareBadConn != nil && hookPrepareBadConn() { + return nil, driver.ErrBadConn + } + parts := strings.Split(query, "|") if len(parts) < 1 { return nil, errf("empty query") @@ -489,10 +497,18 @@ func (s *fakeStmt) Close() error { var errClosed = errors.New("fakedb: statement has been closed") +// hook to simulate broken connections +var hookExecBadConn func() bool + func (s *fakeStmt) Exec(args []driver.Value) (driver.Result, error) { if s.closed { return nil, errClosed } + + if hookExecBadConn != nil && hookExecBadConn() { + return nil, driver.ErrBadConn + } + err := checkSubsetTypes(args) if err != nil { return nil, err @@ -565,10 +581,18 @@ func (s *fakeStmt) execInsert(args []driver.Value, doInsert bool) (driver.Result return driver.RowsAffected(1), nil } +// hook to simulate broken connections +var hookQueryBadConn func() bool + func (s *fakeStmt) Query(args []driver.Value) (driver.Rows, error) { if s.closed { return nil, errClosed } + + if hookQueryBadConn != nil && hookQueryBadConn() { + return nil, driver.ErrBadConn + } + err := checkSubsetTypes(args) if err != nil { return nil, err @@ -686,7 +710,13 @@ func (rc *rowsCursor) Columns() []string { return rc.cols } +var rowsCursorNextHook func(dest []driver.Value) error + func (rc *rowsCursor) Next(dest []driver.Value) error { + if rowsCursorNextHook != nil { + return rowsCursorNextHook(dest) + } + if rc.closed { return errors.New("fakedb: cursor is closed") } diff --git a/src/pkg/database/sql/sql.go b/src/pkg/database/sql/sql.go index 84a096513..765b80c60 100644 --- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -181,7 +181,8 @@ type Scanner interface { // defers this error until a Scan. var ErrNoRows = errors.New("sql: no rows in result set") -// DB is a database handle. It's safe for concurrent use by multiple +// DB is a database handle representing a pool of zero or more +// underlying connections. It's safe for concurrent use by multiple // goroutines. // // The sql package creates and frees connections automatically; it @@ -256,7 +257,7 @@ func (dc *driverConn) prepareLocked(query string) (driver.Stmt, error) { // stmt closes if the conn is about to close anyway? For now // do the safe thing, in case stmts need to be closed. // - // TODO(bradfitz): after Go 1.1, closing driver.Stmts + // TODO(bradfitz): after Go 1.2, closing driver.Stmts // should be moved to driverStmt, using unique // *driverStmts everywhere (including from // *Stmt.connStmt, instead of returning a @@ -405,7 +406,7 @@ func (db *DB) removeDepLocked(x finalCloser, dep interface{}) func() error { // This value should be larger than the maximum typical value // used for db.maxOpen. If maxOpen is significantly larger than // connectionRequestQueueSize then it is possible for ALL calls into the *DB -// to block until the connectionOpener can satify the backlog of requests. +// to block until the connectionOpener can satisfy the backlog of requests. var connectionRequestQueueSize = 1000000 // Open opens a database specified by its database driver name and a @@ -420,6 +421,11 @@ var connectionRequestQueueSize = 1000000 // Open may just validate its arguments without creating a connection // to the database. To verify that the data source name is valid, call // Ping. +// +// The returned DB is safe for concurrent use by multiple goroutines +// and maintains its own pool of idle connections. Thus, the Open +// function should be called just once. It is rarely necessary to +// close a DB. func Open(driverName, dataSourceName string) (*DB, error) { driveri, ok := drivers[driverName] if !ok { @@ -452,6 +458,9 @@ func (db *DB) Ping() error { } // Close closes the database, releasing any open resources. +// +// It is rare to Close a DB, as the DB handle is meant to be +// long-lived and shared between many goroutines. func (db *DB) Close() error { db.mu.Lock() if db.closed { // Make DB.Close idempotent @@ -569,7 +578,7 @@ func (db *DB) maybeOpenNewConnections() { } } -// Runs in a seperate goroutine, opens new connections when requested. +// Runs in a separate goroutine, opens new connections when requested. func (db *DB) connectionOpener() { for _ = range db.openerCh { db.openNewConnection() @@ -652,13 +661,16 @@ func (db *DB) conn() (*driverConn, error) { return conn, nil } + db.numOpen++ // optimistically db.mu.Unlock() ci, err := db.driver.Open(db.dsn) if err != nil { + db.mu.Lock() + db.numOpen-- // correct for earlier optimism + db.mu.Unlock() return nil, err } db.mu.Lock() - db.numOpen++ dc := &driverConn{ db: db, ci: ci, @@ -774,11 +786,11 @@ func (db *DB) putConn(dc *driverConn, err error) { // Satisfy a connRequest or put the driverConn in the idle pool and return true // or return false. // putConnDBLocked will satisfy a connRequest if there is one, or it will -// return the *driverConn to the freeConn list if err != nil and the idle -// connection limit would not be reached. +// return the *driverConn to the freeConn list if err == nil and the idle +// connection limit will not be exceeded. // If err != nil, the value of dc is ignored. // If err == nil, then dc must not equal nil. -// If a connRequest was fullfilled or the *driverConn was placed in the +// If a connRequest was fulfilled or the *driverConn was placed in the // freeConn list, then true is returned, otherwise false is returned. func (db *DB) putConnDBLocked(dc *driverConn, err error) bool { if db.connRequests.Len() > 0 { @@ -791,20 +803,24 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool { req <- dc } return true - } else if err == nil && !db.closed && db.maxIdleConnsLocked() > 0 && db.maxIdleConnsLocked() > db.freeConn.Len() { + } else if err == nil && !db.closed && db.maxIdleConnsLocked() > db.freeConn.Len() { dc.listElem = db.freeConn.PushFront(dc) return true } return false } +// maxBadConnRetries is the number of maximum retries if the driver returns +// driver.ErrBadConn to signal a broken connection. +const maxBadConnRetries = 10 + // Prepare creates a prepared statement for later queries or executions. // Multiple queries or executions may be run concurrently from the // returned statement. func (db *DB) Prepare(query string) (*Stmt, error) { var stmt *Stmt var err error - for i := 0; i < 10; i++ { + for i := 0; i < maxBadConnRetries; i++ { stmt, err = db.prepare(query) if err != driver.ErrBadConn { break @@ -846,7 +862,7 @@ func (db *DB) prepare(query string) (*Stmt, error) { func (db *DB) Exec(query string, args ...interface{}) (Result, error) { var res Result var err error - for i := 0; i < 10; i++ { + for i := 0; i < maxBadConnRetries; i++ { res, err = db.exec(query, args) if err != driver.ErrBadConn { break @@ -895,7 +911,7 @@ func (db *DB) exec(query string, args []interface{}) (res Result, err error) { func (db *DB) Query(query string, args ...interface{}) (*Rows, error) { var rows *Rows var err error - for i := 0; i < 10; i++ { + for i := 0; i < maxBadConnRetries; i++ { rows, err = db.query(query, args) if err != driver.ErrBadConn { break @@ -983,7 +999,7 @@ func (db *DB) QueryRow(query string, args ...interface{}) *Row { func (db *DB) Begin() (*Tx, error) { var tx *Tx var err error - for i := 0; i < 10; i++ { + for i := 0; i < maxBadConnRetries; i++ { tx, err = db.begin() if err != driver.ErrBadConn { break @@ -1245,13 +1261,24 @@ type Stmt struct { func (s *Stmt) Exec(args ...interface{}) (Result, error) { s.closemu.RLock() defer s.closemu.RUnlock() - dc, releaseConn, si, err := s.connStmt() - if err != nil { - return nil, err - } - defer releaseConn(nil) - return resultFromStatement(driverStmt{dc, si}, args...) + var res Result + for i := 0; i < maxBadConnRetries; i++ { + dc, releaseConn, si, err := s.connStmt() + if err != nil { + if err == driver.ErrBadConn { + continue + } + return nil, err + } + + res, err = resultFromStatement(driverStmt{dc, si}, args...) + releaseConn(err) + if err != driver.ErrBadConn { + return res, err + } + } + return nil, driver.ErrBadConn } func resultFromStatement(ds driverStmt, args ...interface{}) (Result, error) { @@ -1329,26 +1356,21 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St // Make a new conn if all are busy. // TODO(bradfitz): or wait for one? make configurable later? if !match { - for i := 0; ; i++ { - dc, err := s.db.conn() - if err != nil { - return nil, nil, nil, err - } - dc.Lock() - si, err := dc.prepareLocked(s.query) - dc.Unlock() - if err == driver.ErrBadConn && i < 10 { - continue - } - if err != nil { - return nil, nil, nil, err - } - s.mu.Lock() - cs = connStmt{dc, si} - s.css = append(s.css, cs) - s.mu.Unlock() - break + dc, err := s.db.conn() + if err != nil { + return nil, nil, nil, err + } + dc.Lock() + si, err := dc.prepareLocked(s.query) + dc.Unlock() + if err != nil { + s.db.putConn(dc, err) + return nil, nil, nil, err } + s.mu.Lock() + cs = connStmt{dc, si} + s.css = append(s.css, cs) + s.mu.Unlock() } conn := cs.dc @@ -1361,31 +1383,39 @@ func (s *Stmt) Query(args ...interface{}) (*Rows, error) { s.closemu.RLock() defer s.closemu.RUnlock() - dc, releaseConn, si, err := s.connStmt() - if err != nil { - return nil, err - } + var rowsi driver.Rows + for i := 0; i < maxBadConnRetries; i++ { + dc, releaseConn, si, err := s.connStmt() + if err != nil { + if err == driver.ErrBadConn { + continue + } + return nil, err + } - ds := driverStmt{dc, si} - rowsi, err := rowsiFromStatement(ds, args...) - if err != nil { - releaseConn(err) - return nil, err - } + rowsi, err = rowsiFromStatement(driverStmt{dc, si}, args...) + if err == nil { + // Note: ownership of ci passes to the *Rows, to be freed + // with releaseConn. + rows := &Rows{ + dc: dc, + rowsi: rowsi, + // releaseConn set below + } + s.db.addDep(s, rows) + rows.releaseConn = func(err error) { + releaseConn(err) + s.db.removeDep(s, rows) + } + return rows, nil + } - // Note: ownership of ci passes to the *Rows, to be freed - // with releaseConn. - rows := &Rows{ - dc: dc, - rowsi: rowsi, - // releaseConn set below - } - s.db.addDep(s, rows) - rows.releaseConn = func(err error) { releaseConn(err) - s.db.removeDep(s, rows) + if err != driver.ErrBadConn { + return nil, err + } } - return rows, nil + return nil, driver.ErrBadConn } func rowsiFromStatement(ds driverStmt, args ...interface{}) (driver.Rows, error) { @@ -1476,6 +1506,7 @@ func (s *Stmt) finalClose() error { // // rows, err := db.Query("SELECT ...") // ... +// defer rows.Close() // for rows.Next() { // var id int // var name string @@ -1495,10 +1526,12 @@ type Rows struct { closeStmt driver.Stmt // if non-nil, statement to Close on close } -// Next prepares the next result row for reading with the Scan method. -// It returns true on success, false if there is no next result row. -// Every call to Scan, even the first one, must be preceded by a call -// to Next. +// Next prepares the next result row for reading with the Scan method. It +// returns true on success, or false if there is no next result row or an error +// happened while preparing it. Err should be consulted to distinguish between +// the two cases. +// +// Every call to Scan, even the first one, must be preceded by a call to Next. func (rs *Rows) Next() bool { if rs.closed { return false @@ -1625,12 +1658,19 @@ func (r *Row) Scan(dest ...interface{}) error { } if !r.rows.Next() { + if err := r.rows.Err(); err != nil { + return err + } return ErrNoRows } err := r.rows.Scan(dest...) if err != nil { return err } + // Make sure the query can be processed to completion with no errors. + if err := r.rows.Close(); err != nil { + return err + } return nil } diff --git a/src/pkg/database/sql/sql_test.go b/src/pkg/database/sql/sql_test.go index 787a5c9f7..7971f1491 100644 --- a/src/pkg/database/sql/sql_test.go +++ b/src/pkg/database/sql/sql_test.go @@ -348,7 +348,6 @@ func TestStatementQueryRow(t *testing.T) { t.Errorf("%d: age=%d, want %d", n, age, tt.want) } } - } // golang.org/issue/3734 @@ -462,7 +461,7 @@ func TestTxStmt(t *testing.T) { } // Issue: http://golang.org/issue/2784 -// This test didn't fail before because we got luckly with the fakedb driver. +// This test didn't fail before because we got lucky with the fakedb driver. // It was failing, and now not, in github.com/bradfitz/go-sql-test func TestTxQuery(t *testing.T) { db := newTestDB(t, "") @@ -660,6 +659,35 @@ func TestQueryRowClosingStmt(t *testing.T) { } } +// Test issue 6651 +func TestIssue6651(t *testing.T) { + db := newTestDB(t, "people") + defer closeDB(t, db) + + var v string + + want := "error in rows.Next" + rowsCursorNextHook = func(dest []driver.Value) error { + return fmt.Errorf(want) + } + defer func() { rowsCursorNextHook = nil }() + err := db.QueryRow("SELECT|people|name|").Scan(&v) + if err == nil || err.Error() != want { + t.Errorf("error = %q; want %q", err, want) + } + rowsCursorNextHook = nil + + want = "error in rows.Close" + rowsCloseHook = func(rows *Rows, err *error) { + *err = fmt.Errorf(want) + } + defer func() { rowsCloseHook = nil }() + err = db.QueryRow("SELECT|people|name|").Scan(&v) + if err == nil || err.Error() != want { + t.Errorf("error = %q; want %q", err, want) + } +} + type nullTestRow struct { nullParam interface{} notNullParam interface{} @@ -1249,6 +1277,111 @@ func TestStmtCloseOrder(t *testing.T) { } } +// golang.org/issue/5781 +func TestErrBadConnReconnect(t *testing.T) { + db := newTestDB(t, "foo") + defer closeDB(t, db) + exec(t, db, "CREATE|t1|name=string,age=int32,dead=bool") + + simulateBadConn := func(name string, hook *func() bool, op func() error) { + broken, retried := false, false + numOpen := db.numOpen + + // simulate a broken connection on the first try + *hook = func() bool { + if !broken { + broken = true + return true + } + retried = true + return false + } + + if err := op(); err != nil { + t.Errorf(name+": %v", err) + return + } + + if !broken || !retried { + t.Error(name + ": Failed to simulate broken connection") + } + *hook = nil + + if numOpen != db.numOpen { + t.Errorf(name+": leaked %d connection(s)!", db.numOpen-numOpen) + numOpen = db.numOpen + } + } + + // db.Exec + dbExec := func() error { + _, err := db.Exec("INSERT|t1|name=?,age=?,dead=?", "Gordon", 3, true) + return err + } + simulateBadConn("db.Exec prepare", &hookPrepareBadConn, dbExec) + simulateBadConn("db.Exec exec", &hookExecBadConn, dbExec) + + // db.Query + dbQuery := func() error { + rows, err := db.Query("SELECT|t1|age,name|") + if err == nil { + err = rows.Close() + } + return err + } + simulateBadConn("db.Query prepare", &hookPrepareBadConn, dbQuery) + simulateBadConn("db.Query query", &hookQueryBadConn, dbQuery) + + // db.Prepare + simulateBadConn("db.Prepare", &hookPrepareBadConn, func() error { + stmt, err := db.Prepare("INSERT|t1|name=?,age=?,dead=?") + if err != nil { + return err + } + stmt.Close() + return nil + }) + + // stmt.Exec + stmt1, err := db.Prepare("INSERT|t1|name=?,age=?,dead=?") + if err != nil { + t.Fatalf("prepare: %v", err) + } + defer stmt1.Close() + // make sure we must prepare the stmt first + for _, cs := range stmt1.css { + cs.dc.inUse = true + } + + stmtExec := func() error { + _, err := stmt1.Exec("Gopher", 3, false) + return err + } + simulateBadConn("stmt.Exec prepare", &hookPrepareBadConn, stmtExec) + simulateBadConn("stmt.Exec exec", &hookExecBadConn, stmtExec) + + // stmt.Query + stmt2, err := db.Prepare("SELECT|t1|age,name|") + if err != nil { + t.Fatalf("prepare: %v", err) + } + defer stmt2.Close() + // make sure we must prepare the stmt first + for _, cs := range stmt2.css { + cs.dc.inUse = true + } + + stmtQuery := func() error { + rows, err := stmt2.Query() + if err == nil { + err = rows.Close() + } + return err + } + simulateBadConn("stmt.Query prepare", &hookPrepareBadConn, stmtQuery) + simulateBadConn("stmt.Query exec", &hookQueryBadConn, stmtQuery) +} + type concurrentTest interface { init(t testing.TB, db *DB) finish(t testing.TB) |