Skip to content

Respect currentTime schema option in bulkWrite updates#15976

Merged
vkarpov15 merged 3 commits intoAutomattic:masterfrom
sderrow:fix-timestamps-value-in-bulk-write-updates
Jan 23, 2026
Merged

Respect currentTime schema option in bulkWrite updates#15976
vkarpov15 merged 3 commits intoAutomattic:masterfrom
sderrow:fix-timestamps-value-in-bulk-write-updates

Conversation

@sderrow
Copy link
Contributor

@sderrow sderrow commented Jan 21, 2026

Summary

Model.bulkWrite has inconsistent behavior when it comes to filling in createdAt and updatedAt with the current timestamp. If custom property names and a custom currentTime function are specified in the schema, then:

  • mongoose will respect the custom property names always
  • only respect the custom currentTime function on insertOne and replaceOne operations, but not updateOne and updateMany

I propose to unify the behavior of the different operations within bulkWrite by calling the custom currentTime schema and passing the value into the castUpdateOne and castUpdateMany functions.

Reproducible example of the issue

import mongoose from "mongoose";

type Person = {
  _id: mongoose.Types.ObjectId;
  name: string;
  created_at: number;
  updated_at: number;
};

const personSchema = new mongoose.Schema<Person>(
  {
    name: { type: String, required: true },
    created_at: { type: Number },
    updated_at: { type: Number },
  },
  {
    versionKey: false,
    timestamps: {
      createdAt: "created_at",
      updatedAt: "updated_at",
      currentTime: () => Date.now().valueOf() / 1000,
    },
  },
);

const Person = mongoose.model("Person", personSchema);

const main = async () => {
  await mongoose.connect("mongodb://localhost:27017/test");
  console.log("Connected to MongoDB");

  const alice = await Person.create({ name: "Alice" });
  const bob = await Person.create({ name: "Bob" });

  console.log("Everybody pre-changes:", JSON.stringify([alice, bob], null, 2));
  [
    {
      name: "Alice",
      _id: "696f29b40994418ab464eea6",
      created_at: 1768892852.062,
      updated_at: 1768892852.062,
    },
    {
      name: "Bob",
      _id: "696f29b40994418ab464eea8",
      created_at: 1768892852.082,
      updated_at: 1768892852.082,
    },
  ];

  await new Promise((resolve) => setTimeout(resolve, 1000));

  await Person.bulkWrite([
    { insertOne: { document: { name: "David" } } },
    { replaceOne: { filter: { _id: alice._id }, replacement: { name: "Alice 2" } } },
    { updateOne: { filter: { _id: bob._id }, update: { name: "Bob 2" } } },
    { updateOne: { filter: { name: "Charlie" }, update: { name: "Charlie" }, upsert: true } },
  ]);

  const everybody = await Person.find().sort({ name: 1 }).lean().exec();
  console.log("Everybody post-changes:", JSON.stringify(everybody, null, 2));

  [
    {
      _id: "696f29b40994418ab464eea6",
      name: "Alice 2",
      created_at: 1768892853.089, // replaceOne uses the currentTime custom function (it's increased by 1 second from the previous value)
      updated_at: 1768892853.089,
    },
    {
      _id: "696f29b40994418ab464eea8",
      name: "Bob 2",
      created_at: 1768892852.082,
      updated_at: 1768892853087, // updateOne/updateMany uses the default mongoose behavior of milliseconds, regardless of the currentTime custom function
    },
    {
      _id: "696f29b591bae2bde7a1d075",
      name: "Charlie",
      created_at: 1768892853087, // updateOne/updateMany uses the default behavior on upserts as well
      updated_at: 1768892853087,
    },
    {
      _id: "696f29b50994418ab464eeaa",
      name: "David",
      created_at: 1768892853.088, // insertOne uses the currentTime custom function
      updated_at: 1768892853.088,
    },
  ];

  await Person.deleteMany({});
  console.log("Deleted all people");
};

void main();

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes inconsistent behavior in Model.bulkWrite where custom currentTime functions defined in schema timestamps options were only being respected for insertOne and replaceOne operations, but not for updateOne and updateMany operations.

Changes:

  • Modified castBulkWrite to check for and use custom currentTime function from schema timestamps options
  • Added test coverage to verify custom currentTime functions are respected across different bulkWrite operations

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
lib/helpers/model/castBulkWrite.js Added logic to extract and use custom currentTime function from schema timestamps options, falling back to default originalModel.base.now() when not present
test/model.test.js Added test suite to verify that custom currentTime functions are respected in bulkWrite operations including insertOne, replaceOne, and updateOne with upsert

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@AbdelrahmanHafez AbdelrahmanHafez left a comment

Choose a reason for hiding this comment

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

A couple of suggestions mostly about the test case, otherwise LGTM. 👍

Copy link
Collaborator

@AbdelrahmanHafez AbdelrahmanHafez left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks!

Copy link
Collaborator

@vkarpov15 vkarpov15 left a comment

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 7e016bc into Automattic:master Jan 23, 2026
26 checks passed
@vkarpov15 vkarpov15 modified the milestones: 9.1.6, 8.21.1 Jan 23, 2026
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.

4 participants