diff --git a/.travis.yml b/.travis.yml index c3d7e6f..9b1a3b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: go go: - 1.x -- 1.8.x +- 1.13.x script: - go test -p 1 -v ./... diff --git a/crdb/README.md b/crdb/README.md index 9088da5..ff2ba80 100644 --- a/crdb/README.md +++ b/crdb/README.md @@ -6,7 +6,8 @@ retries (as required by CockroachDB). Note that unfortunately there is no generic way of extracting a pg error code; the library has to recognize driver-dependent error types. We currently support -`github.com/lib/pq` and `github.com/jackc/pgx`. +`github.com/lib/pq` and `github.com/jackc/pgconn` (which is used by +`github.com/jackc/pgx/v4`; previous pgx versions are not supported). Note for developers: if you make any changes here (especially if they modify public APIs), please verify that the code in https://github.com/cockroachdb/examples-go diff --git a/crdb/error.go b/crdb/error.go index 32e1001..7a2a45c 100644 --- a/crdb/error.go +++ b/crdb/error.go @@ -11,12 +11,32 @@ type ErrorCauser interface { Cause() error } +// UnwrappableError describes errors compatible with errors.Unwrap. +type UnwrappableError interface { + // Unwrap returns the proximate cause of this error. + Unwrap() error +} + +// Unwrap is equivalent to errors.Unwrap. It's implemented here to maintain +// compatibility with Go versions before 1.13 (when the errors package was +// introduced). +// It returns the result of calling the Unwrap method on err, if err's type +// implements UnwrappableError. +// Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + u, ok := err.(UnwrappableError) + if !ok { + return nil + } + return u.Unwrap() +} + // errorCause returns the original cause of the error, if possible. An error has -// a proximate cause if it implements ErrorCauser; the original cause is the -// first error in the cause chain that does not implement ErrorCauser. -// -// errorCause is intentionally equivalent to pkg/errors.Cause. +// a proximate cause if it's type is compatible with Go's errors.Unwrap() (and +// also, for legacy reasons, if it implements ErrorCauser); the original cause +// is the bottom of the causal chain. func errorCause(err error) error { + // First handle errors implementing ErrorCauser. for err != nil { cause, ok := err.(ErrorCauser) if !ok { @@ -24,6 +44,14 @@ func errorCause(err error) error { } err = cause.Cause() } + // Then handle go1.13+ error wrapping. + for { + cause := Unwrap(err) + if cause == nil { + break + } + err = cause + } return err } diff --git a/crdb/tx.go b/crdb/tx.go index 96e0945..f464e3d 100644 --- a/crdb/tx.go +++ b/crdb/tx.go @@ -20,7 +20,7 @@ import ( "context" "database/sql" - "github.com/jackc/pgx" + "github.com/jackc/pgconn" "github.com/lib/pq" ) @@ -143,7 +143,7 @@ func errCode(err error) string { case *pq.Error: return string(t.Code) - case pgx.PgError: + case *pgconn.PgError: return t.Code default: diff --git a/crdb/tx_test.go b/crdb/tx_test.go index 2c91d0d..0dc2378 100644 --- a/crdb/tx_test.go +++ b/crdb/tx_test.go @@ -95,8 +95,16 @@ INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); runTxn := func(wg *sync.WaitGroup, iter *int) <-chan error { errCh := make(chan error, 1) go func() { + *iter = 0 - errCh <- executeTxFn(context.Background(), db, nil, func(tx *sql.Tx) error { + errCh <- executeTxFn(context.Background(), db, nil, func(tx *sql.Tx) (retErr error) { + defer func() { + if retErr == nil { + return + } + // Wrap the error so that we test the library's unwrapping. + retErr = testError{cause: retErr} + }() *iter++ bal1, bal2, err := getBalances(tx) if err != nil { @@ -150,3 +158,15 @@ UPDATE d.t SET balance=balance-100 WHERE acct=2; "got acct1=%d, acct2=%d: %s", bal1, bal2, err) } } + +type testError struct { + cause error +} + +func (t testError) Error() string { + return "test error" +} + +func (t testError) Unwrap() error { + return t.cause +}