aboutsummaryrefslogtreecommitdiff
path: root/libgo/go/database
diff options
context:
space:
mode:
authorIan Lance Taylor <iant@golang.org>2019-01-18 19:04:36 +0000
committerIan Lance Taylor <ian@gcc.gnu.org>2019-01-18 19:04:36 +0000
commit4f4a855d82a889cebcfca150a7a43909bcb6a346 (patch)
treef12bae0781920fa34669fe30b6f4615a86d9fb80 /libgo/go/database
parent225220d668dafb8262db7012bced688acbe63b33 (diff)
downloadgcc-4f4a855d82a889cebcfca150a7a43909bcb6a346.zip
gcc-4f4a855d82a889cebcfca150a7a43909bcb6a346.tar.gz
gcc-4f4a855d82a889cebcfca150a7a43909bcb6a346.tar.bz2
libgo: update to Go1.12beta2
Reviewed-on: https://go-review.googlesource.com/c/158019 gotools/: * Makefile.am (go_cmd_vet_files): Update for Go1.12beta2 release. (GOTOOLS_TEST_TIMEOUT): Increase to 600. (check-runtime): Export LD_LIBRARY_PATH before computing GOARCH and GOOS. (check-vet): Copy golang.org/x/tools into check-vet-dir. * Makefile.in: Regenerate. gcc/testsuite/: * go.go-torture/execute/names-1.go: Stop using debug/xcoff, which is no longer externally visible. From-SVN: r268084
Diffstat (limited to 'libgo/go/database')
-rw-r--r--libgo/go/database/sql/convert.go45
-rw-r--r--libgo/go/database/sql/driver/driver.go7
-rw-r--r--libgo/go/database/sql/example_cli_test.go88
-rw-r--r--libgo/go/database/sql/example_service_test.go160
-rw-r--r--libgo/go/database/sql/fakedb_test.go20
-rw-r--r--libgo/go/database/sql/sql.go56
-rw-r--r--libgo/go/database/sql/sql_test.go102
7 files changed, 448 insertions, 30 deletions
diff --git a/libgo/go/database/sql/convert.go b/libgo/go/database/sql/convert.go
index 92a2ebe..c450d98 100644
--- a/libgo/go/database/sql/convert.go
+++ b/libgo/go/database/sql/convert.go
@@ -203,10 +203,18 @@ func driverArgsConnLocked(ci driver.Conn, ds *driverStmt, args []interface{}) ([
}
-// 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.
+// convertAssign is the same as convertAssignRows, but without the optional
+// rows argument.
func convertAssign(dest, src interface{}) error {
+ return convertAssignRows(dest, src, nil)
+}
+
+// convertAssignRows 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. If rows is passed in, the rows will
+// be used as the parent for any cursor values converted from a
+// driver.Rows to a *Rows.
+func convertAssignRows(dest, src interface{}, rows *Rows) error {
// Common cases, without reflect.
switch s := src.(type) {
case string:
@@ -299,6 +307,35 @@ func convertAssign(dest, src interface{}) error {
*d = nil
return nil
}
+ // The driver is returning a cursor the client may iterate over.
+ case driver.Rows:
+ switch d := dest.(type) {
+ case *Rows:
+ if d == nil {
+ return errNilPtr
+ }
+ if rows == nil {
+ return errors.New("invalid context to convert cursor rows, missing parent *Rows")
+ }
+ rows.closemu.Lock()
+ *d = Rows{
+ dc: rows.dc,
+ releaseConn: func(error) {},
+ rowsi: s,
+ }
+ // Chain the cancel function.
+ parentCancel := rows.cancel
+ rows.cancel = func() {
+ // When Rows.cancel is called, the closemu will be locked as well.
+ // So we can access rs.lasterr.
+ d.close(rows.lasterr)
+ if parentCancel != nil {
+ parentCancel()
+ }
+ }
+ rows.closemu.Unlock()
+ return nil
+ }
}
var sv reflect.Value
@@ -381,7 +418,7 @@ func convertAssign(dest, src interface{}) error {
return nil
}
dv.Set(reflect.New(dv.Type().Elem()))
- return convertAssign(dv.Interface(), src)
+ return convertAssignRows(dv.Interface(), src, rows)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s := asString(src)
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits())
diff --git a/libgo/go/database/sql/driver/driver.go b/libgo/go/database/sql/driver/driver.go
index 1e54b4c..5ff2bc9 100644
--- a/libgo/go/database/sql/driver/driver.go
+++ b/libgo/go/database/sql/driver/driver.go
@@ -24,6 +24,11 @@ import (
// []byte
// string
// time.Time
+//
+// If the driver supports cursors, a returned Value may also implement the Rows interface
+// in this package. This is used when, for example, when a user selects a cursor
+// such as "select cursor(select * from my_table) from dual". If the Rows
+// from the select is closed, the cursor Rows will also be closed.
type Value interface{}
// NamedValue holds both the value name and value.
@@ -469,7 +474,7 @@ type RowsAffected int64
var _ Result = RowsAffected(0)
func (RowsAffected) LastInsertId() (int64, error) {
- return 0, errors.New("no LastInsertId available")
+ return 0, errors.New("LastInsertId is not supported by this driver")
}
func (v RowsAffected) RowsAffected() (int64, error) {
diff --git a/libgo/go/database/sql/example_cli_test.go b/libgo/go/database/sql/example_cli_test.go
new file mode 100644
index 0000000..c87c508
--- /dev/null
+++ b/libgo/go/database/sql/example_cli_test.go
@@ -0,0 +1,88 @@
+// Copyright 2018 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.
+
+// +build ignore
+
+package sql_test
+
+import (
+ "context"
+ "database/sql"
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "time"
+)
+
+var pool *sql.DB // Database connection pool.
+
+func Example_openDBCLI() {
+ id := flag.Int64("id", 0, "person ID to find")
+ dsn := flag.String("dsn", os.Getenv("DSN"), "connection data source name")
+ flag.Parse()
+
+ if len(*dsn) == 0 {
+ log.Fatal("missing dsn flag")
+ }
+ if *id == 0 {
+ log.Fatal("missing person ID")
+ }
+ var err error
+
+ // Opening a driver typically will not attempt to connect to the database.
+ pool, err = sql.Open("driver-name", *dsn)
+ if err != nil {
+ // This will not be a connection error, but a DSN parse error or
+ // another initialization error.
+ log.Fatal("unable to use data source name", err)
+ }
+ defer pool.Close()
+
+ pool.SetConnMaxLifetime(0)
+ pool.SetMaxIdleConns(3)
+ pool.SetMaxOpenConns(3)
+
+ ctx, stop := context.WithCancel(context.Background())
+ defer stop()
+
+ appSignal := make(chan os.Signal, 3)
+ signal.Notify(appSignal, os.Interrupt)
+
+ go func() {
+ select {
+ case <-appSignal:
+ stop()
+ }
+ }()
+
+ Ping(ctx)
+
+ Query(ctx, *id)
+}
+
+// Ping the database to verify DSN provided by the user is valid and the
+// server accessible. If the ping fails exit the program with an error.
+func Ping(ctx context.Context) {
+ ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
+ defer cancel()
+
+ if err := pool.PingContext(ctx); err != nil {
+ log.Fatalf("unable to connect to database: %v", err)
+ }
+}
+
+// Query the database for the information requested and prints the results.
+// If the query fails exit the program with an error.
+func Query(ctx context.Context, id int64) {
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ var name string
+ err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&name)
+ if err != nil {
+ log.Fatal("unable to execute search query", err)
+ }
+ log.Println("name=", name)
+}
diff --git a/libgo/go/database/sql/example_service_test.go b/libgo/go/database/sql/example_service_test.go
new file mode 100644
index 0000000..7605e2f
--- /dev/null
+++ b/libgo/go/database/sql/example_service_test.go
@@ -0,0 +1,160 @@
+// Copyright 2018 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.
+
+// +build ignore
+
+package sql_test
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "time"
+)
+
+func Example_openDBService() {
+ // Opening a driver typically will not attempt to connect to the database.
+ db, err := sql.Open("driver-name", "database=test1")
+ if err != nil {
+ // This will not be a connection error, but a DSN parse error or
+ // another initialization error.
+ log.Fatal(err)
+ }
+ db.SetConnMaxLifetime(0)
+ db.SetMaxIdleConns(50)
+ db.SetMaxOpenConns(50)
+
+ s := &Service{db: db}
+
+ http.ListenAndServe(":8080", s)
+}
+
+type Service struct {
+ db *sql.DB
+}
+
+func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ db := s.db
+ switch r.URL.Path {
+ default:
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ case "/healthz":
+ ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
+ defer cancel()
+
+ err := s.db.PingContext(ctx)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("db down: %v", err), http.StatusFailedDependency)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ return
+ case "/quick-action":
+ // This is a short SELECT. Use the request context as the base of
+ // the context timeout.
+ ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
+ defer cancel()
+
+ id := 5
+ org := 10
+ var name string
+ err := db.QueryRowContext(ctx, `
+select
+ p.name
+from
+ people as p
+ join organization as o on p.organization = o.id
+where
+ p.id = :id
+ and o.id = :org
+;`,
+ sql.Named("id", id),
+ sql.Named("org", org),
+ ).Scan(&name)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ io.WriteString(w, name)
+ return
+ case "/long-action":
+ // This is a long SELECT. Use the request context as the base of
+ // the context timeout, but give it some time to finish. If
+ // the client cancels before the query is done the query will also
+ // be canceled.
+ ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
+ defer cancel()
+
+ var names []string
+ rows, err := db.QueryContext(ctx, "select p.name from people as p where p.active = true;")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ for rows.Next() {
+ var name string
+ err = rows.Scan(&name)
+ if err != nil {
+ break
+ }
+ names = append(names, name)
+ }
+ // Check for errors during rows "Close".
+ // This may be more important if multiple statements are executed
+ // in a single batch and rows were written as well as read.
+ if closeErr := rows.Close(); closeErr != nil {
+ http.Error(w, closeErr.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Check for row scan error.
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Check for errors during row iteration.
+ if err = rows.Err(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ json.NewEncoder(w).Encode(names)
+ return
+ case "/async-action":
+ // This action has side effects that we want to preserve
+ // even if the client cancels the HTTP request part way through.
+ // For this we do not use the http request context as a base for
+ // the timeout.
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ var orderRef = "ABC123"
+ tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
+ _, err = tx.ExecContext(ctx, "stored_proc_name", orderRef)
+
+ if err != nil {
+ tx.Rollback()
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ err = tx.Commit()
+ if err != nil {
+ http.Error(w, "action in unknown state, check state before attempting again", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+}
diff --git a/libgo/go/database/sql/fakedb_test.go b/libgo/go/database/sql/fakedb_test.go
index a21bae6..dcdd264 100644
--- a/libgo/go/database/sql/fakedb_test.go
+++ b/libgo/go/database/sql/fakedb_test.go
@@ -539,7 +539,7 @@ func (c *fakeConn) prepareCreate(stmt *fakeStmt, parts []string) (*fakeStmt, err
}
// parts are table|col=?,col2=val
-func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (*fakeStmt, error) {
+func (c *fakeConn) prepareInsert(ctx context.Context, stmt *fakeStmt, parts []string) (*fakeStmt, error) {
if len(parts) != 2 {
stmt.Close()
return nil, errf("invalid INSERT syntax with %d parts; want 2", len(parts))
@@ -574,6 +574,20 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (*fakeStmt, err
return nil, errf("invalid conversion to int32 from %q", value)
}
subsetVal = int64(i) // int64 is a subset type, but not int32
+ case "table": // For testing cursor reads.
+ c.skipDirtySession = true
+ vparts := strings.Split(value, "!")
+
+ substmt, err := c.PrepareContext(ctx, fmt.Sprintf("SELECT|%s|%s|", vparts[0], strings.Join(vparts[1:], ",")))
+ if err != nil {
+ return nil, err
+ }
+ cursor, err := (substmt.(driver.StmtQueryContext)).QueryContext(ctx, []driver.NamedValue{})
+ substmt.Close()
+ if err != nil {
+ return nil, err
+ }
+ subsetVal = cursor
default:
stmt.Close()
return nil, errf("unsupported conversion for pre-bound parameter %q to type %q", value, ctype)
@@ -658,11 +672,11 @@ func (c *fakeConn) PrepareContext(ctx context.Context, query string) (driver.Stm
case "CREATE":
stmt, err = c.prepareCreate(stmt, parts)
case "INSERT":
- stmt, err = c.prepareInsert(stmt, parts)
+ stmt, err = c.prepareInsert(ctx, stmt, parts)
case "NOSERT":
// Do all the prep-work like for an INSERT but don't actually insert the row.
// Used for some of the concurrent tests.
- stmt, err = c.prepareInsert(stmt, parts)
+ stmt, err = c.prepareInsert(ctx, stmt, parts)
default:
stmt.Close()
return nil, errf("unsupported command type %q", cmd)
diff --git a/libgo/go/database/sql/sql.go b/libgo/go/database/sql/sql.go
index 3617985..b0353ab 100644
--- a/libgo/go/database/sql/sql.go
+++ b/libgo/go/database/sql/sql.go
@@ -133,6 +133,7 @@ const (
LevelLinearizable
)
+// String returns the name of the transaction isolation level.
func (i IsolationLevel) String() string {
switch i {
case LevelDefault:
@@ -567,7 +568,6 @@ type finalCloser interface {
// addDep notes that x now depends on dep, and x's finalClose won't be
// called until all of x's dependencies are removed with removeDep.
func (db *DB) addDep(x finalCloser, dep interface{}) {
- //println(fmt.Sprintf("addDep(%T %p, %T %p)", x, x, dep, dep))
db.mu.Lock()
defer db.mu.Unlock()
db.addDepLocked(x, dep)
@@ -597,7 +597,6 @@ func (db *DB) removeDep(x finalCloser, dep interface{}) error {
}
func (db *DB) removeDepLocked(x finalCloser, dep interface{}) func() error {
- //println(fmt.Sprintf("removeDep(%T %p, %T %p)", x, x, dep, dep))
xdep, ok := db.dep[x]
if !ok {
@@ -1322,11 +1321,13 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
err: err,
}
return true
- } else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
- db.freeConn = append(db.freeConn, dc)
+ } else if err == nil && !db.closed {
+ if db.maxIdleConnsLocked() > len(db.freeConn) {
+ db.freeConn = append(db.freeConn, dc)
+ db.startCleanerLocked()
+ return true
+ }
db.maxIdleClosed++
- db.startCleanerLocked()
- return true
}
return false
}
@@ -2605,6 +2606,15 @@ type Rows struct {
lastcols []driver.Value
}
+// lasterrOrErrLocked returns either lasterr or the provided err.
+// rs.closemu must be read-locked.
+func (rs *Rows) lasterrOrErrLocked(err error) error {
+ if rs.lasterr != nil && rs.lasterr != io.EOF {
+ return rs.lasterr
+ }
+ return err
+}
+
func (rs *Rows) initContextClose(ctx, txctx context.Context) {
if ctx.Done() == nil && (txctx == nil || txctx.Done() == nil) {
return
@@ -2681,7 +2691,7 @@ func (rs *Rows) nextLocked() (doClose, ok bool) {
return false, true
}
-// NextResultSet prepares the next result set for reading. It returns true if
+// NextResultSet prepares the next result set for reading. It reports whether
// there is further result sets, or false if there is no further result set
// or if there is an error advancing to it. The Err method should be consulted
// to distinguish between the two cases.
@@ -2728,23 +2738,22 @@ func (rs *Rows) NextResultSet() bool {
func (rs *Rows) Err() error {
rs.closemu.RLock()
defer rs.closemu.RUnlock()
- if rs.lasterr == io.EOF {
- return nil
- }
- return rs.lasterr
+ return rs.lasterrOrErrLocked(nil)
}
+var errRowsClosed = errors.New("sql: Rows are closed")
+var errNoRows = errors.New("sql: no Rows available")
+
// Columns returns the column names.
-// Columns returns an error if the rows are closed, or if the rows
-// are from QueryRow and there was a deferred error.
+// Columns returns an error if the rows are closed.
func (rs *Rows) Columns() ([]string, error) {
rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
- return nil, errors.New("sql: Rows are closed")
+ return nil, rs.lasterrOrErrLocked(errRowsClosed)
}
if rs.rowsi == nil {
- return nil, errors.New("sql: no Rows available")
+ return nil, rs.lasterrOrErrLocked(errNoRows)
}
rs.dc.Lock()
defer rs.dc.Unlock()
@@ -2758,10 +2767,10 @@ func (rs *Rows) ColumnTypes() ([]*ColumnType, error) {
rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
- return nil, errors.New("sql: Rows are closed")
+ return nil, rs.lasterrOrErrLocked(errRowsClosed)
}
if rs.rowsi == nil {
- return nil, errors.New("sql: no Rows available")
+ return nil, rs.lasterrOrErrLocked(errNoRows)
}
rs.dc.Lock()
defer rs.dc.Unlock()
@@ -2812,7 +2821,7 @@ func (ci *ColumnType) ScanType() reflect.Type {
return ci.scanType
}
-// Nullable returns whether the column may be null.
+// Nullable reports whether the column may be null.
// If a driver does not support this property ok will be false.
func (ci *ColumnType) Nullable() (nullable, ok bool) {
return ci.nullable, ci.hasNullable
@@ -2873,6 +2882,7 @@ func rowsColumnInfoSetupConnLocked(rowsi driver.Rows) []*ColumnType {
// *float32, *float64
// *interface{}
// *RawBytes
+// *Rows (cursor value)
// any type implementing Scanner (see Scanner docs)
//
// In the most simple case, if the type of the value from the source
@@ -2909,6 +2919,11 @@ func rowsColumnInfoSetupConnLocked(rowsi driver.Rows) []*ColumnType {
//
// For scanning into *bool, the source may be true, false, 1, 0, or
// string inputs parseable by strconv.ParseBool.
+//
+// Scan can also convert a cursor returned from a query, such as
+// "select cursor(select * from my_table) from dual", into a
+// *Rows value that can itself be scanned from. The parent
+// select query will close any cursor *Rows if the parent *Rows is closed.
func (rs *Rows) Scan(dest ...interface{}) error {
rs.closemu.RLock()
@@ -2917,8 +2932,9 @@ func (rs *Rows) Scan(dest ...interface{}) error {
return rs.lasterr
}
if rs.closed {
+ err := rs.lasterrOrErrLocked(errRowsClosed)
rs.closemu.RUnlock()
- return errors.New("sql: Rows are closed")
+ return err
}
rs.closemu.RUnlock()
@@ -2929,7 +2945,7 @@ func (rs *Rows) Scan(dest ...interface{}) error {
return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
}
for i, sv := range rs.lastcols {
- err := convertAssign(dest[i], sv)
+ err := convertAssignRows(dest[i], sv, rs)
if err != nil {
return fmt.Errorf(`sql: Scan error on column index %d, name %q: %v`, i, rs.rowsi.Columns()[i], err)
}
diff --git a/libgo/go/database/sql/sql_test.go b/libgo/go/database/sql/sql_test.go
index f194744..64b9dfe 100644
--- a/libgo/go/database/sql/sql_test.go
+++ b/libgo/go/database/sql/sql_test.go
@@ -397,7 +397,7 @@ func TestQueryContextWait(t *testing.T) {
prepares0 := numPrepares(t, db)
// TODO(kardianos): convert this from using a timeout to using an explicit
- // cancel when the query signals that is is "executing" the query.
+ // cancel when the query signals that it is "executing" the query.
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
@@ -597,7 +597,7 @@ func TestPoolExhaustOnCancel(t *testing.T) {
state := 0
// waiter will be called for all queries, including
- // initial setup queries. The state is only assigned when no
+ // initial setup queries. The state is only assigned when
// no queries are made.
//
// Only allow the first batch of queries to finish once the
@@ -1338,6 +1338,52 @@ func TestConnQuery(t *testing.T) {
}
}
+func TestCursorFake(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+ defer cancel()
+
+ exec(t, db, "CREATE|peoplecursor|list=table")
+ exec(t, db, "INSERT|peoplecursor|list=people!name!age")
+
+ rows, err := db.QueryContext(ctx, `SELECT|peoplecursor|list|`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rows.Close()
+
+ if !rows.Next() {
+ t.Fatal("no rows")
+ }
+ var cursor = &Rows{}
+ err = rows.Scan(cursor)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cursor.Close()
+
+ const expectedRows = 3
+ var currentRow int64
+
+ var n int64
+ var s string
+ for cursor.Next() {
+ currentRow++
+ err = cursor.Scan(&s, &n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != currentRow {
+ t.Errorf("expected number(Age)=%d, got %d", currentRow, n)
+ }
+ }
+ if currentRow != expectedRows {
+ t.Errorf("expected %d rows, got %d rows", expectedRows, currentRow)
+ }
+}
+
func TestInvalidNilValues(t *testing.T) {
var date1 time.Time
var date2 int
@@ -3415,6 +3461,58 @@ func TestConnectionLeak(t *testing.T) {
wg.Wait()
}
+func TestStatsMaxIdleClosedZero(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+
+ db.SetMaxOpenConns(1)
+ db.SetMaxIdleConns(1)
+ db.SetConnMaxLifetime(0)
+
+ preMaxIdleClosed := db.Stats().MaxIdleClosed
+
+ for i := 0; i < 10; i++ {
+ rows, err := db.Query("SELECT|people|name|")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rows.Close()
+ }
+
+ st := db.Stats()
+ maxIdleClosed := st.MaxIdleClosed - preMaxIdleClosed
+ t.Logf("MaxIdleClosed: %d", maxIdleClosed)
+ if maxIdleClosed != 0 {
+ t.Fatal("expected 0 max idle closed conns, got: ", maxIdleClosed)
+ }
+}
+
+func TestStatsMaxIdleClosedTen(t *testing.T) {
+ db := newTestDB(t, "people")
+ defer closeDB(t, db)
+
+ db.SetMaxOpenConns(1)
+ db.SetMaxIdleConns(0)
+ db.SetConnMaxLifetime(0)
+
+ preMaxIdleClosed := db.Stats().MaxIdleClosed
+
+ for i := 0; i < 10; i++ {
+ rows, err := db.Query("SELECT|people|name|")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rows.Close()
+ }
+
+ st := db.Stats()
+ maxIdleClosed := st.MaxIdleClosed - preMaxIdleClosed
+ t.Logf("MaxIdleClosed: %d", maxIdleClosed)
+ if maxIdleClosed != 10 {
+ t.Fatal("expected 0 max idle closed conns, got: ", maxIdleClosed)
+ }
+}
+
type nvcDriver struct {
fakeDriver
skipNamedValueCheck bool