Skip to content

Commit 52de99b

Browse files
jackcclaude
andcommitted
Send Describe in Batch.ExecStatement when cached fields are empty
This applies the same fix to Batch.ExecStatement that was previously applied to Pipeline.SendQueryStatement and PgConn.ExecStatement. Empty cached fields may mean the results were not knowable at prepare time, e.g. a FETCH from a cursor that did not exist yet, so a Describe is sent and the server-provided row description is used. #2626 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f105936 commit 52de99b

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

pgconn/pgconn.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2022,7 +2022,9 @@ func (batch *Batch) ExecPrepared(stmtName string, paramValues [][]byte, paramFor
20222022
//
20232023
// This differs from ExecPrepared in that it takes a *StatementDescription instead of just the prepared statement name.
20242024
// Because it has the *StatementDescription it can avoid the Describe Portal message that ExecPrepared must send to get
2025-
// the result column descriptions.
2025+
// the result column descriptions. However, if the statement description has no fields then a Describe is still sent, as
2026+
// an empty Fields may mean the results were not knowable at prepare time, e.g. a FETCH from a cursor that did not exist
2027+
// yet.
20262028
func (batch *Batch) ExecStatement(statementDescription *StatementDescription, paramValues [][]byte, paramFormats, resultFormats []int16) {
20272029
if batch.err != nil {
20282030
return
@@ -2033,6 +2035,16 @@ func (batch *Batch) ExecStatement(statementDescription *StatementDescription, pa
20332035
return
20342036
}
20352037

2038+
if len(statementDescription.Fields) == 0 {
2039+
// The cached field descriptions are empty, which can occur when the statement's result set was not known at
2040+
// prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a Describe so the server supplies the
2041+
// actual row description when the statement is executed.
2042+
batch.buf, batch.err = (&pgproto3.Describe{ObjectType: 'P'}).Encode(batch.buf)
2043+
if batch.err != nil {
2044+
return
2045+
}
2046+
}
2047+
20362048
batch.statementDescriptions = append(batch.statementDescriptions, statementDescription)
20372049
batch.resultFormats = append(batch.resultFormats, resultFormats)
20382050

pgconn/pgconn_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,6 +1977,54 @@ func TestConnExecBatchStatementNoRows(t *testing.T) {
19771977
ensureConnValid(t, pgConn)
19781978
}
19791979

1980+
// https://github.com/jackc/pgx/issues/2626
1981+
func TestConnExecBatchStatementCursorFetch(t *testing.T) {
1982+
t.Parallel()
1983+
1984+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
1985+
defer cancel()
1986+
1987+
pgConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
1988+
require.NoError(t, err)
1989+
defer closeConn(t, pgConn)
1990+
1991+
if pgConn.ParameterStatus("crdb_version") != "" {
1992+
t.Skip("Server does not support cursors in implicit transactions")
1993+
}
1994+
1995+
// Prepare the FETCH before the cursor exists. The server describes the result as NoData so the statement
1996+
// description has no fields. The actual fields are only known at execution time.
1997+
sd, err := pgConn.Prepare(ctx, "ps_batch_fetch", `fetch all in "exec_batch_cursor"`, nil)
1998+
require.NoError(t, err)
1999+
require.Empty(t, sd.Fields)
2000+
2001+
// DECLARE CURSOR requires an explicit transaction block.
2002+
_, err = pgConn.Exec(ctx, "begin").ReadAll()
2003+
require.NoError(t, err)
2004+
2005+
batch := &pgconn.Batch{}
2006+
batch.ExecParams(`declare "exec_batch_cursor" cursor for select n, n::text from generate_series(1, 3) n`, nil, nil, nil, nil)
2007+
batch.ExecStatement(sd, nil, nil, nil)
2008+
2009+
results, err := pgConn.ExecBatch(ctx, batch).ReadAll()
2010+
require.NoError(t, err)
2011+
require.Len(t, results, 2)
2012+
2013+
fetchResult := results[1]
2014+
require.NoError(t, fetchResult.Err)
2015+
require.Len(t, fetchResult.FieldDescriptions, 2)
2016+
require.Equal(t, uint32(pgtype.Int4OID), fetchResult.FieldDescriptions[0].DataTypeOID)
2017+
require.Equal(t, uint32(pgtype.TextOID), fetchResult.FieldDescriptions[1].DataTypeOID)
2018+
require.Len(t, fetchResult.Rows, 3)
2019+
require.Equal(t, "1", string(fetchResult.Rows[0][0]))
2020+
require.Equal(t, "3", string(fetchResult.Rows[2][1]))
2021+
2022+
_, err = pgConn.Exec(ctx, "rollback").ReadAll()
2023+
require.NoError(t, err)
2024+
2025+
ensureConnValid(t, pgConn)
2026+
}
2027+
19802028
type mockConnection struct {
19812029
net.Conn
19822030
writeLatency *time.Duration

0 commit comments

Comments
 (0)