drizzle-team / drizzle-team/drizzle-orm
Batch insert crashes with RangeError when row-count × column-count exceeds ~8 k rows × 15 cols (~120 k params)
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
## Bug: batch insert throws `RangeError: Maximum call stack size exceeded` at ~8 000 rows × 15 columns
**Affected:** `drizzle-orm` (pg dialect) — `db.insert(table).values(rows).toSQL()` / `.execute()`
**Severity:** crash (data loss risk — the insert never fires)
**Reproducible in:** `drizzle-orm` latest
---
### Reproduction
```ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { pgTable, serial, integer } from 'drizzle-orm/pg-core';
const db = drizzle(client);
const cols = Array.from({ length: 15 }, (_, i) => `c${i}`);
const wide = pgTable('wide', Object.fromEntries([
['id', serial('id').primaryKey()],
...cols.map(c => [c, integer(c)]),
]));
// Fine at 5 000 rows (75 000 params):
db.insert(wide).values(Array.from({ length: 5_000 }, (_, i) =>
Object.fromEntries(cols.map((c, j) => [c, i * 15 + j]))
)).toSQL();
// Crashes at 8 000 rows (120 000 params):
db.insert(wide).values(Array.from({ length: 8_000 }, (_, i) =>
Object.fromEntries(cols.map((c, j) => [c, i * 15 + j]))
)).toSQL();
// → RangeError: Maximum call stack size exceeded
```
Benchmarked crash boundary:
| rows | params | result |
|------|--------|--------|
| 5 000 | 75 000 | OK (185 ms) |
| 8 000 | ~120 000 | **CRASH** |
---
### Root cause
`mergeQueries` in `src/sql/sql.ts` (line ~74) collects bound parameters with:
```ts
result.params.push(...query.params);
```
`buildQueryFromSourceParams` is called recursively — once per SQL chunk — so a param introduced at nesting depth *d* is spread into `result.params` at each level on the way back up. For a bulk insert, `buildInsertQuery` wraps values in multiple `sql\`\`` template layers, so each row's params are spread multiple times. At ~125 000 total spread arguments V8 throws `RangeError: Maximum call stack size exceeded` because `Function.prototype.apply` (which `push(...arr)` uses internally) has a hard argument-count limit.
**Fix:** replace spread with a loop:
```ts
// before
result.params.push(...query.params);
// after
for (const p of query.params) result.params.push(p);
// or: result.params = result.params.concat(query.params);
```
This eliminates the apply-argument-count limit and also removes the O(depth)-copies-per-param behaviour, making param collection O(total_params) instead of O(total_params × nesting_depth).
---
### Notes
- The same `mergeQueries` function also does `result.sql += query.sql` across all chunks (O(N²) string copies), though V8 rope optimisation absorbs that in practice. The param spread is the hard crash.
- Workaround until fixed: use `.prepare()` with a static query + individual executes, or chunk the batch into groups of ≤ 5 000 rows.
benchmark confirmed
Contributor guide
Research direction
Start in src/sql/sql.ts around mergeQueries, then reproduce the issue through db.insert(...).values(rows).toSQL() using the 8,000-row, 15-column example. Verify that large parameter collections no longer throw RangeError and that the generated SQL and parameters remain correct; the payload does not name a specific test file.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100