Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mongoose bulkWrite Inconsistency: Alters Model Instance and Fails on Schemas with Timestamps #14164

Closed
2 tasks done
NavpreetDevpuri opened this issue Dec 7, 2023 · 1 comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@NavpreetDevpuri
Copy link

NavpreetDevpuri commented Dec 7, 2023

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.0.2

Node.js version

21.0.0

MongoDB server version

6.0.4

Typescript version (if applicable)

No response

Description

The primary issue lies with the bulkWrite operation in Mongoose. When using a Model instance in the $set clause, the behavior varies depending on the model's schema configuration:

  • With Timestamps: The bulkWrite operation unexpectedly modifies the original object passed in the $set clause.
  • Without Timestamps: The operation fails, throwing an error "Update document requires atomic operators".

In contrast, the updateOne method works correctly in both scenarios, maintaining the integrity of the original object and completing the update without errors. This difference emphasizes the specific challenge with bulkWrite in handling Model instances.

Steps to Reproduce

Following code reproduce the issue

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/test', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

const productSchema = new mongoose.Schema({
  name: String,
  properties: { type: mongoose.Schema.Types.Mixed, default: {} },
}, {
  timestamps: true
});

const productSchemaWithoutTimestamps = new mongoose.Schema({
  name: String,
  properties: { type: mongoose.Schema.Types.Mixed, default: {} },
});

const Product = mongoose.model('Product', productSchema);
const ProductWithoutTimestamps = mongoose.model('ProductWithoutTimestamps', productSchemaWithoutTimestamps);

function printLogsForProduct(Model, product) {
  console.log(`   JSON.stringify(product) -> ${JSON.stringify(product)}`);
  console.log(`   typeof product -> ${typeof product}`);
  console.log(`   product instanceof Model -> ${product instanceof Model}`);
  console.log(`   product._id -> ${product._id}`);
}

async function tryBulkWriteUpdateOne(Model, product) {
  product.properties.color = "Red";
  console.log(`\n${Model.modelName}: Before bulkWrite: `)
  printLogsForProduct(Model, product);
  await Model.bulkWrite([{
    updateOne: {
      filter: { _id: product._id },
      update: { $set: product },
    }
  }]);
  console.log(`\n${Model.modelName}: After bulkWrite: `)
  printLogsForProduct(Model, product);
}

async function tryUpdateOne(Model, product) {
  product.properties.color = "Red";
  console.log(`\n${Model.modelName}: Before updateOne: `)
  printLogsForProduct(Model, product);
  await Model.updateOne({ _id: product._id }, { $set: product });
  console.log(`\n${Model.modelName}: After updateOne: `)
  printLogsForProduct(Model, product);
}

(async () => {
  try {
    await Product.insertMany([{ name: 'Product 1' }]);
    const product = await Product.findOne({}, { name: 1, properties: 1 }).exec();
    await tryUpdateOne(Product, product);
    await tryBulkWriteUpdateOne(Product, product);
  } catch (error) {
    console.log(`Product: error.message -> ${error.message}`)
  }
  console.log()
  try {
    await ProductWithoutTimestamps.insertMany([{ name: 'Product 1' }]);
    const product = await ProductWithoutTimestamps.findOne().exec();
    await tryUpdateOne(ProductWithoutTimestamps, product);
    await tryBulkWriteUpdateOne(ProductWithoutTimestamps, product);
  } catch (error) {
    console.log(`ProductWithoutTimestamps: error.message -> ${error.message}`)
  }
})().then(() => mongoose.connection.close());

Output:

Product: Before updateOne: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6e9","name":"Product 1","properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6e9

Product: After updateOne: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6e9","name":"Product 1","properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6e9

Product: Before bulkWrite: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6e9","name":"Product 1","properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6e9

Product: After bulkWrite: 
   JSON.stringify(product) -> {}
   typeof product -> object
   product instanceof Model -> true
Product: error.message -> Cannot read properties of undefined (reading 'Symbol(mongoose#Document#scope)')


ProductWithoutTimestamps: Before updateOne: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6ef","name":"Product 1","__v":0,"properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6ef

