From 969b244d87cba2e2321f5771030d2d75829c4b0a Mon Sep 17 00:00:00 2001 From: James Hartig Date: Mon, 4 Dec 2023 16:42:16 -0600 Subject: [PATCH] pgconn: add LastError() LastError() returns the last error encountered by the underlying connection or received from postgres. It is cleared when a new request is initiated. Fixes #1803 --- pgconn/pgconn.go | 101 +++++++++++++++++++++++++++++++----------- pgconn/pgconn_test.go | 48 ++++++++++++++++++-- 2 files changed, 119 insertions(+), 30 deletions(-) diff --git a/pgconn/pgconn.go b/pgconn/pgconn.go index 1ccdc4db9..00cd9bd8e 100644 --- a/pgconn/pgconn.go +++ b/pgconn/pgconn.go @@ -87,6 +87,8 @@ type PgConn struct { peekedMsg pgproto3.BackendMessage + lastError error + // Reusable / preallocated resources resultReader ResultReader multiResultReader MultiResultReader @@ -460,6 +462,12 @@ func (pgConn *PgConn) signalMessage() chan struct{} { return ch } +// setLastError stores the received error for the LastError function to return. +// This MUST be called before the connection is closed. +func (pgConn *PgConn) setLastError(err error) { + pgConn.lastError = err +} + // ReceiveMessage receives one wire protocol message from the PostgreSQL server. It must only be used when the // connection is not busy. e.g. It is an error to call ReceiveMessage while reading the result of a query. The messages // are still handled by the core pgconn message handling system so receiving a NotificationResponse will still trigger @@ -485,6 +493,7 @@ func (pgConn *PgConn) ReceiveMessage(ctx context.Context) (pgproto3.BackendMessa msg, err := pgConn.receiveMessage() if err != nil { + pgConn.setLastError(err) err = &pgconnError{ msg: "receive message failed", err: normalizeTimeoutError(ctx, err), @@ -523,9 +532,8 @@ func (pgConn *PgConn) peekMessage() (pgproto3.BackendMessage, error) { var netErr net.Error isNetErr := errors.As(err, &netErr) if !(isNetErr && netErr.Timeout()) { - pgConn.asyncClose() + pgConn.asyncClose(err) } - return nil, err } @@ -547,11 +555,14 @@ func (pgConn *PgConn) receiveMessage() (pgproto3.BackendMessage, error) { case *pgproto3.ParameterStatus: pgConn.parameterStatuses[msg.Name] = msg.Value case *pgproto3.ErrorResponse: + err := ErrorResponseToPgError(msg) if msg.Severity == "FATAL" { + // call setLastError before we close but otherwise leave it up to the caller + pgConn.setLastError(err) pgConn.status = connStatusClosed pgConn.conn.Close() // Ignore error as the connection is already broken and there is already an error to return. close(pgConn.cleanupDone) - return nil, ErrorResponseToPgError(msg) + return nil, err } case *pgproto3.NoticeResponse: if pgConn.config.OnNotice != nil { @@ -590,6 +601,16 @@ func (pgConn *PgConn) TxStatus() byte { return pgConn.txStatus } +// LastError returns the last error caused either by the underlying connection +// or returned from Postgres. If the error was returned from Postgres then the +// error will be *pgconn.PgError. +// +// When a new request is initiated (via Exec, CopyFrom, CopyTo, etc) any previous +// error will be cleared. +func (pgConn *PgConn) LastError() error { + return pgConn.lastError +} + // SecretKey returns the backend secret key used to send a cancel query message to the server. func (pgConn *PgConn) SecretKey() uint32 { return pgConn.secretKey @@ -637,11 +658,12 @@ func (pgConn *PgConn) Close(ctx context.Context) error { // asyncClose marks the connection as closed and asynchronously sends a cancel query message and closes the underlying // connection. -func (pgConn *PgConn) asyncClose() { +func (pgConn *PgConn) asyncClose(err error) { if pgConn.status == connStatusClosed { return } pgConn.status = connStatusClosed + pgConn.setLastError(err) go func() { defer close(pgConn.cleanupDone) @@ -832,12 +854,14 @@ func (pgConn *PgConn) Prepare(ctx context.Context, name, sql string, paramOIDs [ defer pgConn.contextWatcher.Unwatch() } + // clear the last error since we are sending a new command + pgConn.setLastError(nil) pgConn.frontend.SendParse(&pgproto3.Parse{Name: name, Query: sql, ParameterOIDs: paramOIDs}) pgConn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'S', Name: name}) pgConn.frontend.SendSync(&pgproto3.Sync{}) err := pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return nil, err } @@ -849,7 +873,7 @@ readloop: for { msg, err := pgConn.receiveMessage() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return nil, normalizeTimeoutError(ctx, err) } @@ -861,6 +885,7 @@ readloop: psd.Fields = pgConn.convertRowDescription(nil, msg) case *pgproto3.ErrorResponse: parseErr = ErrorResponseToPgError(msg) + pgConn.setLastError(parseErr) case *pgproto3.ReadyForQuery: break readloop } @@ -1027,7 +1052,9 @@ func (pgConn *PgConn) WaitForNotification(ctx context.Context) error { for { msg, err := pgConn.receiveMessage() if err != nil { - return normalizeTimeoutError(ctx, err) + err = normalizeTimeoutError(ctx, err) + pgConn.setLastError(err) + return err } switch msg.(type) { @@ -1067,10 +1094,12 @@ func (pgConn *PgConn) Exec(ctx context.Context, sql string) *MultiResultReader { pgConn.contextWatcher.Watch(ctx) } + // clear the last error since we are sending a new command + pgConn.setLastError(nil) pgConn.frontend.SendQuery(&pgproto3.Query{String: sql}) err := pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) pgConn.contextWatcher.Unwatch() multiResult.closed = true multiResult.err = err @@ -1175,13 +1204,15 @@ func (pgConn *PgConn) execExtendedPrefix(ctx context.Context, paramValues [][]by } func (pgConn *PgConn) execExtendedSuffix(result *ResultReader) { + // clear the last error since we are sending a new command + pgConn.setLastError(nil) pgConn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'P'}) pgConn.frontend.SendExecute(&pgproto3.Execute{}) pgConn.frontend.SendSync(&pgproto3.Sync{}) err := pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) result.concludeCommand(CommandTag{}, err) pgConn.contextWatcher.Unwatch() result.closed = true @@ -1209,12 +1240,13 @@ func (pgConn *PgConn) CopyTo(ctx context.Context, w io.Writer, sql string) (Comm defer pgConn.contextWatcher.Unwatch() } + // clear the last error since we are sending a new command + pgConn.setLastError(nil) // Send copy to command pgConn.frontend.SendQuery(&pgproto3.Query{String: sql}) - err := pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) pgConn.unlock() return CommandTag{}, err } @@ -1225,7 +1257,7 @@ func (pgConn *PgConn) CopyTo(ctx context.Context, w io.Writer, sql string) (Comm for { msg, err := pgConn.receiveMessage() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return CommandTag{}, normalizeTimeoutError(ctx, err) } @@ -1234,7 +1266,7 @@ func (pgConn *PgConn) CopyTo(ctx context.Context, w io.Writer, sql string) (Comm case *pgproto3.CopyData: _, err := w.Write(msg.Data) if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return CommandTag{}, err } case *pgproto3.ReadyForQuery: @@ -1244,6 +1276,7 @@ func (pgConn *PgConn) CopyTo(ctx context.Context, w io.Writer, sql string) (Comm commandTag = pgConn.makeCommandTag(msg.CommandTag) case *pgproto3.ErrorResponse: pgErr = ErrorResponseToPgError(msg) + pgConn.setLastError(pgErr) } } } @@ -1268,11 +1301,13 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co defer pgConn.contextWatcher.Unwatch() } + // clear the last error since we are sending a new command + pgConn.setLastError(nil) // Send copy from query pgConn.frontend.SendQuery(&pgproto3.Query{String: sql}) err := pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return CommandTag{}, err } @@ -1297,6 +1332,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co writeErr := pgConn.frontend.SendUnbufferedEncodedCopyData(*buf) if writeErr != nil { + pgConn.setLastError(writeErr) // Write errors are always fatal, but we can't use asyncClose because we are in a different goroutine. Not // setting pgConn.status or closing pgConn.cleanupDone for the same reason. pgConn.conn.Close() @@ -1306,6 +1342,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co } } if readErr != nil { + pgConn.setLastError(readErr) copyErrChan <- readErr return } @@ -1328,6 +1365,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co // the goroutine. So instead check pgConn.bufferingReceiveErr which will have been set by the signalMessage. If an // error is found then forcibly close the connection without sending the Terminate message. if err := pgConn.bufferingReceiveErr; err != nil { + pgConn.setLastError(err) pgConn.status = connStatusClosed pgConn.conn.Close() close(pgConn.cleanupDone) @@ -1338,6 +1376,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co switch msg := msg.(type) { case *pgproto3.ErrorResponse: pgErr = ErrorResponseToPgError(msg) + pgConn.setLastError(pgErr) default: signalMessageChan = pgConn.signalMessage() } @@ -1354,7 +1393,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co } err = pgConn.flushWithPotentialWriteReadDeadlock() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return CommandTag{}, err } @@ -1363,7 +1402,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co for { msg, err := pgConn.receiveMessage() if err != nil { - pgConn.asyncClose() + pgConn.asyncClose(err) return CommandTag{}, normalizeTimeoutError(ctx, err) } @@ -1374,6 +1413,7 @@ func (pgConn *PgConn) CopyFrom(ctx context.Context, r io.Reader, sql string) (Co commandTag = pgConn.makeCommandTag(msg.CommandTag) case *pgproto3.ErrorResponse: pgErr = ErrorResponseToPgError(msg) + pgConn.setLastError(pgErr) } } } @@ -1408,7 +1448,7 @@ func (mrr *MultiResultReader) receiveMessage() (pgproto3.BackendMessage, error) mrr.pgConn.contextWatcher.Unwatch() mrr.err = normalizeTimeoutError(mrr.ctx, err) mrr.closed = true - mrr.pgConn.asyncClose() + mrr.pgConn.asyncClose(err) return nil, mrr.err } @@ -1423,6 +1463,7 @@ func (mrr *MultiResultReader) receiveMessage() (pgproto3.BackendMessage, error) } case *pgproto3.ErrorResponse: mrr.err = ErrorResponseToPgError(msg) + mrr.pgConn.setLastError(mrr.err) } return msg, nil @@ -1584,6 +1625,7 @@ func (rr *ResultReader) Close() (CommandTag, error) { // Detect a deferred constraint violation where the ErrorResponse is sent after CommandComplete. case *pgproto3.ErrorResponse: rr.err = ErrorResponseToPgError(msg) + rr.pgConn.setLastError(rr.err) case *pgproto3.ReadyForQuery: rr.pgConn.contextWatcher.Unwatch() rr.pgConn.unlock() @@ -1628,7 +1670,7 @@ func (rr *ResultReader) receiveMessage() (msg pgproto3.BackendMessage, err error rr.pgConn.contextWatcher.Unwatch() rr.closed = true if rr.multiResultReader == nil { - rr.pgConn.asyncClose() + rr.pgConn.asyncClose(err) } return nil, rr.err @@ -1652,6 +1694,7 @@ func (rr *ResultReader) concludeCommand(commandTag CommandTag, err error) { // Keep the first error that is recorded. Store the error before checking if the command is already concluded to // allow for receiving an error after CommandComplete but before ReadyForQuery. if err != nil && rr.err == nil { + rr.pgConn.setLastError(err) rr.err = err } @@ -1713,10 +1756,13 @@ func (pgConn *PgConn) ExecBatch(ctx context.Context, batch *Batch) *MultiResultR batch.buf = (&pgproto3.Sync{}).Encode(batch.buf) + // clear the last error since we are sending a new command + pgConn.setLastError(nil) pgConn.enterPotentialWriteReadDeadlock() defer pgConn.exitPotentialWriteReadDeadlock() _, err := pgConn.conn.Write(batch.buf) if err != nil { + pgConn.setLastError(err) multiResult.closed = true multiResult.err = err pgConn.unlock() @@ -2032,7 +2078,7 @@ func (p *Pipeline) Flush() error { if err != nil { err = normalizeTimeoutError(p.ctx, err) - p.conn.asyncClose() + p.conn.asyncClose(err) p.conn.contextWatcher.Unwatch() p.conn.unlock() @@ -2116,7 +2162,7 @@ func (p *Pipeline) getResultsPrepare() (*StatementDescription, error) { for { msg, err := p.conn.receiveMessage() if err != nil { - p.conn.asyncClose() + p.conn.asyncClose(err) return nil, normalizeTimeoutError(p.ctx, err) } @@ -2138,11 +2184,13 @@ func (p *Pipeline) getResultsPrepare() (*StatementDescription, error) { pgErr := ErrorResponseToPgError(msg) return nil, pgErr case *pgproto3.CommandComplete: - p.conn.asyncClose() - return nil, errors.New("BUG: received CommandComplete while handling Describe") + err := errors.New("BUG: received CommandComplete while handling Describe") + p.conn.asyncClose(err) + return nil, err case *pgproto3.ReadyForQuery: - p.conn.asyncClose() - return nil, errors.New("BUG: received ReadyForQuery while handling Describe") + err := errors.New("BUG: received ReadyForQuery while handling Describe") + p.conn.asyncClose(err) + return nil, err } } } @@ -2155,11 +2203,10 @@ func (p *Pipeline) Close() error { p.closed = true if p.pendingSync { - p.conn.asyncClose() p.err = errors.New("pipeline has unsynced requests") + p.conn.asyncClose(p.err) p.conn.contextWatcher.Unwatch() p.conn.unlock() - return p.err } @@ -2169,7 +2216,7 @@ func (p *Pipeline) Close() error { p.err = err var pgErr *PgError if !errors.As(err, &pgErr) { - p.conn.asyncClose() + p.conn.asyncClose(err) break } } diff --git a/pgconn/pgconn_test.go b/pgconn/pgconn_test.go index 806219302..f3d3f51f4 100644 --- a/pgconn/pgconn_test.go +++ b/pgconn/pgconn_test.go @@ -636,6 +636,7 @@ func TestConnPrepareSyntaxError(t *testing.T) { psd, err := pgConn.Prepare(ctx, "ps1", "SYNTAX ERROR", nil) require.Nil(t, psd) require.NotNil(t, err) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -657,6 +658,7 @@ func TestConnPrepareContextPrecanceled(t *testing.T) { assert.Error(t, err) assert.True(t, errors.Is(err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(err)) + assert.Nil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -866,6 +868,7 @@ func TestConnExecMultipleQueriesError(t *testing.T) { } else { t.Errorf("unexpected error: %v", err) } + assert.NotNil(t, pgConn.LastError()) if pgConn.ParameterStatus("crdb_version") != "" { // CockroachDB starts the second query result set and then sends the divide by zero error. @@ -910,6 +913,7 @@ func TestConnExecDeferredError(t *testing.T) { _, err = pgConn.Exec(ctx, `update t set n=n+1 where id='b' returning *`).ReadAll() require.NotNil(t, err) + assert.NotNil(t, pgConn.LastError()) var pgErr *pgconn.PgError require.True(t, errors.As(err, &pgErr)) @@ -939,6 +943,7 @@ func TestConnExecContextCanceled(t *testing.T) { assert.True(t, pgconn.Timeout(err)) assert.ErrorIs(t, err, context.DeadlineExceeded) assert.True(t, pgConn.IsClosed()) + assert.NotNil(t, pgConn.LastError()) select { case <-pgConn.CleanupDone(): case <-time.After(5 * time.Second): @@ -961,6 +966,7 @@ func TestConnExecContextPrecanceled(t *testing.T) { assert.Error(t, err) assert.True(t, errors.Is(err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(err)) + assert.Nil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1022,6 +1028,7 @@ func TestConnExecParamsDeferredError(t *testing.T) { var pgErr *pgconn.PgError require.True(t, errors.As(result.Err, &pgErr)) require.Equal(t, "23505", pgErr.Code) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1074,6 +1081,7 @@ func TestConnExecParamsTooManyParams(t *testing.T) { result := pgConn.ExecParams(ctx, sql, args, nil, nil, nil).Read() require.Error(t, result.Err) require.Equal(t, "extended protocol limited to 65535 parameters", result.Err.Error()) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1100,8 +1108,8 @@ func TestConnExecParamsCanceled(t *testing.T) { assert.Equal(t, pgconn.CommandTag{}, commandTag) assert.True(t, pgconn.Timeout(err)) assert.ErrorIs(t, err, context.DeadlineExceeded) - assert.True(t, pgConn.IsClosed()) + assert.NotNil(t, pgConn.LastError()) select { case <-pgConn.CleanupDone(): case <-time.After(5 * time.Second): @@ -1124,6 +1132,7 @@ func TestConnExecParamsPrecanceled(t *testing.T) { require.Error(t, result.Err) assert.True(t, errors.Is(result.Err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(result.Err)) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1273,6 +1282,7 @@ func TestConnExecPreparedTooManyParams(t *testing.T) { result := pgConn.ExecPrepared(ctx, "ps1", args, nil, nil).Read() require.EqualError(t, result.Err, "extended protocol limited to 65535 parameters") } + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1302,6 +1312,7 @@ func TestConnExecPreparedCanceled(t *testing.T) { assert.Equal(t, pgconn.CommandTag{}, commandTag) assert.True(t, pgconn.Timeout(err)) assert.True(t, pgConn.IsClosed()) + assert.NotNil(t, pgConn.LastError()) select { case <-pgConn.CleanupDone(): case <-time.After(5 * time.Second): @@ -1327,6 +1338,7 @@ func TestConnExecPreparedPrecanceled(t *testing.T) { require.Error(t, result.Err) assert.True(t, errors.Is(result.Err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(result.Err)) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1420,6 +1432,7 @@ func TestConnExecBatchDeferredError(t *testing.T) { var pgErr *pgconn.PgError require.True(t, errors.As(err, &pgErr)) require.Equal(t, "23505", pgErr.Code) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1448,6 +1461,7 @@ func TestConnExecBatchPrecanceled(t *testing.T) { require.Error(t, err) assert.True(t, errors.Is(err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(err)) + assert.Nil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1515,6 +1529,7 @@ func TestConnExecBatchImplicitTransaction(t *testing.T) { batch.ExecParams("select 1/0", nil, nil, nil, nil) _, err = pgConn.ExecBatch(ctx, batch).ReadAll() require.Error(t, err) + assert.NotNil(t, pgConn.LastError()) result := pgConn.ExecParams(ctx, "select count(*) from t", nil, nil, nil, nil).Read() require.Equal(t, "0", string(result.Rows[0][0])) @@ -1807,6 +1822,7 @@ func TestConnCopyToQueryError(t *testing.T) { require.Error(t, err) assert.IsType(t, &pgconn.PgError{}, err) assert.Equal(t, int64(0), res.RowsAffected()) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -1832,8 +1848,8 @@ func TestConnCopyToCanceled(t *testing.T) { res, err := pgConn.CopyTo(ctx, outputWriter, "copy (select *, pg_sleep(0.01) from generate_series(1,1000)) to stdout") assert.Error(t, err) assert.Equal(t, pgconn.CommandTag{}, res) - assert.True(t, pgConn.IsClosed()) + assert.NotNil(t, pgConn.LastError()) select { case <-pgConn.CleanupDone(): case <-time.After(5 * time.Second): @@ -1859,6 +1875,7 @@ func TestConnCopyToPrecanceled(t *testing.T) { assert.True(t, errors.Is(err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(err)) assert.Equal(t, pgconn.CommandTag{}, res) + assert.Nil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -2000,8 +2017,8 @@ func TestConnCopyFromCanceled(t *testing.T) { cancel() assert.Equal(t, int64(0), ct.RowsAffected()) assert.Error(t, err) - assert.True(t, pgConn.IsClosed()) + assert.NotNil(t, pgConn.LastError()) select { case <-pgConn.CleanupDone(): case <-time.After(5 * time.Second): @@ -2045,6 +2062,7 @@ func TestConnCopyFromPrecanceled(t *testing.T) { assert.True(t, errors.Is(err, context.Canceled)) assert.True(t, pgconn.SafeToRetry(err)) assert.Equal(t, pgconn.CommandTag{}, ct) + assert.Nil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -2150,6 +2168,7 @@ func TestConnCopyFromQuerySyntaxError(t *testing.T) { require.Error(t, err) assert.IsType(t, &pgconn.PgError{}, err) assert.Equal(t, int64(0), res.RowsAffected()) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -2170,6 +2189,7 @@ func TestConnCopyFromQueryNoTableError(t *testing.T) { require.Error(t, err) assert.IsType(t, &pgconn.PgError{}, err) assert.Equal(t, int64(0), res.RowsAffected()) + assert.NotNil(t, pgConn.LastError()) ensureConnValid(t, pgConn) } @@ -2537,6 +2557,7 @@ func TestFatalErrorReceivedAfterCommandComplete(t *testing.T) { _, err = rr.Close() require.Error(t, err) + assert.NotNil(t, conn.LastError()) } // https://github.com/jackc/pgconn/issues/27 @@ -3148,6 +3169,27 @@ func TestPipelineCloseDetectsUnsyncedRequests(t *testing.T) { require.EqualError(t, err, "pipeline has unsynced requests") } +func TestConnLastError(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + pgConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE")) + require.NoError(t, err) + defer closeConn(t, pgConn) + + err = pgConn.Exec(ctx, "select 1/0").Close() + require.NotNil(t, err) + assert.Equal(t, err, pgConn.LastError()) + + ensureConnValid(t, pgConn) + + _, err = pgConn.Prepare(ctx, "ps1", "select 1", nil) + require.Nil(t, err) + assert.Nil(t, pgConn.LastError()) +} + func Example() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel()