drizzle-team / drizzle-team/drizzle-orm
[BUG]: tx handle still works after the transaction is committed or rolled back, queries run outside the transaction (node-postgres)
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Report hasn't been filed before.
- [x] I have verified that the bug I'm about to report hasn't been filed before.
### What version of `drizzle-orm` are you using?
0.45.2
### What version of `drizzle-kit` are you using?
not involved, runtime bug
### Describe the Bug
If a reference to the `tx` object from `db.transaction()` outlives the callback, you can keep running queries on it. No error, nothing in the logs. They just execute with no transaction around them.
Nothing in `NodePgSession.transaction()` marks the tx as finished after commit/rollback:
```ts
const session = isPool
? new NodePgSession(await this.client.connect(), this.dialect, this.schema, this.options)
: this;
const tx = new NodePgTransaction(this.dialect, session, this.schema);
await tx.execute(sql`begin`);
try {
const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
} catch (error) {
await tx.execute(sql`rollback`);
throw error;
} finally {
if (isPool) (session.client as PoolClient).release();
}
```
Quick repro (drizzle-orm 0.45.2, pg 8.22.0):
```ts
let leaked;
await db.transaction(async (tx) => {
leaked = tx;
await tx.insert(accounts).values({ balance: '10.00' });
});
// committed at this point
await leaked.insert(accounts).values({ balance: '99999.00' }); // works, no error
```
What actually happens (checked against a real Postgres, both `Client` and `Pool`):
- with a `Pool`, the insert above runs in autocommit on a pool client that was already released back to the pool
- if another request has meanwhile checked out that same client and opened its own transaction, the leaked write lands inside that transaction. Their rollback erases it, their commit commits it. Depends entirely on pool timing
- with a single `Client` it's the same minus the pool: the handle shares the root connection, so everything runs in autocommit. Works after a rollback too, not just commit
- ``tx.execute(sql`...`)`` behaves the same as the builders
Expected: using the handle after the transaction ended should throw. Right now it silently succeeds and the writes look transactional at the call site when they aren't.
Same `transaction()` code is on `beta`, so the 1.0 line is affected as well. PR incoming.
Contributor guide
Research direction
Start in NodePgSession.transaction() and the NodePgTransaction construction shown in the report; reproduce the leaked-handle case with both a Client and Pool. Done means a tx handle used after commit or rollback throws rather than executing, including tx.execute and builder queries; check the corresponding beta transaction code too.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, postgresql, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100