Skip to content

fix: a compiled query can be bound to a transaction - #194

Merged
productdevbook merged 1 commit into
mainfrom
fix/compiled-in-transactions
Aug 21, 2026
Merged

productdevbook merged 1 commit into
mainfrom
fix/compiled-in-transactions

Conversation

@productdevbook

Copy link
Copy Markdown
Owner

A hazard the previous two PRs made more likely, found by asking what a compiled query does inside a transaction.

The trap

A compiled query captures the instance it was built from — that is what lets it run itself. A transaction checks out its own connection, so a query compiled outside one sends its statement down the pool:

client (the transaction): ["BEGIN", "ROLLBACK"]
pool   (outside it):      ["INSERT INTO users …"]   ← committed

The write commits while the transaction around it rolls back.

pglite cannot show this. It has one connection, so the write appears to participate and the rollback appears to take it back — the probe against pglite came back clean and the bug was still there. It took a recording pool to see.

This gets more likely the more the compiled path is used, which is exactly what #192 and #193 have been encouraging.

The fix

const addUser = db.insertInto("users").values({}).toCompiled<Args>()

await db.transaction(async (tx) => {
  await tx.prepared(addUser).run({}) // on the transaction's connection
})

tx.prepared(...) compiles the SQL again for that instance — once, not per call — and leaves the original untouched.

The tests pin the trap as well as the fix. A test that only covered the correct usage would pass just as happily if the trap came back.

pnpm test green: 3315 tests, lint clean, types clean.

A compiled query captures the instance it was built from, which is what lets
it run itself. Inside a transaction that is the wrong instance — the
transaction checks out its own connection, so a query compiled outside sends
its statement down the pool and commits while the transaction around it
rolls back:

    client (the transaction): ["BEGIN", "ROLLBACK"]
    pool   (outside it):      ["INSERT INTO users …"]   ← committed

pglite cannot show this: it has one connection, so the write appears to
participate and the rollback appears to take it back. It took a recording
pool to see, and it gets more likely the more the compiled path is used —
which this branch has been making the default.

`tx.prepared(query)` binds a query compiled elsewhere to that instance,
compiling the SQL again once rather than per call and leaving the original
alone. The test pins the trap as well as the fix, because a test that only
covers the correct usage would pass just as happily if the trap came back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAwiBVhLmNhWjC9Ro6UMpb
@productdevbook
productdevbook merged commit 099dc42 into main Aug 21, 2026
1 check passed
@productdevbook
productdevbook deleted the fix/compiled-in-transactions branch August 21, 2026 14:00
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.

1 participant