Skip to content

Commit 5862cf6

Browse files
jackcclaude
andcommitted
Send Describe in ExecStatement when cached fields are empty
PgConn.ExecStatement skipped the Describe portal message and used the cached StatementDescription.Fields. As with Pipeline.SendQueryStatement, empty cached fields can mean the result set was not known at prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a Describe in that case so the server supplies the actual row description at execution time. #2626 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 15718e5 commit 5862cf6

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

pgconn/pgconn.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,9 @@ func (pgConn *PgConn) ExecPrepared(ctx context.Context, stmtName string, paramVa
13031303
//
13041304
// This differs from [PgConn.ExecPrepared] in that it takes a [*StatementDescription] instead of the prepared statement name.
13051305
// Because it has the [*StatementDescription] it can avoid the Describe Portal message that [PgConn.ExecPrepared] must send to get
1306-
// the result column descriptions.
1306+
// the result column descriptions. However, if the statement description has no fields then a Describe is still sent, as
1307+
// an empty Fields may mean the results were not knowable at prepare time, e.g. a FETCH from a cursor that did not exist
1308+
// yet.
13071309
//
13081310
// paramValues are the parameter values. It must be encoded in the format given by paramFormats.
13091311
//
@@ -1364,7 +1366,10 @@ func (pgConn *PgConn) execExtendedPrefix(ctx context.Context, paramValues [][]by
13641366
}
13651367

13661368
func (pgConn *PgConn) execExtendedSuffix(result *ResultReader, statementDescription *StatementDescription, resultFormats []int16) {
1367-
if statementDescription == nil {
1369+
if statementDescription == nil || len(statementDescription.Fields) == 0 {
1370+
// The cached field descriptions are missing or empty. Empty field descriptions can occur when the statement's
1371+
// result set was not known at prepare time, e.g. a FETCH from a cursor that did not exist yet. Send a Describe
1372+
// so the server supplies the actual row description when the statement is executed.
13681373
pgConn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'P'})
13691374
}
13701375
pgConn.frontend.SendExecute(&pgproto3.Execute{})

pgconn/pgconn_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,49 @@ func TestConnExecStatement(t *testing.T) {
17111711
ensureConnValid(t, pgConn)
17121712
}
17131713

1714+
// https://github.com/jackc/pgx/issues/2626
1715+
func TestConnExecStatementCursorFetch(t *testing.T) {
1716+
t.Parallel()
1717+
1718+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
1719+
defer cancel()
1720+
1721+
pgConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
1722+
require.NoError(t, err)
1723+
defer closeConn(t, pgConn)
1724+
1725+
if pgConn.ParameterStatus("crdb_version") != "" {
1726+
t.Skip("Server does not support cursors in implicit transactions")
1727+
}
1728+
1729+
// Prepare the FETCH before the cursor exists. The server describes the result as NoData so the statement
1730+
// description has no fields. The actual fields are only known at execution time.
1731+
sd, err := pgConn.Prepare(ctx, "ps_fetch", `fetch all in "exec_statement_cursor"`, nil)
1732+
require.NoError(t, err)
1733+
require.Empty(t, sd.Fields)
1734+
1735+
// DECLARE CURSOR requires an explicit transaction block.
1736+
_, err = pgConn.Exec(ctx, "begin").ReadAll()
1737+
require.NoError(t, err)
1738+
1739+
_, err = pgConn.Exec(ctx, `declare "exec_statement_cursor" cursor for select n, n::text from generate_series(1, 3) n`).ReadAll()
1740+
require.NoError(t, err)
1741+
1742+
result := pgConn.ExecStatement(ctx, sd, nil, nil, nil).Read()
1743+
require.NoError(t, result.Err)
1744+
require.Len(t, result.FieldDescriptions, 2)
1745+
require.Equal(t, uint32(pgtype.Int4OID), result.FieldDescriptions[0].DataTypeOID)
1746+
require.Equal(t, uint32(pgtype.TextOID), result.FieldDescriptions[1].DataTypeOID)
1747+
require.Len(t, result.Rows, 3)
1748+
require.Equal(t, "1", string(result.Rows[0][0]))
1749+
require.Equal(t, "3", string(result.Rows[2][1]))
1750+
1751+
_, err = pgConn.Exec(ctx, "rollback").ReadAll()
1752+
require.NoError(t, err)
1753+
1754+
ensureConnValid(t, pgConn)
1755+
}
1756+
17141757
type byteCounterConn struct {
17151758
conn net.Conn
17161759
bytesRead int

query_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,53 @@ func TestConnQueryRowsFieldDescriptionsBeforeNext(t *testing.T) {
7272
assert.Equal(t, "msg", rows.FieldDescriptions()[0].Name)
7373
}
7474

75+
// https://github.com/jackc/pgx/issues/2626
76+
func TestConnQueryPreparedCursorFetch(t *testing.T) {
77+
t.Parallel()
78+
79+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
80+
defer cancel()
81+
82+
conn := mustConnectString(t, os.Getenv("PGX_TEST_DATABASE"))
83+
defer closeConn(t, conn)
84+
85+
pgxtest.SkipCockroachDB(t, conn, "Server does not support cursors in implicit transactions")
86+
87+
// Prepare the FETCH before the cursor exists. The server describes the result as NoData so the statement
88+
// description has no fields. The actual fields are only known at execution time.
89+
sd, err := conn.Prepare(ctx, "fetch_cursor", `fetch all in "query_cursor"`)
90+
require.NoError(t, err)
91+
require.Empty(t, sd.Fields)
92+
93+
tx, err := conn.Begin(ctx)
94+
require.NoError(t, err)
95+
defer tx.Rollback(ctx)
96+
97+
_, err = tx.Exec(ctx, `declare "query_cursor" cursor for select n, n::text as str from generate_series(1, 3) n`)
98+
require.NoError(t, err)
99+
100+
rows, err := tx.Query(ctx, "fetch_cursor")
101+
require.NoError(t, err)
102+
103+
fds := rows.FieldDescriptions()
104+
require.Len(t, fds, 2)
105+
require.Equal(t, "n", fds[0].Name)
106+
require.Equal(t, "str", fds[1].Name)
107+
108+
var ns []int32
109+
var strs []string
110+
for rows.Next() {
111+
var n int32
112+
var str string
113+
require.NoError(t, rows.Scan(&n, &str))
114+
ns = append(ns, n)
115+
strs = append(strs, str)
116+
}
117+
require.NoError(t, rows.Err())
118+
require.Equal(t, []int32{1, 2, 3}, ns)
119+
require.Equal(t, []string{"1", "2", "3"}, strs)
120+
}
121+
75122
func TestConnQueryWithoutResultSetCommandTag(t *testing.T) {
76123
t.Parallel()
77124

0 commit comments

Comments
 (0)