Skip to content

[Bug] Version mode can be downgraded when modifying multiple paths #15888

@AbdelrahmanHafez

Description

@AbdelrahmanHafez

Prerequisites

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

Mongoose version

9.0.

Node.js version

N/A

MongoDB server version

N/A

Typescript version (if applicable)

N/A

Description

When modifying multiple paths in a single save, the version mode (this.$__.version) can be incorrectly downgraded. This happens because the versioning logic uses direct assignment instead of accumulating with bitwise OR.

For example, if you:

  1. Push to a top-level array (should set VERSION_INC = 2)
  2. Set a value on an array index path (should set VERSION_WHERE = 1)

The second operation overwrites the first, resulting in VERSION_WHERE (1) instead of VERSION_ALL (3). This means __v is added to the query filter but NOT incremented, even though we pushed to an array.

Steps to Reproduce

const mongoose = require('mongoose');
const assert = require('assert');

run();

async function run() {
  await mongoose.connect('mongodb://127.0.0.1:27017/test');

  const Model = mongoose.model('Test', new mongoose.Schema({
    items: [{ name: String }],
    tags: [String]
  }));

  const doc = await Model.create({
    items: [{ name: 'item1' }, { name: 'item2' }, { name: 'item3' }],
    tags: ['tag']
  });

  // Two clients load the same document
  const clientA = await Model.findById(doc._id);
  const clientB = await Model.findById(doc._id);

  // Client A modifies the document
  clientA.items.pull(clientA.items[0]._id); // pull (VERSION_INC)
  clientA.tags[0] = 'modified'; // set index (VERSION_WHERE overwrites it)
  await clientA.save();
  // items is now ['item2', 'item3'], __v should be 1

  const afterA = await Model.findById(doc._id);
  assert.strictEqual(afterA.__v, 1, '__v should be incremented after pull');

  // Client B has stale view, thinks items[1] is "item2"
  clientB.items[1].name = 'item2-updated';
  const err = await clientB.save().then(() => null, err => err);
  assert.ok(err, 'save should throw VersionError due to stale __v');
  assert.strictEqual(err.name, 'VersionError');
}

Expected Behavior

When modifying multiple paths that trigger different version modes, the modes should be combined using bitwise OR:

  • VERSION_WHERE (1) + VERSION_INC (2) = VERSION_ALL (3)

This ensures that:

  1. __v is included in the query filter (for index safety)
  2. __v is incremented (to signal array position changes to other clients)

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions