Repository navigation
fix(model): preserve unsaved documents after ordered bulkSave errors - #16533
Conversation
vkarpov15
left a comment
There was a problem hiding this comment.
You're right that there's a bug in the ordered case, however the fix presented in this PR seems to break the unordered case. Also removes a performance optimization.
Ideal fix would be to handle the ordered and unordered cases separately. if (options.ordered) { /* successfulDocs are everything up to first write index error */ } else { /* check explicit indexes for unordered case */ }
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
lib/model.js uses the wrong write-error index property, causing failed and unattempted documents to be treated as successful.
Review effort: Lite
Findings: 1
What changed in this PR
Fixes bulkSave() handling after ordered bulk-write errors so unattempted documents remain retryable.
Changes:
- Tracks generated operations back to their source documents.
- Preserves state and skips success hooks for failed or unattempted writes.
- Adds regression coverage for ordered failures, retries, hooks, duplicate IDs, and mixed operations.
| File | Description |
|---|---|
test/model.test.js |
Adds ordered bulkSave() regression coverage. |
lib/model.js |
Maps bulk operation indexes to documents and classifies failures. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
vkarpov15
left a comment
There was a problem hiding this comment.
One minor improvement requested but otherwise looks good.
| } | ||
| const writeOperation = { insertOne: { document } }; | ||
| utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps); | ||
| documentsToWrite?.push(document); |
There was a problem hiding this comment.
Instead of returning the documentsToWrite, we should return the documentIndexes - documentIndexes.push(i) here. That way, the ordered case doesn't need an indexOf(), can just slice(0, documentIndexes[writeErrors[0].index]); and the unordered case can use a POJO instead of a Set() for faster initialization.
There was a problem hiding this comment.
Implemented in 780de3f. The mapping now stores original input indexes for both inserts and updates. Ordered handling slices directly at the mapped index, and unordered handling uses a plain object for larger batches while retaining the existing 25-document threshold.
Verified against MongoDB 8.2.3: the full model suite passes (403 passing, 15 pending), plus 26 additional ordered/unordered scenarios covering unchanged inputs, mixed writes, repeated IDs, hooks, retries, and threshold boundaries. ESLint and diff checks also pass.
A small local microbenchmark favored the object lookup for larger/error-heavy batches, but not every sparse small-batch case, so I am not claiming a universal speedup. The ordered path does eliminate the extra indexOf() scan.

Summary
Fixes #16532.
After an ordered bulk write fails, MongoDB leaves later operations unattempted.
bulkSave()currently treats every document without an individual write error as successful. It resets pending changes, marks unattempted inserts as no longer new, and runs their post-save hooks. The bulk promise rejects, but retrying those document instances can then omit writes that never reached the database.Track the original input document index for each generated operation and handle ordered and unordered errors separately:
slice()without anindexOf()search to select the preceding documents for successful-write handling. Treat an omittedorderedoption as ordered.Operation indexes must map back to documents because unchanged documents produce no operation and distinct instances can share an
_id. The optional internal mapping array is populated while operations are built. No public API changes.Examples
For new documents
[A, B, C], where A and B violate the same unique index:isNew === false, loses modified state, and receives a post-save hook despite being absent from the database. Retrying C does not insert it.The linked issue contains a complete reproduction. Six new tests cover small and 25-document ordered batches, pending updates with unchanged inputs, repeated
_idvalues, mixed inserts/updates, hooks, retries, and unordered failures followed by successful writes. The existing 25-document unordered regression remains covered. Tests use explicit declarations rather than a loop.Validation
Current revision
780de3f, Windows, Node 22.17.1, MongoDB 8.2.3, MongoDB driver 7.6.0:test/model.test.js: 403 passing, 15 pending.git diff --check: passed.Based on master
940de5d2c62dc29104b664a0e15be8d4aae38e91. Duplicate checks included issues and open/closed PRs about ordered bulkSave, skipped writes, retries, and duplicate IDs; related work is distinguished in the issue.