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)

Open Beginner friendly
#5,994 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

## 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.