Found while implementing #13340 (which made bulkCreate all-or-nothing). Not folded into that PR — different methods, and the fix is not the mechanical one #13340 got, for the reason set out below. Unassigned; grading and routing are triage's.
The observation
#13340 fixed bulkCreate, and #13197 had already fixed updateMany. Two batch doors of InMemoryDriver are still the pre-#13340 shape, in packages/drivers/driver-memory/src/memory-driver.ts (line numbers against c4e841368b, the #13340 branch head — re-derive them, they move):
// ~line 741 -- "Compatibility aliases"asyncbulkUpdate(object,updates,options){constresults=awaitPromise.all(updates.map(u=>this.update(object,u.id,u.data,options)));returnresults;}asyncbulkDelete(object,ids,options){awaitPromise.all(ids.map(id=>this.delete(object,id,options)));}update writes into the table synchronously and calls assertUnique, so bulkUpdate has exactly the defect #13340 measured on bulkCreate: when one row of the batch is refused with UNIQUE_VIOLATION / 409, every row applied before it stays mutated, and the caller gets a rejection describing a batch that partly landed. bulkDelete is the same shape against delete, which throws on a missing row when strictMode is on.
So after #13340 the driver has four batch doors giving two different answers to "is a batch atomic?":
| door | posture | since |
|---|
updateMany | check-then-mutate | #13197 |
bulkCreate | check-then-push | #13340 |
bulkUpdate | Promise.all(map(update)) — prefix survives | — |
bulkDelete | Promise.all(map(delete)) — prefix survives | — |
Why this was NOT folded into #13340
It fails the bounded-in-place-fix test on one criterion: the fix is not mechanical, so copying updateMany's shape into it would be forcing the pattern rather than transferring it. Three things have to be decided first, and none of them is settled by existing evidence:
bulkUpdate applies a DIFFERENT patch per id.updateMany stamps one data onto every matched row, and bulkCreate has no pre-image to exclude. bulkUpdate needs, per pending row, its own exceptIdand a projected row set holding the other rows' post-images while dropping their pre-images. That construction exists nowhere in the file yet.- Non-strict missing rows.
update returns null rather than throwing when the id is absent and strictMode is off. Check-then-mutate has to decide whether a missing id refuses the whole batch or is skipped, which changes what the returned array means to callers. bulkDelete returns void and swallows per-row outcomes entirely, so "atomic delete" needs a decision about what it reports before it can be about what it does.
These are semantics questions, not a transcription. #13340's dispatch was explicit that if the updateMany pattern needs something the target method does lazily, that is a finding to report rather than a shape to force — this is that case, one method further on.
Scope note
Also worth deciding as part of this: bulkCreate/bulkUpdate/bulkDelete are labelled "Compatibility aliases" in the source. If they are genuinely legacy surface, "these three are not atomic, use the non-alias doors" is a legitimate answer — it is simply not the answer anywhere in the tree today, and the docstring on InMemoryDriver now advertises batch atomicity in general terms after #13340, which reads as covering all four doors.
Where it is
packages/drivers/driver-memory/src/memory-driver.ts — bulkUpdate and bulkDelete, with bulkCreate (post-#13340) and updateMany in the same file as the two proven patterns.
Related
#13340 (made bulkCreate all-or-nothing; found there) · #13197 / #13239 (the uniqueness cards; updateMany's check-before-mutate came from the first)
Found while implementing #13340 (which made
bulkCreateall-or-nothing). Not folded into that PR — different methods, and the fix is not the mechanical one #13340 got, for the reason set out below. Unassigned; grading and routing are triage's.The observation
#13340 fixed
bulkCreate, and #13197 had already fixedupdateMany. Two batch doors ofInMemoryDriverare still the pre-#13340 shape, inpackages/drivers/driver-memory/src/memory-driver.ts(line numbers againstc4e841368b, the #13340 branch head — re-derive them, they move):updatewrites into the table synchronously and callsassertUnique, sobulkUpdatehas exactly the defect #13340 measured onbulkCreate: when one row of the batch is refused withUNIQUE_VIOLATION/ 409, every row applied before it stays mutated, and the caller gets a rejection describing a batch that partly landed.bulkDeleteis the same shape againstdelete, which throws on a missing row whenstrictModeis on.So after #13340 the driver has four batch doors giving two different answers to "is a batch atomic?":
updateManybulkCreatebulkUpdatePromise.all(map(update))— prefix survivesbulkDeletePromise.all(map(delete))— prefix survivesWhy this was NOT folded into #13340
It fails the bounded-in-place-fix test on one criterion: the fix is not mechanical, so copying
updateMany's shape into it would be forcing the pattern rather than transferring it. Three things have to be decided first, and none of them is settled by existing evidence:bulkUpdateapplies a DIFFERENT patch per id.updateManystamps onedataonto every matched row, andbulkCreatehas no pre-image to exclude.bulkUpdateneeds, per pending row, its ownexceptIdand a projected row set holding the other rows' post-images while dropping their pre-images. That construction exists nowhere in the file yet.updatereturnsnullrather than throwing when the id is absent andstrictModeis off. Check-then-mutate has to decide whether a missing id refuses the whole batch or is skipped, which changes what the returned array means to callers.bulkDeletereturnsvoidand swallows per-row outcomes entirely, so "atomic delete" needs a decision about what it reports before it can be about what it does.These are semantics questions, not a transcription. #13340's dispatch was explicit that if the
updateManypattern needs something the target method does lazily, that is a finding to report rather than a shape to force — this is that case, one method further on.Scope note
Also worth deciding as part of this:
bulkCreate/bulkUpdate/bulkDeleteare labelled "Compatibility aliases" in the source. If they are genuinely legacy surface, "these three are not atomic, use the non-alias doors" is a legitimate answer — it is simply not the answer anywhere in the tree today, and the docstring onInMemoryDrivernow advertises batch atomicity in general terms after #13340, which reads as covering all four doors.Where it is
packages/drivers/driver-memory/src/memory-driver.ts—bulkUpdateandbulkDelete, withbulkCreate(post-#13340) andupdateManyin the same file as the two proven patterns.Related
#13340 (made
bulkCreateall-or-nothing; found there) · #13197 / #13239 (the uniqueness cards;updateMany's check-before-mutate came from the first)