drizzle-team / drizzle-team/drizzle-orm
[BUG]: Pooled transaction() leaks the pool client when BEGIN fails (node-postgres / neon-serverless / vercel-postgres)
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
**drizzle-orm:** 0.45.2 — same code on `main` (0.45.3) and `beta` (1.0.0-rc.4)
**drizzle-kit:** 0.31.10
**Other packages:** pg 8.22.0
### Describe the Bug
If `db.transaction()` runs on a `Pool` and the `BEGIN` statement fails, the client taken from the pool is never released.
`begin` is executed before the `try/finally` that calls `release()`:
```ts
// drizzle-orm/src/node-postgres/session.ts
// neon-serverless/session.ts and vercel-postgres/session.ts have the same shape
const session = isPool ? new NodePgSession(await this.client.connect(), ...) : this;
const tx = new NodePgTransaction(...);
await tx.execute(sql`begin...`); // throws here → the finally below never runs
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.release();
}
```
`BEGIN` does fail in practice: `statement_timeout` (`57014`), a connection dropped right after connect (`57P01`, "Connection terminated unexpectedly"), admin shutdown. Each time, the pool loses one client for good. Under load `pool.connect()` eventually hangs on an empty pool.
Repro without a database — subclass `pg.Pool` and make `BEGIN` throw:
```ts
import pg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
class BeginFailurePool extends pg.Pool {
released = 0;
async connect() {
return {
query: async (q: string | { text: string }) => {
const text = typeof q === 'string' ? q : q.text;
if (text.toLowerCase().startsWith('begin')) {
throw Object.assign(new Error('canceling statement due to statement timeout'), { code: '57014' });
}
return { rows: [], rowCount: 0 };
},
release: () => { this.released++; },
} as any;
}
}
const pool = new BeginFailurePool({ connectionString: 'postgres://u:p@localhost/db' });
await drizzle({ client: pool }).transaction(async () => {}).catch(() => {});
console.log(pool.released); // 0 — expected 1
```
Expected: the client is released on every exit path, including a failing `BEGIN`. Moving `begin` inside the `try` is enough: the existing `catch` runs `rollback` (only a WARNING when no transaction is open) and `finally` releases. The error the caller sees stays the same `DrizzleQueryError`.
I have the fix plus a driver-mocked test for the three sessions; PR follows.
Contributor guide
Research direction
Start with the transaction implementations in drizzle-orm/src/node-postgres/session.ts, neon-serverless/session.ts, and vercel-postgres/session.ts, then inspect the driver-mocked tests mentioned in the issue. Verify that a failing BEGIN releases the pooled client for all three sessions, preserves the caller-visible DrizzleQueryError, and passes the relevant test suite.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, postgres, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100