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)

Open
#6,083 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.