ProductWithoutTimestamps: After updateOne: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6ef","name":"Product 1","__v":0,"properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6ef

ProductWithoutTimestamps: Before bulkWrite: 
   JSON.stringify(product) -> {"_id":"6571dbfea1f89fdfa06dd6ef","name":"Product 1","__v":0,"properties":{"color":"Red"}}
   typeof product -> object
   product instanceof Model -> true
   product._id -> 6571dbfea1f89fdfa06dd6ef
ProductWithoutTimestamps: error.message -> Update document requires atomic operators

Expected Behavior

The expected behavior for Mongoose's bulkWrite operation, when using a Model instance in the $set clause, should be similar to the updateOne method:

  • The original object should remain unchanged after the operation.
  • Successful and consistent document update in the database, irrespective of the schema's timestamp configuration.
  • The process should be error-free, mirroring the reliable performance of updateOne.
@NavpreetDevpuri NavpreetDevpuri changed the title bulkWrite Functionality Issues with Timestamps in Model Schema Mongoose bulkWrite Inconsistency: Alters Model Instance and Fails on Schemas with Timestamps Dec 7, 2023
@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Dec 11, 2023
@IslandRhythms
Copy link
Collaborator

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/test', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

const productSchema = new mongoose.Schema({
  name: String,
  properties: { type: mongoose.Schema.Types.Mixed, default: {} },
}, {
  timestamps: true
});

const productSchemaWithoutTimestamps = new mongoose.Schema({
  name: String,
  properties: { type: mongoose.Schema.Types.Mixed, default: {} },
});

const Product = mongoose.model('Product', productSchema);
const ProductWithoutTimestamps = mongoose.model('ProductWithoutTimestamps', productSchemaWithoutTimestamps);

function printLogsForProduct(Model, product) {
  console.log(`   JSON.stringify(product) -> ${JSON.stringify(product)}`);
  console.log(`   typeof product -> ${typeof product}`);
  console.log(`   product instanceof Model -> ${product instanceof Model}`);
  console.log(`   product._id -> ${product._id}`);
}

async function tryBulkWriteUpdateOne(Model, product) {
  product.properties.color = "Red";
  console.log(`\n${Model.modelName}: Before bulkWrite: `)
  printLogsForProduct(Model, product);
  await Model.bulkWrite([{
    updateOne: {
      filter: { _id: product._id },
      update: { $set: product },
    }
  }]);
  console.log(`\n${Model.modelName}: After bulkWrite: `)
  printLogsForProduct(Model, product);
}

async function tryUpdateOne(Model, product) {
  product.properties.color = "Red";
  console.log(`\n${Model.modelName}: Before updateOne: `)
  printLogsForProduct(Model, product);
  await Model.updateOne({ _id: product._id }, { $set: product });
  console.log(`\n${Model.modelName}: After updateOne: `)
  printLogsForProduct(Model, product);
}

(async () => {
  try {
    await Product.insertMany([{ name: 'Product 1' }]);
    const product = await Product.findOne({}, { name: 1, properties: 1 }).exec();
    await tryUpdateOne(Product, product);
    await tryBulkWriteUpdateOne(Product, product);
  } catch (error) {
    console.log(`Product: error.message -> ${error.message}`)
  }
  console.log()
  try {
    await ProductWithoutTimestamps.insertMany([{ name: 'Product 1' }]);
    const product = await ProductWithoutTimestamps.findOne().exec();
    await tryUpdateOne(ProductWithoutTimestamps, product);
    await tryBulkWriteUpdateOne(ProductWithoutTimestamps, product);
  } catch (error) {
    console.log(`ProductWithoutTimestamps: error.message -> ${error.message}`)
  }
})().then(async () => {await mongoose.connection.dropDatabase(); mongoose.connection.close();});

@vkarpov15 vkarpov15 added this to the 8.0.4 milestone Dec 18, 2023
vkarpov15 added a commit that referenced this issue Dec 30, 2023
fix(model): deep clone bulkWrite() updateOne arguments to avoid mutating documents in update
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Projects
None yet
Development

No branches or pull requests

3 participants