Skip to content

driver-memory: bulkCreate is Promise.all(map(create)), so a refused row leaves every earlier row of the batch landed — updateMany on the same driver refuses before mutating anything #13340

Description

@claude

Found while implementing #13239 (object-level declared indexes[] uniqueness in driver-memory). Not folded into that PR — different defect class, and it predates both uniqueness cards. Unassigned; grading and routing are triage's.

Triage routing, stated up front

#5499 (maintainer 2026-08-05) freezes defect-fix investment in driver-memory, with a standing rule that new cards in this family carry domain:engine and go straight to pm:on-hold referencing it rather than into pm:queue. This card is filed that way. It is recorded, not queued.

Whether it reaches the ruling's exception channel — a driver-memory semantics error that makes CI green wrongly — is triage's call, and the argument is weak here: bulkCreate is not used as a uniqueness oracle by any suite this card found, so the false-green exposure is smaller than the one #13197 / #13239 were escalated on.

The observation

InMemoryDriver.bulkCreate is one line:

constresults=awaitPromise.all(dataArray.map(data=>this.create(object,data,options)));

create writes into the table synchronously, so the batch is not atomic: when any row in it is refused, every row accepted BEFORE the refusal stays in the store, and the caller gets a rejection describing a batch that partly landed. Measured while writing #13239's suite, on a two-row batch whose second row collides with its first:

before: 2 rows
bulkCreate([A9/Z, A9/Z]) -> rejects with UNIQUE_VIOLATION / 409
after: 3 rows (the FIRST batch row landed; the duplicate did not)

The refusal itself is correct — the duplicate is refused, which is what #13197 and #13239 pin. What is wrong is the surviving prefix.

Why it is worth recording

It is older than uniqueness: any mid-batch create failure has always left a partial batch, so a caller's retry of the same array is unsafe on this driver for reasons that have nothing to do with constraints. And it is a divergence from the family this driver stands in for — driver-sql issues a batch insert inside one statement, so a constraint failure there leaves the table untouched. updateMany on this same driver already takes the opposite, correct posture: #13197 made it prepare and check every row before mutating any of it, precisely so a half-applied batch cannot happen. bulkCreate never got that treatment, so the two batch paths of one driver disagree about whether a batch is all-or-nothing.

The shape a fix would take is updateMany's, one method over: build every stored record, check the whole set (against the table and against each other), then push. create's own single-row path is unchanged by that.

Where it is

packages/drivers/driver-memory/src/memory-driver.tsbulkCreate, with updateMany in the same file as the pattern to copy.

Not claimed here

No position on whether it should be fixed. This driver is documented as a deliberately WEAK oracle, and "batches are not atomic here, use SQLite when that matters" is a legitimate answer — it is simply not the answer anywhere in the tree states today. #13239's suite records the current behaviour explicitly rather than silently, so nothing is asserting the atomic reading.

Related

#5499 (the investment freeze this is filed under) - #13197 / #13239 (the uniqueness cards that surfaced it; updateMany's check-before-mutate came from the first)

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions