Repository: denisenkom/go-mssqldb
Pull request #677 (2021-07-14 19:29:15)

Support a few other cases in isProc
This PR fixes `isProc` which detects if a string contains nothing but an object name.

It may still be a long way from the real lexer.

I had to fix 2 tests:
- Proc name `"[proc\n1]"` is valid
- Table type name `"123.[Test]"` is not


codecov[bot]: # [Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#677](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (b197f83) into [master](https://codecov.io/gh/denisenkom/go-mssqldb/commit/a78da2aabd81789ce2188494d95efcdf97d940a3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (a78da2a) will **increase** coverage by `0.20%`.
> The diff coverage is `87.50%`.

[![Impacted file tree graph](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677/graphs/tree.svg?width=650&height=150&src=pr&token=MoMB2u5avp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master     #677      +/-   ##
==========================================
+ Coverage   70.19%   70.39%   +0.20%     
==========================================
  Files          23       23              
  Lines        5163     5179      +16     
==========================================
+ Hits         3624     3646      +22     
+ Misses       1310     1305       -5     
+ Partials      229      228       -1     
```


| [Impacted Files](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [mssql.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bXNzcWwuZ28=) | `87.61% <87.50%> (+0.34%)` | :arrow_up: |
| [tvp\_go19.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dHZwX2dvMTkuZ28=) | `93.02% <0.00%> (-1.40%)` | :arrow_down: |
| [token.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dG9rZW4uZ28=) | `60.60% <0.00%> (+0.12%)` | :arrow_up: |
| [tds.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dGRzLmdv) | `66.70% <0.00%> (+0.94%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [a78da2a...b197f83](https://codecov.io/gh/denisenkom/go-mssqldb/pull/677?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/bulkcopy.go b/bulkcopy.go
index d455327..bbae4e6 100644
--- a/bulkcopy.go
+++ b/bulkcopy.go
@@ -238,7 +238,7 @@ func (b *Bulk) Done() (rowcount int64, err error) {
 	reader := startReading(b.cn.sess, b.ctx, outputs{})
 	err = reader.iterateResponse()
 	if err != nil {
-		return 0, b.cn.checkBadConn(err)
+		return 0, b.cn.checkBadConn(err, false)
 	}
 
 	return reader.rowCount, nil
diff --git a/error.go b/error.go
index cb732b1..59a7a1a 100644
--- a/error.go
+++ b/error.go
@@ -1,6 +1,7 @@
 package mssql
 
 import (
+	"database/sql/driver"
 	"fmt"
 )
 
@@ -68,9 +69,53 @@ func streamErrorf(format string, v ...interface{}) StreamError {
 }
 
 func badStreamPanic(err error) {
-	panic(err)
+	panic(streamErrorf("%v", err))
 }
 
 func badStreamPanicf(format string, v ...interface{}) {
 	panic(streamErrorf(format, v...))
 }
+
+// ServerError is returned when the server got a fatal error
+// that aborts the process and severs the connection.
+//
+// To get the errors returned before the process was aborted,
+// unwrap this error or call errors.As with a pointer to an
+// mssql.Error variable.
+type ServerError struct {
+	sqlError Error
+}
+
+func (e ServerError) Error() string {
+	return "SQL Server had internal error"
+}
+
+func (e ServerError) Unwrap() error {
+	return e.sqlError
+}
+
+// RetryableError is returned when an error was caused by a bad
+// connection at the start of a query and can be safely retried
+// using database/sql's automatic retry logic.
+//
+// In many cases database/sql's retry logic will transparently
+// handle this error, the retried call will return successfully,
+// and you won't even see this error. However, you may see this
+// error if the retry logic cannot successfully handle the error.
+// In that case you can get the underlying error by calling this
+// error's UnWrap function.
+type RetryableError struct {
+	err error
+}
+
+func (r RetryableError) Error() string {
+	return r.err.Error()
+}
+
+func (r RetryableError) Unwrap() error {
+	return r.err
+}
+
+func (r RetryableError) Is(err error) bool {
+	return err == driver.ErrBadConn
+}
diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go
index 9a141e3..d0eddbd 100644
--- a/msdsn/conn_str.go
+++ b/msdsn/conn_str.go
@@ -61,6 +61,10 @@ type Config struct {
 	Workstation string
 	AppName     string
 
+	// If true disables database/sql's automatic retry of queries
+	// that start on bad connections.
+	DisableRetry bool
+
 	// Do not use the following.
 
 	DialTimeout time.Duration // DialTimeout defaults to 15s. Set negative to disable.
@@ -293,11 +297,23 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		var err error
 		p.FailOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
 		if err != nil {
-			f := "invalid tcp port '%v': %v"
+			f := "invalid failover port '%v': %v"
 			return p, params, fmt.Errorf(f, failOverPort, err.Error())
 		}
 	}
 
+	disableRetry, ok := params["disableretry"]
+	if ok {
+		var err error
+		p.DisableRetry, err = strconv.ParseBool(disableRetry)
+		if err != nil {
+			f := "invalid disableRetry '%s': %s"
+			return p, params, fmt.Errorf(f, disableRetry, err.Error())
+		}
+	} else {
+		p.DisableRetry = disableRetryDefault
+	}
+
 	return p, params, nil
 }
 
@@ -315,6 +331,7 @@ func (p Config) URL() *url.URL {
 	if p.Port > 0 {
 		host = fmt.Sprintf("%s:%d", p.Host, p.Port)
 	}
+	q.Add("disableRetry", fmt.Sprintf("%t", p.DisableRetry))
 	res := url.URL{
 		Scheme: "sqlserver",
 		Host:   host,
diff --git a/msdsn/conn_str_go118.go b/msdsn/conn_str_go118.go
new file mode 100644
index 0000000..b1a1d1d
--- /dev/null
+++ b/msdsn/conn_str_go118.go
@@ -0,0 +1,9 @@
+// +build go1.18
+
+package msdsn
+
+// disableRetryDefault is false for Go versions 1.18 and higher. This matches
+// the behavior requested in issue #586. A query that fails at the start due to
+// a bad connection is automatically retried. An error is returned only if the
+// query fails all of its retries.
+const disableRetryDefault bool = false
diff --git a/msdsn/conn_str_go118pre.go b/msdsn/conn_str_go118pre.go
new file mode 100644
index 0000000..d3ce195
--- /dev/null
+++ b/msdsn/conn_str_go118pre.go
@@ -0,0 +1,9 @@
+// +build !go1.18
+
+package msdsn
+
+// disableRetryDefault is true for versions of Go less than 1.18. This matches
+// the behavior requested in issue #275. A query that fails at the start due to
+// a bad connection is not retried. Instead, the detailed error is immediately
+// returned to the caller.
+const disableRetryDefault bool = true
diff --git a/mssql.go b/mssql.go
index 6ba4690..550cb91 100644
--- a/mssql.go
+++ b/mssql.go
@@ -186,23 +186,19 @@ func (c *Conn) IsValid() bool {
 	return c.connectionGood
 }
 
-func (c *Conn) checkBadConn(err error) error {
-	// this is a hack to address Issue #275
-	// we set connectionGood flag to false if
-	// error indicates that connection is not usable
-	// but we return actual error instead of ErrBadConn
-	// this will cause connection to stay in a pool
-	// but next request to this connection will return ErrBadConn
-
-	// it might be possible to revise this hack after
-	// https://github.com/golang/go/issues/20807
-	// is implemented
+// checkBadConn marks the connection as bad based on the characteristics
+// of the supplied error. Bad connections will be dropped from the connection
+// pool rather than reused.
+//
+// If bad connection retry is enabled and the error + connection state permits
+// retrying, checkBadConn will return a RetryableError that allows database/sql
+// to automatically retry the query with another connection.
+func (c *Conn) checkBadConn(err error, mayRetry bool) error {
 	switch err {
 	case nil:
 		return nil
 	case io.EOF:
 		c.connectionGood = false
-		return driver.ErrBadConn
 	case driver.ErrBadConn:
 		// It is an internal programming error if driver.ErrBadConn
 		// is ever passed to this function. driver.ErrBadConn should
@@ -214,13 +210,17 @@ func (c *Conn) checkBadConn(err error) error {
 	switch err.(type) {
 	case net.Error:
 		c.connectionGood = false
-		return err
 	case StreamError:
 		c.connectionGood = false
-		return err
-	default:
-		return err
+	case ServerError:
+		c.connectionGood = false
+	}
+
+	if !c.connectionGood && mayRetry && !c.connector.params.DisableRetry {
+		return newRetryableError(err)
 	}
+
+	return err
 }
 
 func (c *Conn) clearOuts() {
@@ -234,7 +234,7 @@ func (c *Conn) simpleProcessResp(ctx context.Context) error {
 	var resultError error
 	err := reader.iterateResponse()
 	if err != nil {
-		return c.checkBadConn(err)
+		return c.checkBadConn(err, false)
 	}
 	return resultError
 }
@@ -244,7 +244,7 @@ func (c *Conn) Commit() error {
 		return driver.ErrBadConn
 	}
 	if err := c.sendCommitRequest(); err != nil {
-		return c.checkBadConn(err)
+		return c.checkBadConn(err, true)
 	}
 	return c.simpleProcessResp(c.transactionCtx)
 }
@@ -271,7 +271,7 @@ func (c *Conn) Rollback() error {
 		return driver.ErrBadConn
 	}
 	if err := c.sendRollbackRequest(); err != nil {
-		return c.checkBadConn(err)
+		return c.checkBadConn(err, true)
 	}
 	return c.simpleProcessResp(c.transactionCtx)
 }
@@ -303,7 +303,7 @@ func (c *Conn) begin(ctx context.Context, tdsIsolation isoLevel) (tx driver.Tx,
 	}
 	err = c.sendBeginRequest(ctx, tdsIsolation)
 	if err != nil {
-		return nil, c.checkBadConn(err)
+		return nil, c.checkBadConn(err, true)
 	}
 	tx, err = c.processBeginResponse(ctx)
 	if err != nil {
@@ -470,7 +470,8 @@ func (s *Stmt) sendQuery(args []namedValue) (err error) {
 
 	reset := conn.resetSession
 	conn.resetSession = false
-	if len(args) == 0 {
+	isProc := isProc(s.query)
+	if len(args) == 0 && !isProc {
 		if err = sendSqlBatch72(conn.sess.buf, s.query, headers, reset); err != nil {
 			if conn.sess.logFlags&logErrors != 0 {
 				conn.sess.log.Printf("Failed to send SqlBatch with %v", err)
@@ -481,7 +482,7 @@ func (s *Stmt) sendQuery(args []namedValue) (err error) {
 	} else {
 		proc := sp_ExecuteSql
 		var params []param
-		if isProc(s.query) {
+		if isProc {
 			proc.name = s.query
 			params, _, err = s.makeRPCParams(args, true)
 			if err != nil {
@@ -523,30 +524,38 @@ func isProc(s string) bool {
 	for _, r := range s {
 		rPrev = rn1
 		rn1 = r
-		switch r {
-		// No newlines or string sequences.
-		case '\n', '\r', '\'', ';':
-			return false
+		if st != escaped {
+			switch r {
+			// No newlines or string sequences.
+			case '\n', '\r', '\'', ';':
+				return false
+			}
 		}
 		switch st {
 		case outside:
 			switch {
-			case unicode.IsSpace(r):
-				return false
 			case r == '[':
 				st = escaped
-				continue
 			case r == ']' && rPrev == ']':
 				st = escaped
-				continue
 			case unicode.IsLetter(r):
 				st = text
+			case r == '_':
+				st = text
+			case r == '#':
+				st = text
+			case r == '.':
+			default:
+				return false
 			}
 		case text:
 			switch {
 			case r == '.':
 				st = outside
-				continue
+			case r == '[':
+				return false
+			case r == '(':
+				return false
 			case unicode.IsSpace(r):
 				return false
 			}
@@ -554,7 +563,6 @@ func isProc(s string) bool {
 			switch {
 			case r == ']':
 				st = outside
-				continue
 			}
 		}
 	}
@@ -620,7 +628,7 @@ func (s *Stmt) queryContext(ctx context.Context, args []namedValue) (rows driver
 		return nil, driver.ErrBadConn
 	}
 	if err = s.sendQuery(args); err != nil {
-		return nil, s.c.checkBadConn(err)
+		return nil, s.c.checkBadConn(err, true)
 	}
 	return s.processQueryResponse(ctx)
 }
@@ -653,7 +661,7 @@ loop:
 					if token.isError() {
 						// need to cleanup cancellable context
 						cancel()
-						return nil, s.c.checkBadConn(token.getError())
+						return nil, s.c.checkBadConn(token.getError(), false)
 					}
 				case ReturnStatus:
 					if reader.outs.returnStatus != nil {
@@ -664,7 +672,7 @@ loop:
 		} else {
 			// need to cleanup cancellable context
 			cancel()
-			return nil, s.c.checkBadConn(err)
+			return nil, s.c.checkBadConn(err, false)
 		}
 	}
 	res = &Rows{stmt: s, reader: reader, cols: cols, cancel: cancel}
@@ -682,7 +690,7 @@ func (s *Stmt) exec(ctx context.Context, args []namedValue) (res driver.Result,
 		return nil, driver.ErrBadConn
 	}
 	if err = s.sendQuery(args); err != nil {
-		return nil, s.c.checkBadConn(err)
+		return nil, s.c.checkBadConn(err, true)
 	}
 	if res, err = s.processExec(ctx); err != nil {
 		return nil, err
@@ -695,7 +703,7 @@ func (s *Stmt) processExec(ctx context.Context) (res driver.Result, err error) {
 	s.c.clearOuts()
 	err = reader.iterateResponse()
 	if err != nil {
-		return nil, s.c.checkBadConn(err)
+		return nil, s.c.checkBadConn(err, false)
 	}
 	return &Result{s.c, reader.rowCount}, nil
 }
@@ -765,7 +773,7 @@ func (rc *Rows) Next(dest []driver.Value) error {
 					return nil
 				case doneStruct:
 					if tokdata.isError() {
-						return rc.stmt.c.checkBadConn(tokdata.getError())
+						return rc.stmt.c.checkBadConn(tokdata.getError(), false)
 					}
 				case ReturnStatus:
 					if rc.reader.outs.returnStatus != nil {
@@ -775,7 +783,7 @@ func (rc *Rows) Next(dest []driver.Value) error {
 			}
 
 		} else {
-			return rc.stmt.c.checkBadConn(err)
+			return rc.stmt.c.checkBadConn(err, false)
 		}
 	}
 }
diff --git a/mssql_go118.go b/mssql_go118.go
new file mode 100644
index 0000000..9b8014b
--- /dev/null
+++ b/mssql_go118.go
@@ -0,0 +1,14 @@
+// +build go1.18
+
+package mssql
+
+// newRetryableError returns an error that allows the database/sql package
+// to automatically retry the failed query. Versions of Go 1.18 and higher
+// use errors.Is to determine whether or not a failed query can be retried.
+// Therefore, we wrap the underlying error in a RetryableError that both
+// implements errors.Is for automatic retry and maintains the error details.
+func newRetryableError(err error) error {
+	return RetryableError{
+		err: err,
+	}
+}
diff --git a/mssql_go118pre.go b/mssql_go118pre.go
new file mode 100644
index 0000000..ac83e1d
--- /dev/null
+++ b/mssql_go118pre.go
@@ -0,0 +1,17 @@
+// +build !go1.18
+
+package mssql
+
+import (
+	"database/sql/driver"
+)
+
+// newRetryableError returns an error that allows the database/sql package
+// to automatically retry the failed query. Versions of Go lower than 1.18
+// compare directly to the sentinel error driver.ErrBadConn to determine
+// whether or not a failed query can be retried. Therefore, we replace the
+// actual error with driver.ErrBadConn, enabling retry but losing the error
+// details.
+func newRetryableError(err error) error {
+	return driver.ErrBadConn
+}
diff --git a/tds.go b/tds.go
index c7253ba..30c72a7 100644
--- a/tds.go
+++ b/tds.go
@@ -1190,6 +1190,8 @@ initiate_connection:
 	// packet exchanges to complete the login sequence.
 	for loginAck := false; !loginAck; {
 		reader := startReading(&sess, ctx, outputs{})
+		// don't send attention or wait for cancel confirmation during login
+		reader.noAttn = true
 
 		for {
 			tok, err := reader.nextToken()
diff --git a/token.go b/token.go
index 95b3744..6000fb9 100644
--- a/token.go
+++ b/token.go
@@ -699,7 +699,7 @@ func processSingleResponse(sess *tdsSession, ch chan tokenStruct, outs outputs)
 				sess.log.Printf("got DONE or DONEPROC status=%d", done.Status)
 			}
 			if done.Status&doneSrvError != 0 {
-				ch <- errors.New("SQL Server had internal error")
+				ch <- ServerError{done.getError()}
 				return
 			}
 			if sess.logFlags&logRows != 0 && done.Status&doneCount != 0 {
@@ -765,6 +765,8 @@ type tokenProcessor struct {
 	lastRow    []interface{}
 	rowCount   int64
 	firstError error
+	// whether to skip sending attention when ctx is done
+	noAttn bool
 }
 
 func startReading(sess *tdsSession, ctx context.Context, outs outputs) *tokenProcessor {
@@ -848,6 +850,9 @@ func (t tokenProcessor) nextToken() (tokenStruct, error) {
 			return nil, nil
 		}
 	case <-t.ctx.Done():
+		if t.noAttn {
+			return nil, t.ctx.Err()
+		}
 		if err := sendAttention(t.sess.buf); err != nil {
 			// unable to send attention, current connection is bad
 			// notify caller and close channel


Test patch:
diff --git a/error_test.go b/error_test.go
new file mode 100644
index 0000000..463d9fa
--- /dev/null
+++ b/error_test.go
@@ -0,0 +1,87 @@
+package mssql
+
+import (
+	"database/sql/driver"
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestServerError(t *testing.T) {
+
+	originalErr := Error{Message: "underlying error"}
+	sererErr := ServerError{sqlError: originalErr}
+
+	// Verify that error message is backwards compatible
+	oldMessage := "SQL Server had internal error"
+	if newMessage := sererErr.Error(); newMessage != oldMessage {
+		t.Fatalf("ServerError returned incompatible error message. Got '%s', wanted '%s'", newMessage, oldMessage)
+	}
+
+	// Verify that the underlying error is preserved
+	unwrappedErr := sererErr.Unwrap()
+	if underlyingErr, ok := unwrappedErr.(Error); !ok || underlyingErr.Message != originalErr.Message {
+		t.Fatalf("ServerError did not preserve wrapped error. Got '%+v', wanted '%+v'", unwrappedErr, originalErr)
+	}
+}
+
+func TestRetryableError(t *testing.T) {
+
+	originalErr := driver.ErrBadConn
+	retryableErr := RetryableError{err: originalErr}
+
+	// Verify that the error message matches the original error's
+	origMessage := originalErr.Error()
+	if wrappedMessage := retryableErr.Error(); wrappedMessage != origMessage {
+		t.Fatalf("RetryableError returned incorrect error message. Got '%s', wanted '%s'", wrappedMessage, origMessage)
+	}
+
+	// Verify that the underlying error is preserved
+	unwrappedErr := retryableErr.Unwrap()
+	if unwrappedErr != originalErr {
+		t.Fatalf("RetryableError did not preserve wrapped error. Got '%+v', wanted '%+v'", unwrappedErr, originalErr)
+	}
+
+	// Verify that underlying error is correctly recognized
+	if !retryableErr.Is(driver.ErrBadConn) {
+		t.Fatalf("RetryableError wrapping driver.ErrBadConn does not report it is a driver.ErrBadConn error")
+	}
+
+}
+
+func TestBadStreamPanic(t *testing.T) {
+
+	errMsg := "test error XYZ"
+	err := fmt.Errorf(errMsg)
+
+	defer func() {
+		r := recover()
+		if e, ok := r.(error); !ok || !strings.HasSuffix(e.Error(), errMsg) {
+			t.Fatalf("unexpected error recovered from panic: "+
+				"got error = '%+v', wanted error to end with '%s'", e, errMsg)
+		}
+	}()
+
+	badStreamPanic(err)
+
+	t.Fatalf("badStreamPanic did not panic as expected when passed %+v", err)
+}
+
+func TestBadStreamPanicf(t *testing.T) {
+
+	errfmt := "the error is '%s'"
+	errMsg := "test error XYZ"
+	expectedMsg := fmt.Sprintf(errfmt, errMsg)
+
+	defer func() {
+		r := recover()
+		if e, ok := r.(error); !ok || !strings.HasSuffix(e.Error(), expectedMsg) {
+			t.Fatalf("unexpected error recovered from panic: "+
+				"got error = '%+v', wanted error to end with '%s'", e, expectedMsg)
+		}
+	}()
+
+	badStreamPanicf(errfmt, errMsg)
+
+	t.Fatalf("badStreamPanicf did not panic as expected when passed %s", expectedMsg)
+}
diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go
index 0b22ae5..594b5b3 100644
--- a/msdsn/conn_str_test.go
+++ b/msdsn/conn_str_test.go
@@ -18,6 +18,7 @@ func TestInvalidConnectionString(t *testing.T) {
 		"trustservercertificate=invalid",
 		"failoverport=invalid",
 		"applicationintent=ReadOnly",
+		"disableretry=invalid",
 
 		// ODBC mode
 		"odbc:password={",
@@ -70,6 +71,11 @@ func TestValidConnectionString(t *testing.T) {
 		{"log=64;packet size=300", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 512 }},
 		{"log=64;packet size=8192", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 8192 }},
 		{"log=64;packet size=48000", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 32767 }},
+		{"disableretry=true", func(p Config) bool { return p.DisableRetry }},
+		{"disableretry=false", func(p Config) bool { return !p.DisableRetry }},
+		{"disableretry=1", func(p Config) bool { return p.DisableRetry }},
+		{"disableretry=0", func(p Config) bool { return !p.DisableRetry }},
+		{"", func(p Config) bool { return p.DisableRetry == disableRetryDefault }},
 
 		// those are supported currently, but maybe should not be
 		{"someparam", func(p Config) bool { return true }},
@@ -121,6 +127,12 @@ func TestValidConnectionString(t *testing.T) {
 		{"odbc:password={value}  ", func(p Config) bool {
 			return p.Password == "value"
 		}},
+		{"odbc:server=somehost;user id=someuser;password=somepass;disableretry=true", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "somepass" && p.DisableRetry
+		}},
+		{"odbc:server=somehost;user id=someuser;password=somepass; disableretry =  1 ", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "somepass" && p.DisableRetry
+		}},
 
 		// URL mode
 		{"sqlserver://somehost?connection+timeout=30", func(p Config) bool {
@@ -141,6 +153,12 @@ func TestValidConnectionString(t *testing.T) {
 		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434/someinstance?connection+timeout=30", func(p Config) bool {
 			return p.Host == "somehost" && p.Port == 1434 && p.Instance == "someinstance" && p.User == "someuser" && p.Password == "foo:/\\!~@;bar" && p.ConnTimeout == 30*time.Second
 		}},
+		{"sqlserver://someuser@somehost?disableretry=true", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "" && p.DisableRetry
+		}},
+		{"sqlserver://someuser@somehost?connection+timeout=30&disableretry=1", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "" && p.ConnTimeout == 30*time.Second && p.DisableRetry
+		}},
 	}
 	for _, ts := range connStrings {
 		p, _, err := Parse(ts.connStr)
diff --git a/mssql_test.go b/mssql_test.go
index adc5dcd..da6d918 100644
--- a/mssql_test.go
+++ b/mssql_test.go
@@ -3,7 +3,14 @@ package mssql
 import (
 	"context"
 	"database/sql"
+	"database/sql/driver"
+	"fmt"
+	"io"
+	"net"
+	"reflect"
 	"testing"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 func TestBadOpen(t *testing.T) {
@@ -23,12 +30,22 @@ func TestIsProc(t *testing.T) {
 		{"select 1;", false},
 		{"select 1", false},
 		{"[proc 1]", true},
-		{"[proc\n1]", false},
+		{"[proc\n1]", true},
 		{"schema.name", true},
 		{"[schema].[name]", true},
 		{"schema.[name]", true},
 		{"[schema].name", true},
 		{"schema.[proc name]", true},
+		{"db.schema.[proc name]", true},
+		{"db..[proc name]", true},
+		{"#temp_@_proc", true},
+		{"_temp.[_proc]", true},
+		{"raiserror(13000,1,1)", false},
+		{"select*from(@table)", false},
+		{"select[A]]]+1from[B]", false},
+		{"--proc", false},
+		{"[proc;]", true},
+		{" proc", false},
 	}
 
 	for _, item := range list {
@@ -69,3 +86,155 @@ func TestConvertIsolationLevel(t *testing.T) {
 		t.Fatal("must fail but it didn't")
 	}
 }
+
+// equalErrors is a helper function that compares two errors
+// by comparing their nilness, underlying type, and Error messages
+func equalErrors(e1 error, e2 error) bool {
+	if e1 == nil && e2 == nil {
+		return true
+	}
+	if e1 == nil && e2 != nil || e1 != nil && e2 == nil {
+		return false
+	}
+	return reflect.TypeOf(e1) == reflect.TypeOf(e2) &&
+		e1.Error() == e2.Error()
+}
+
+// TestCheckBadConn verifies that different combinations of
+// configuration, inputs and errors result in the proper output
+// error and connection state.
+func TestCheckBadConn(t *testing.T) {
+
+	netErr := &net.OpError{Err: fmt.Errorf("fake net.Error")}
+	streamErr := StreamError{Message: "fake StreamError"}
+	serverErr := ServerError{sqlError: Error{Message: "fake ServerError"}}
+	goodConnErr := fmt.Errorf("fake error that leaves connection good")
+
+	testInputs := []struct {
+		err              error
+		mayRetry         bool
+		disableRetry     bool
+		expectedErr      error
+		expectedConnGood bool
+	}{
+		{nil, false, false, nil, true},
+		{nil, true, false, nil, true},
+		{nil, false, true, nil, true},
+		{nil, true, true, nil, true},
+		{io.EOF, false, false, io.EOF, false},
+		{io.EOF, true, false, newRetryableError(io.EOF), false},
+		{io.EOF, false, true, io.EOF, false},
+		{io.EOF, true, true, io.EOF, false},
+		{netErr, false, false, netErr, false},
+		{netErr, true, false, newRetryableError(netErr), false},
+		{netErr, false, true, netErr, false},
+		{netErr, true, true, netErr, false},
+		{streamErr, false, false, streamErr, false},
+		{streamErr, true, false, newRetryableError(streamErr), false},
+		{streamErr, false, true, streamErr, false},
+		{streamErr, true, true, streamErr, false},
+		{serverErr, false, false, serverErr, false},
+		{serverErr, true, false, newRetryableError(serverErr), false},
+		{serverErr, false, true, serverErr, false},
+		{serverErr, true, true, serverErr, false},
+		{goodConnErr, false, false, goodConnErr, true},
+		{goodConnErr, true, false, goodConnErr, true},
+		{goodConnErr, false, true, goodConnErr, true},
+		{goodConnErr, true, true, goodConnErr, true},
+	}
+
+	c := Conn{connector: &Connector{params: msdsn.Config{}}}
+
+	for _, ti := range testInputs {
+		c.connectionGood = true
+		c.connector.params.DisableRetry = ti.disableRetry
+		actualErr := c.checkBadConn(ti.err, ti.mayRetry)
+		if !equalErrors(actualErr, ti.expectedErr) ||
+			c.connectionGood != ti.expectedConnGood {
+			t.Fatalf("checkBadConn returned unexpected result for input err = '%+v', mayRetry = '%t', disableRetry = '%t': "+
+				"got output err = '%+v', connectionGood = '%t', "+
+				"wanted output err = '%+v', connectionGood = '%t'",
+				ti.err, ti.mayRetry, ti.disableRetry, actualErr, c.connectionGood, ti.expectedErr, ti.expectedConnGood)
+		}
+	}
+
+	// This must be the final test in this function, because we expect it to panic
+	defer func() { recover() }()
+	c.checkBadConn(driver.ErrBadConn, true)
+	t.Fatalf("checkBadConn did not panic as expected when passed driverErrBadConn")
+}
+
+// TestBadConnRejection verifies that database operations that start
+// with a bad connection fail with the sentinel error driver.ErrBadConn.
+// That instructs the database/sql connection pool logic to discard the
+// bad connection and, if appropriate, attempt to retry the operation
+// with another connection.
+func TestBadConnRejection(t *testing.T) {
+
+	c := Conn{connectionGood: false}
+
+	if _, err := c.Begin(); err != driver.ErrBadConn {
+		t.Fatalf("Begin did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if err := c.Commit(); err != driver.ErrBadConn {
+		t.Fatalf("Commit did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if err := c.Rollback(); err != driver.ErrBadConn {
+		t.Fatalf("Rollback did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := c.Prepare("query"); err != driver.ErrBadConn {
+		t.Fatalf("Prepare did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := c.PrepareContext(context.Background(), "query"); err != driver.ErrBadConn {
+		t.Fatalf("PrepareContext did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if err := c.Ping(context.Background()); err != driver.ErrBadConn {
+		t.Fatalf("Ping did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := c.BeginTx(context.Background(), driver.TxOptions{}); err != driver.ErrBadConn {
+		t.Fatalf("BeginTx did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	s := &Stmt{c: &c}
+
+	if _, err := s.Query(nil); err != driver.ErrBadConn {
+		t.Fatalf("Query did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := s.QueryContext(context.Background(), nil); err != driver.ErrBadConn {
+		t.Fatalf("QueryContext did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := s.Exec(nil); err != driver.ErrBadConn {
+		t.Fatalf("Exec did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	if _, err := s.ExecContext(context.Background(), nil); err != driver.ErrBadConn {
+		t.Fatalf("ExecContext did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+	r := &Rows{stmt: s}
+
+	if err := r.Next(nil); err != driver.ErrBadConn {
+		t.Fatalf("Next did not fail on bad connection: "+
+			"got err = '%+v', wanted err = '%+v'", err, driver.ErrBadConn)
+	}
+
+}
diff --git a/queries_test.go b/queries_test.go
index 78a7dad..5711787 100644
--- a/queries_test.go
+++ b/queries_test.go
@@ -1054,16 +1054,15 @@ func TestSetLanguage(t *testing.T) {
 	conn := open(t)
 	defer conn.Close()
 
-	_, err := conn.Exec("set language russian")
-	if err != nil {
-		t.Errorf("Query failed with unexpected error %s", err)
-	}
-
-	row := conn.QueryRow("select cast(getdate() as varchar(50))")
+	// sp_reset_connection is called when reusing a pooled connection
+	// which resets all SET values to defaults. Thus we can only
+	// validate the language change within one particular connection's usage
+	row := conn.QueryRowContext(context.TODO(), `set language Russian
+	select cast(N'июл 19 2021  8:12PM' as datetime)`)
 	var val interface{}
-	err = row.Scan(&val)
+	err := row.Scan(&val)
 	if err != nil {
-		t.Errorf("Query failed with unexpected error %s", err)
+		t.Errorf("datetime query failed with unexpected error %s", err)
 	}
 	t.Log("Returned value", val)
 }
@@ -1936,11 +1935,13 @@ func TestQueryCancelHighLevel(t *testing.T) {
 	defer conn.Close()
 
 	ctx, cancel := context.WithCancel(context.Background())
+	latency, _ := getLatency(t)
+
 	go func() {
-		time.Sleep(200 * time.Millisecond)
+		time.Sleep(latency + 500*time.Millisecond)
 		cancel()
 	}()
-	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:03'")
+	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:20'")
 	if err != context.Canceled {
 		t.Errorf("ExecContext expected to fail with Cancelled but it returned %v", err)
 	}
@@ -1954,12 +1955,46 @@ func TestQueryCancelHighLevel(t *testing.T) {
 	}
 }
 
+// returns the minimum time needed to connect to the server
+// Minimum return value is 1 ms, max is 2k ms
+func getLatency(t *testing.T) (latency time.Duration, increment time.Duration) {
+	latency = 1 * time.Millisecond
+	connstr := makeConnStr(t)
+	// .Host may not have a port
+	host := connstr.Hostname()
+	port := connstr.Port()
+	increment = 50 * time.Millisecond
+	if host == "." || host == "localhost" {
+		increment = 1 * time.Millisecond
+	}
+	if port == "" {
+		port = "1433"
+	}
+	for latency < 2000*time.Millisecond {
+		t.Logf("Dialing host %s with timeout %s", host, latency)
+		_, err := net.DialTimeout("tcp", host+":"+port, latency)
+		if err == nil {
+			return latency, increment
+		}
+		if oe, ok := err.(*net.OpError); ok {
+			if !oe.Timeout() {
+				t.Logf("Got non-timeout error %s", oe.Error())
+				return latency, increment
+			}
+			latency += increment
+		}
+	}
+	return latency, increment
+}
+
 func TestQueryTimeout(t *testing.T) {
 	conn := open(t)
 	defer conn.Close()
-	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
+	// 2 seconds should be enough time to complete the login
+	latency, _ := getLatency(t)
+	ctx, cancel := context.WithTimeout(context.Background(), latency+2000*time.Millisecond)
 	defer cancel()
-	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:03'")
+	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:20'")
 	if err != context.DeadlineExceeded {
 		t.Errorf("ExecContext expected to fail with DeadlineExceeded but it returned %v", err)
 	}
@@ -1973,6 +2008,34 @@ func TestQueryTimeout(t *testing.T) {
 	}
 }
 
+// Regression test for #679
+func TestLoginTimeout(t *testing.T) {
+	conn := open(t)
+	defer conn.Close()
+	// Try to timeout during the login using a small delta from raw connect time
+	// In environments where latency is really low this degenerates into TestQueryTimeout
+	latency, increment := getLatency(t)
+
+	ctx, cancel := context.WithTimeout(context.Background(), latency+(2*increment))
+	defer cancel()
+	_, err := conn.ExecContext(ctx, "waitfor delay '00:00:03'")
+	t.Log("Got error ", err)
+	if oe, ok := err.(*net.OpError); ok {
+		if !oe.Timeout() {
+			t.Fatalf("Got non-timeout error %s", oe.Error())
+		}
+	} else if err != context.DeadlineExceeded {
+		t.Fatalf("wrong kind of error for login or query timeout: %+v", err)
+	}
+
+	// connection should be usable after timeout
+	row := conn.QueryRow("select 1")
+	var val int64
+	err = row.Scan(&val)
+	if err != nil {
+		t.Fatal("Scan failed with", err)
+	}
+}
 func TestDriverParams(t *testing.T) {
 	checkConnStr(t)
 	SetLogger(testLogger{t})
@@ -2206,6 +2269,7 @@ func TestDisconnect2(t *testing.T) {
 	}
 	checkConnStr(t)
 	SetLogger(testLogger{t})
+	latency, _ := getLatency(t)
 
 	// Revert to the normal dialer after the test is done.
 	normalCreateDialer := createDialer
@@ -2220,7 +2284,7 @@ func TestDisconnect2(t *testing.T) {
 
 	go func() {
 		waitDisrupt := make(chan struct{})
-		ctx, cancel = context.WithTimeout(ctx, time.Second*2)
+		ctx, cancel = context.WithTimeout(ctx, latency+time.Second*2)
 		defer cancel()
 
 		createDialer = func(p *msdsn.Config) Dialer {
@@ -2266,6 +2330,253 @@ func TestDisconnect2(t *testing.T) {
 	}
 }
 
+// TestDisconnect3 verifies the bad connection behavior defined in
+// response to issue #275. In this mode a query that starts on a bad
+// connection should always fail immediately, returning a detailed
+// error message.
+func TestDisconnect3(t *testing.T) {
+	if testing.Short() {
+		t.Skip("short")
+	}
+	checkConnStr(t)
+	SetLogger(testLogger{t})
+
+	// Revert to the normal dialer creation function after the test is done.
+	normalCreateDialer := createDialer
+	defer func() {
+		createDialer = normalCreateDialer
+	}()
+
+	// Implement a dialer creation function that returns interruptable dialers.
+	// These dialers make connections that we can interrupt to simulate breakage.
+	// The driver may call our dialer creation function multiple times to create
+	// multiple independent dialers, so we must signal all of them to ensure that
+	// the dialer controlling our test connection interrupts it.
+	var wg sync.WaitGroup
+	var mu sync.Mutex
+	makeInterruptDialers := true
+	disconnect := make(chan bool)
+	createDialer = func(p *msdsn.Config) Dialer {
+		nd := netDialer{&net.Dialer{Timeout: p.DialTimeout, KeepAlive: p.KeepAlive}}
+		mu.Lock()
+		defer mu.Unlock()
+		if makeInterruptDialers {
+			di := &dialerInterrupt{nd: nd}
+			wg.Add(1)
+			go func() {
+				// Wait for signal, then break connection in both directions
+				<-disconnect
+				di.Interrupt(true)
+				di.Interrupt(false)
+				wg.Done()
+			}()
+			return di
+		}
+		return nd
+	}
+
+	// Disable retry to provide behavior requested in #275
+	cp := testConnParams(t)
+	cp.DisableRetry = true
+	connStr := cp.URL().String()
+
+	// Open a connection pool that holds a single connection to make sure all
+	// queries run on the same connection.
+	db, err := sql.Open("sqlserver", connStr)
+	if err != nil {
+		t.Log(err)
+	}
+	defer db.Close()
+	db.SetMaxIdleConns(1)
+	db.SetMaxOpenConns(1)
+
+	// Demonstrate connection is good the first time we use it
+	result := "<none>"
+	desiredResult := "before disconnect"
+	row := db.QueryRow(fmt.Sprintf("select '%s'", desiredResult))
+	if err = row.Scan(&result); err != nil || result != desiredResult {
+		t.Fatalf("Connection bad on first use. "+
+			"Got result = '%s', err = '%v',  result = '%s', err = '%v'",
+			result, err, desiredResult, nil)
+	}
+
+	// Stop making interuptable dialers, because we have already established
+	// our interruptable test connection, then signal and wait for our test
+	// connection to be broken
+	mu.Lock()
+	makeInterruptDialers = false
+	mu.Unlock()
+	close(disconnect)
+	wg.Wait()
+
+	// Broken connection should cause immediate and final failure
+	result = "<none>"
+	desiredErr := "failed to send SQL Batch: disconnect"
+	row = db.QueryRow("select 'after disconnect'")
+	if err = row.Scan(&result); err == nil || err.Error() != desiredErr {
+		t.Fatalf("Connection did not fail as expected. "+
+			"Got result = '%s', err = '%v', wanted result = '%s', err = '%v'",
+			result, err, "<none>", desiredErr)
+	}
+}
+
+// TestDisconnect4 verifies the bad connection behavior defined in
+// response to issue #586. In this mode a query that starts on a bad
+// connection should fail with an error that allows the automatic retry
+// logic within database/sql to retry the query with a new connection,
+// which then succeeds. From the perspective of the consuming application,
+// there is no error.
+func TestDisconnect4(t *testing.T) {
+	if testing.Short() {
+		t.Skip("short")
+	}
+	checkConnStr(t)
+	SetLogger(testLogger{t})
+
+	// Revert to the normal dialer creation function after the test is done.
+	normalCreateDialer := createDialer
+	defer func() {
+		createDialer = normalCreateDialer
+	}()
+
+	// Implement a dialer creation function that returns interruptable dialers.
+	// These dialers make connections that we can interrupt to simulate breakage.
+	// The driver may call our dialer creation function multiple times to create
+	// multiple independent dialers, so we must signal all of them to ensure that
+	// the dialer controlling our test connection interrupts it.
+	var wg sync.WaitGroup
+	var mu sync.Mutex
+	makeInterruptDialers := true
+	disconnect := make(chan bool)
+	createDialer = func(p *msdsn.Config) Dialer {
+		nd := netDialer{&net.Dialer{Timeout: p.DialTimeout, KeepAlive: p.KeepAlive}}
+		mu.Lock()
+		defer mu.Unlock()
+		if makeInterruptDialers {
+			di := &dialerInterrupt{nd: nd}
+			wg.Add(1)
+			go func() {
+				// Wait for signal, then break connection in both directions
+				<-disconnect
+				di.Interrupt(true)
+				di.Interrupt(false)
+				wg.Done()
+			}()
+			return di
+		}
+		return nd
+	}
+
+	// Enable retry to provide behavior requested in #586
+	cp := testConnParams(t)
+	cp.DisableRetry = false
+	connStr := cp.URL().String()
+
+	// Open a connection pool that holds a single connection to make sure all
+	// queries run on the same connection.
+	db, err := sql.Open("sqlserver", connStr)
+	if err != nil {
+		t.Log(err)
+	}
+	defer db.Close()
+	db.SetMaxIdleConns(1)
+	db.SetMaxOpenConns(1)
+
+	// Demonstrate connection is good the first time we use it
+	result := "<none>"
+	desiredResult := "before disconnect"
+	row := db.QueryRow(fmt.Sprintf("select '%s'", desiredResult))
+	if err = row.Scan(&result); err != nil || result != desiredResult {
+		t.Fatalf("Connection bad on first use. "+
+			"Got result = '%s', err = '%v', wanted result = '%s', err = '%v'",
+			result, err, desiredResult, nil)
+	}
+
+	// Stop making interuptable dialers, because we have already established
+	// our interruptable test connection, then signal and wait for our test
+	// connection to be broken
+	mu.Lock()
+	makeInterruptDialers = false
+	mu.Unlock()
+	close(disconnect)
+	wg.Wait()
+
+	// Broken connection should cause the next query to initially fail internally,
+	// but then the logic within database/sql should transparently discard the bad
+	// connection, retry the query with a new connection, and ultimately succeed.
+	result = "<none>"
+	desiredResult = "after disconnect"
+	row = db.QueryRow(fmt.Sprintf("select '%s'", desiredResult))
+	if err = row.Scan(&result); err != nil || result != desiredResult {
+		t.Fatalf("Connection bad on second use. "+
+			"Got result = '%s', err = '%v', wanted result = '%s', err = '%v'",
+			result, err, desiredResult, nil)
+	}
+}
+
+// TestDisconnect5 verifies that a connection that goes bad during a query results in:
+// 1. The query failing with a detailed error message
+// 2. The bad connection being immediately removed from the pool
+// 3. The subsequent query using a new connection and succeeding
+func TestDisconnect5(t *testing.T) {
+	if testing.Short() {
+		t.Skip("short")
+	}
+	checkConnStr(t)
+	SetLogger(testLogger{t})
+
+	// Open a connection pool that holds a single connection to make sure all
+	// queries run on the same connection.
+	db, err := sql.Open("sqlserver", makeConnStr(t).String())
+	if err != nil {
+		t.Log(err)
+	}
+	defer db.Close()
+	db.SetMaxIdleConns(1)
+	db.SetMaxOpenConns(1)
+
+	// Demonstrate connection is good the first time we use it
+	result := "<none>"
+	desiredResult := "before disconnect"
+	row := db.QueryRow(fmt.Sprintf("select '%s'", desiredResult))
+	if err = row.Scan(&result); err != nil || result != desiredResult {
+		t.Fatalf("Connection bad on first use. "+
+			"Got result = '%s', err = '%v', wanted result = '%s', err = '%v'",
+			result, err, desiredResult, nil)
+	}
+
+	// Break the connection during a query by raising a fatal error in SQL Server
+	_, err = db.Exec("raiserror('fatal error', 20, 1) with log")
+	_, isServerError := err.(ServerError)
+	_, isNetError := err.(*net.OpError)
+	if !isServerError && !isNetError {
+		// There are a variety of errors that could be returned for a broken
+		// connection. The majority should fall within the mssql.ServerError
+		// and net.OpError categories. We try to make our test more specific
+		// by accepting only those errors, but that makes this test somewhat
+		// less resilient than desired. If this test fails because we start
+		// seeing previously unseen errors and those new errors also indicate
+		// a broken connection, then add them above. If this happens regularly,
+		// consider just allowing any error here. That will reduce any false
+		// alarms from this test at the cost of reduced ability to detect
+		// genuine test failure.
+		t.Fatalf("Failed to break connection. Got err = '%#v', wanted error = '%s'",
+			err, "mssql.ServerError or net.OpError")
+	}
+
+	// The broken connection should have been removed from the pool and replaced
+	// with a new connection at the end of the previous query, allowing this next
+	// query to succeed.
+	result = "<none>"
+	desiredResult = "after disconnect"
+	row = db.QueryRow(fmt.Sprintf("select '%s'", desiredResult))
+	if err = row.Scan(&result); err != nil || result != desiredResult {
+		t.Fatalf("Connection bad on second use. "+
+			"Got result = '%s', err = '%v', wanted result = '%s', err = '%v'",
+			result, err, desiredResult, nil)
+	}
+}
+
 func TestClose(t *testing.T) {
 	conn := open(t)
 
diff --git a/tds_test.go b/tds_test.go
index 4301024..8d0fa25 100644
--- a/tds_test.go
+++ b/tds_test.go
@@ -221,7 +221,7 @@ func testConnParams(t testing.TB) msdsn.Config {
 	if err == nil {
 		rdr := bufio.NewReader(f)
 		dsn, err := rdr.ReadString('\n')
-		if err != io.EOF {
+		if err != io.EOF && err != nil {
 			t.Fatal(err)
 		}
 		params, _, err := msdsn.Parse(dsn)
diff --git a/tvp_go19_test.go b/tvp_go19_test.go
index 4badf56..5a990a7 100644
--- a/tvp_go19_test.go
+++ b/tvp_go19_test.go
@@ -235,7 +235,7 @@ func TestTVPType_check(t *testing.T) {
 				TVPName:  "123.[Test]",
 				TVPValue: []fields{},
 			},
-			wantErr: false,
+			wantErr: true,
 		},
 		{
 			name: "TVP name is wrong",