Skip to content

fix(postgres): re-parse unnamed statement after resolving custom-type OIDs - #4361

Open
DoTuanAnh2k1 wants to merge 1 commit into
transact-rs:mainfrom
DoTuanAnh2k1:fix/pg-4305-unnamed-stmt-custom-type
Open

fix(postgres): re-parse unnamed statement after resolving custom-type OIDs#4361
DoTuanAnh2k1 wants to merge 1 commit into
transact-rs:mainfrom
DoTuanAnh2k1:fix/pg-4305-unnamed-stmt-custom-type

Conversation

@DoTuanAnh2k1

Copy link
Copy Markdown

Why

A non-persistent query (persistent(false)) that references a type whose OID is not built-in or cached fails the first time that type is seen on a connection:

error returned from database: unnamed prepared statement does not exist (SQLSTATE 26000)

Thanks to @patrickt's diagnosis in #4305: for persistent(false) the query is parsed as the unnamed statement. Resolving the OID of a custom result type (resolve_statement_metadatafill_cache) runs a catalog lookup over the simple query protocol, and per the Postgres protocol a simple Query destroys the unnamed prepared statement. That happens after Parse but before Bind, so the Bind fails. Named (persistent) statements are unaffected, which is why it only reproduces with persistent(false) on a cold type-OID cache. It's a regression from 0.8.

The module already anticipates exactly this case:

…we'd have to replace the unnamed prepared statement which is already the one the user wanted to execute. This means we'd have to immediately re-prepare it, adding an extra round trip.

What changed

TypeResolver/ColumnResolver::fill_cache and resolve_statement_metadata now report whether they actually ran a catalog lookup. In prepare, when the statement is unnamed and such a lookup ran, the unnamed statement is re-parsed before returning, so the following Bind can reference it. The extra Parse only happens on the persistent(false) + cold-cache path that already pays for the catalog round-trip; the common path is unchanged.

Testing

Added test_issue_4305: a persistent(false) query returning a custom enum on a fresh connection. It fails with SQLSTATE 26000 on the current code and passes with the fix. The full tests/postgres/postgres.rs suite passes (57 passed, 0 failed), including the existing custom-type/array/domain tests. cargo fmt --check and cargo clippy -p sqlx-postgres are clean.

Fixes #4305.


Disclosure: prepared with AI assistance.

… OIDs

A non-persistent query (`persistent(false)`) that references a type whose
OID is not built-in or cached failed with "unnamed prepared statement does
not exist" (SQLSTATE 26000) the first time the type was seen on a connection.

Resolving the type OID runs a catalog lookup over the simple query protocol,
which Postgres uses to also destroy the unnamed prepared statement. That
happened between parsing the unnamed statement and binding it, so the Bind
failed. Named (persistent) statements are unaffected.

Re-parse the unnamed statement when a catalog lookup ran while resolving its
metadata, so the following Bind can reference it.

Fixes transact-rs#4305.
@DoTuanAnh2k1
DoTuanAnh2k1 force-pushed the fix/pg-4305-unnamed-stmt-custom-type branch from 5a9b100 to f5e852f Compare August 1, 2026 08:39
@samuelselleck

samuelselleck commented Aug 4, 2026

Copy link
Copy Markdown

Just chiming in to say that we are hitting this when trying to upgrade as well! Appreciate this being fixed.

@DoTuanAnh2k1

Copy link
Copy Markdown
Author

Thanks for the extra data point — good to know it's biting more than one upgrade. Hopefully it gets a review soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-persistent queries fail spuriously if they reference custom types needing OID lookup

2 participants