Repository navigation
Conversation
When a query throws while it is built (an undefined value, more than
65534 parameters, a serializer that throws), `execute` has already queued
it to await an answer, but nothing of it was written. The catch then
rejected the connection's current query with the new query's error and
left the failed query waiting for an answer that never comes, so every
later answer on the connection went to the wrong query:
await sql`select 1`
const a = sql`select pg_sleep(0.1), 1 as x`
, b = sql`select ${ undefined }`
, c = sql`select 3 as x`
// a rejects with UNDEFINED_VALUE, b with "Cannot set properties of
// null (setting 'columns')", c never settles, nor does any later query
This is also why `sql.begin(sql => [sql`select 1`, sql`select ${ undefined }`])`
never settles (porsager#1082). On a reserved connection with the transaction
pipelined (`begin`, insert, failing query, insert, `commit`), both inserts
committed.
Send a query the server is sure to refuse in the failed query's place.
The failed query then gets an answer of its own, every later answer stays
with its query, and a transaction it was part of is aborted rather than
committed without it. The failed query is rejected with its own error, as
a retried query already is. It is no longer marked to be described first
or as a cursor, since either would make the server's error send a Sync of
its own and shift the next answer.
Fixes porsager#1082
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
I made a few PRs from Claude which fix some problems I was having. Can you take a look? |
… the pipeline (porsager/postgres#1236), so a transaction it was part of aborts instead of committing without it The first cut built and serialized the statement before it joined the connection's queue, which left later statements untouched. In a pipelined begin … commit on a reserved connection that meant the two inserts around the culprit both committed. The driver now sends a query the server refuses in the culprit's place, as upstream PR #1236 does: the culprit rejects with its own error, the member behind it is refused with 25P02, the commit lands as ROLLBACK, and the patch drops when #1236 merges. Two e2e cases cover the sql.begin and the reserved pipelined transaction; both fail on master (hang) and on the first cut (both inserts commit). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
|
An independent production report for this one, from garrytan/gbrain on postgres 3.4.9 under Bun 1.4, against Postgres 16 both directly and through PgBouncer in transaction mode. How it looked in production: with const sql = postgres(url, { max: 1, prepare: false, shared_types: true })
await sql`select 1`
const head = sql`select pg_sleep(0.3), 'head' as who`
const culprit = sql`select ${ undefined }::text as who`
const next = sql`select 'next' as who, ${ 1 }::int as n`
// 3.4.9: head rejects UNDEFINED_VALUE, culprit gets a TypeError on query.statement, next never settlesWe vendored this PR's approach: on a build failure, send a statement the server is certain to refuse in the culprit's place. It fixes all of our cases: the pipelined culprit above; a reserved, pipelined |
|
We fixed the same fault another way, in } catch (error) {
query === q
? (write(Sync), errored(error))
: (sent.remove(q), queryError(q, error))
return true
} |
When a query throws while it is built (an undefined value, more than 65534 parameters, a serializer that throws),
executehas already queued it to await an answer, but nothing of it was written. The catch then rejected the connection's current query with the new query's error and left the failed query waiting for an answer that never comes, so every later answer on the connection went to the wrong query:This is also why
sql.begin(sql => [sqlselect 1, sqlselect ${ undefined }])never settles (#1082). On a reserved connection with the transaction pipelined (begin, insert, failing query, insert,commit), both inserts committed.Send a query the server is sure to refuse in the failed query's place. The failed query then gets an answer of its own, every later answer stays with its query, and a transaction it was part of is aborted rather than committed without it. The failed query is rejected with its own error, as a retried query already is. It is no longer marked to be described first or as a cursor, since either would make the server's error send a Sync of its own and shift the next answer.
Fixes #1082