Skip to content

fix(model): don't mutate the caller's update object in bulkWrite updateMany - #16526

Merged
vkarpov15 merged 1 commit into
Automattic:masterfrom
fadiroot:fix/bulkwrite-updatemany-clone
Sep 24, 2026
Merged

vkarpov15 merged 1 commit into
Automattic:masterfrom
fadiroot:fix/bulkwrite-updatemany-clone

Conversation

@fadiroot

Copy link
Copy Markdown

Summary

Model.bulkWrite() with an updateMany op mutates the update object the caller passed in.

castUpdateOne() clones the update before applying timestamps, defaults and the version key (added in #14164, "deep clone bulkWrite() updateOne arguments to avoid mutating documents in update"), but castUpdateMany() never received the same treatment: it runs setDefaultsOnInsert(), applyTimestampsToUpdate(), applyTimestampsToChildren() and decorateUpdateWithVersionKey() directly on updateMany.update.

const schema = new Schema({ name: String }, { timestamps: true });
const Test = db.model('Test', schema);

const update = { $set: { name: 'Renamed' } };
await Test.bulkWrite([{ updateMany: { filter: {}, update } }]);
console.log(update); // { $set: { name: 'Renamed', updatedAt: 2026-... } }  <- mutated

With updateOne instead of updateMany, update comes back unchanged. The gh-14164 scenario (update: { $set: doc }) likewise writes updatedAt into the caller's document for updateMany.

This PR clones the update in castUpdateMany() exactly the way castUpdateOne() does, and passes the clone to every helper; the cast result is still assigned back to updateMany.update.

Examples

Added should not modify the update object passed to updateMany (gh-14164) in test/model.test.js next to the existing gh-14164 test. It covers both the $set: doc shape and a plain update object, and checks the write itself still lands. It fails on master and passes with this change.

…teMany

castUpdateOne() clones the update before applying timestamps, defaults and
the version key (Automattic#14164), but castUpdateMany() never did: it ran
setDefaultsOnInsert(), applyTimestampsToUpdate(), applyTimestampsToChildren()
and decorateUpdateWithVersionKey() directly on `updateMany.update`. So
`bulkWrite([{ updateMany: { filter, update: { $set: doc } } }])` wrote
`updatedAt` into the caller's document, and a plain update object came back
with extra keys, the same problem Automattic#14164 fixed for updateOne.

Clone the update first, the same way castUpdateOne() does.

Re: Automattic#14164
@vkarpov15 vkarpov15 added this to the 9.10.3 milestone Sep 24, 2026

@vkarpov15 vkarpov15 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍

@vkarpov15
vkarpov15 merged commit 940de5d into Automattic:master Sep 24, 2026
26 checks passed
vkarpov15 added a commit that referenced this pull request Oct 19, 2026
…e-updatemany-clone

fix(model): don't mutate the caller's update object in bulkWrite updateMany (backport #16526 to 8.x)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants