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

Inconsistent behaviour of isDirectModified and ignoreAtomics #14502

Closed
2 tasks done
khanguyen-unity opened this issue Apr 5, 2024 · 2 comments
Closed
2 tasks done

Inconsistent behaviour of isDirectModified and ignoreAtomics #14502

khanguyen-unity opened this issue Apr 5, 2024 · 2 comments
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@khanguyen-unity
Copy link

khanguyen-unity commented Apr 5, 2024

Prerequisites

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

Mongoose version

7.6.9

Node.js version

20.11.0

MongoDB server version

5.0

Typescript version (if applicable)

4.9.5

Description

{ ignoreAtomics: true }

The { ignoreAtomics: true } option was introduced in Mongoose 6.12.5 as a solution for issue #14024. The intended behavior is that, when using { ignoreAtomics: true }, the isModified method should return false for elements not affected by atomic operators such as push and pull. While I've confirmed that this option functions correctly with push, its behavior appears inconsistent with the pull operator.

When I pull comment1 or comment2 out of the comments array, all other elements are modified.

blogPost.init({
  title: 'Test',
  slug: 'test',
  comments: [comment1, comment2, comment3],
});

console.log('Is comment1.title modified: ', blogPost.comments[0].isModified('title')); // -> False (expected)
console.log('Is comment3.title modified: ', blogPost.comments[2].isModified('title')); // -> False (expected)

blogPost.comments.pull({ _id: comment2._id });
// Now there are 2 elements in blogPost.comments
console.log(
  'Is comment1.title modified after pull: ',
  blogPost.comments[0].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False
console.log(
  'Is comment3.title modified after pull: ',
  blogPost.comments[1].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False

But when I pull comment3 out of the array, the rest of the array are left unmodified.

blogPost.init({
  title: 'Test',
  slug: 'test',
  comments: [comment1, comment2, comment3],
});

console.log('Is comment1.title modified: ', blogPost.comments[0].isModified('title')); // -> False (expected)
console.log('Is comment3.title modified: ', blogPost.comments[2].isModified('title')); // -> False (expected)

blogPost.comments.pull({ _id: comment3._id });
// Now there are 2 elements in blogPost.comments
console.log(
  'Is comment1.title modified after pull: ',
  blogPost.comments[0].isModified('title', { ignoreAtomics: true }),
); // -> False (expected)
console.log(
  'Is comment3.title modified after pull: ',
  blogPost.comments[1].isModified('title', { ignoreAtomics: true }),
); //  False (expected)

isDirectModified

The behavior of isDirectModified is inconsistent, depending on what level isDirectModified is called.

const update = {
  title: 'New test',
  jsonField: {
    fieldA: 'new Field A',
  },
};

Object.assign(comment1, update);
console.log(
  'Is jsonField.fieldA of the first comment directly modified: ',
  comment1.isDirectModified('jsonField.fieldA'),
); // -> False (should be True)

console.log(
  'Is fieldA of the first comment directly modified: ',
  (comment1.jsonField as unknown as Document).isDirectModified('fieldA'),
); // -> True
console.log(
  'Is the fieldB of the first comment directly modified: ',
  (comment1.jsonField as unknown as Document).isDirectModified('fieldB'),
); // -> False

Steps to Reproduce

import { Schema, model, Document } from 'mongoose';

const JsonFieldSchema = new Schema({
  fieldA: String,
  fieldB: String,
});

const CommentSchema = new Schema({
  title: String,
  body: String,
  jsonField: JsonFieldSchema,
});

const JsonField = model('Jsonfield', JsonFieldSchema);

const Comments = model('Comments', CommentSchema);

const BlogPostSchema = new Schema({
  title: String,
  slug: String,
  comments: [CommentSchema],
});

const BlogPost = model('BlogPost', BlogPostSchema);

describe('test isModified', () => {
  it('{ ignoreAtomics: true }', async () => {
    const blogPost = new BlogPost();
    const comment1 = new Comments({
      title: 'Test',
      body: 'Test',
      jsonField: new JsonField({
        fieldA: 'field A',
        fieldB: 'field B',
      }),
    });

    const comment2 = new Comments({
      title: 'Test 2',
      body: 'Test 2',
      jsonField: new JsonField({
        fieldA: 'field A 2',
        fieldB: 'field B 2',
      }),
    });

    const comment3 = new Comments({
      title: 'Test 3',
      body: 'Test 3',
      jsonField: new JsonField({
        fieldA: 'a',
        fieldB: 'b',
      }),
    });

    blogPost.init({
      title: 'Test',
      slug: 'test',
      comments: [comment1, comment2, comment3],
    });

    console.log('Is comment1.title modified: ', blogPost.comments[0].isModified('title')); // -> False (expected)
    console.log('Is comment3.title modified: ', blogPost.comments[2].isModified('title')); // -> False (expected)

    blogPost.comments.pull({ _id: comment3._id });
    // Now there are 2 elements in blogPost.comments
    console.log(
      'Is comment1.title modified after pull: ',
      blogPost.comments[0].isModified('title', { ignoreAtomics: true }),
    ); // -> True, expected False
    console.log(
      'Is comment3.title modified after pull: ',
      blogPost.comments[1].isModified('title', { ignoreAtomics: true }),
    ); // -> True, expected False
  });

  it('isDirectModified', () => {
    const comment1 = new Comments({
      title: 'Test',
      body: 'Test',
      jsonField: new JsonField({
        fieldA: 'field A',
        fieldB: 'field B',
      }),
    });

    const update = {
      title: 'New test',
      jsonField: {
        fieldA: 'new Field A',
      },
    };

    Object.assign(comment1, update);
    console.log(
      'Is jsonField.fieldA of the first comment directly modified: ',
      comment1.isDirectModified('jsonField.fieldA'),
    ); // -> False (should be True)

    console.log(
      'Is fieldA of the first comment directly modified: ',
      (comment1.jsonField as unknown as Document).isDirectModified('fieldA'),
    ); // -> True
    console.log(
      'Is the fieldB of the first comment directly modified: ',
      (comment1.jsonField as unknown as Document).isDirectModified('fieldB'),
    ); // -> False
  });
});

Expected Behavior

  • ignoreAtomics works correctly for both push and pull.
  • The behaviour of isDirectModified is consistent for the same path.
@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Apr 8, 2024
@IslandRhythms
Copy link
Collaborator

Output is different but some is the same

import { Schema, model, Document } from 'mongoose';

const JsonFieldSchema = new Schema({
  fieldA: String,
  fieldB: String,
});

const CommentSchema = new Schema({
  title: String,
  body: String,
  jsonField: JsonFieldSchema,
});

const JsonField = model('Jsonfield', JsonFieldSchema);

const Comments = model('Comments', CommentSchema);

const BlogPostSchema = new Schema({
  title: String,
  slug: String,
  comments: [CommentSchema],
});

const BlogPost = model('BlogPost', BlogPostSchema);

const blogPost = new BlogPost();
const comment1 = new Comments({
  title: 'Test',
  body: 'Test',
  jsonField: new JsonField({
    fieldA: 'field A',
    fieldB: 'field B',
  }),
});

const comment2 = new Comments({
  title: 'Test 2',
  body: 'Test 2',
  jsonField: new JsonField({
    fieldA: 'field A 2',
    fieldB: 'field B 2',
  }),
});

const comment3 = new Comments({
  title: 'Test 3',
  body: 'Test 3',
  jsonField: new JsonField({
    fieldA: 'a',
    fieldB: 'b',
  }),
});

blogPost.init({
  title: 'Test',
  slug: 'test',
  comments: [comment1, comment2, comment3],
});

console.log('Is comment1.title modified: ', blogPost.comments[0].isModified('title')); // -> False (expected)
console.log('Is comment3.title modified: ', blogPost.comments[2].isModified('title')); // -> False (expected)

blogPost.comments.pull({ _id: comment3._id });
// Now there are 2 elements in blogPost.comments
console.log(
  'Is comment1.title modified after pull: ',
  blogPost.comments[0].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False
console.log(
  'Is comment3.title modified after pull: ',
  blogPost.comments[1].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False

const comment4 = new Comments({
  title: 'Test',
  body: 'Test',
  jsonField: new JsonField({
    fieldA: 'field A',
    fieldB: 'field B',
  }),
});

const update = {
  title: 'New test',
  jsonField: {
    fieldA: 'new Field A',
  },
};

Object.assign(comment4, update);
console.log(
  'Is jsonField.fieldA of the first comment directly modified: ',
  comment4.isDirectModified('jsonField.fieldA'),
); // -> False (should be True)

console.log(
  'Is fieldA of the first comment directly modified: ',
  (comment4.jsonField as unknown as Document).isDirectModified('fieldA'),
); // -> True
console.log(
  'Is the fieldB of the first comment directly modified: ',
  (comment4.jsonField as unknown as Document).isDirectModified('fieldB'),
); // -> False

Output:

Is comment1.title modified:  false
Is comment3.title modified:  false
Is comment1.title modified after pull:  false
Is comment3.title modified after pull:  false
Is jsonField.fieldA of the first comment directly modified:  false
Is fieldA of the first comment directly modified:  true
Is the fieldB of the first comment directly modified:  false

@khanguyen-unity
Copy link
Author

khanguyen-unity commented Apr 10, 2024

@IslandRhythms Thanks for checking out. Actually, I forgot to adjust the script here

blogPost.comments.pull({ _id: comment3._id });

If you remove comment2 instead of comment3 with blogPost.comments.pull({ _id: comment2._id });, then our outputs should match.

blogPost.comments.pull({ _id: comment2._id });
// Now there are 2 elements in blogPost.comments
console.log(
  'Is comment1.title modified after pull: ',
  blogPost.comments[0].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False
console.log(
  'Is comment3.title modified after pull: ',
  blogPost.comments[1].isModified('title', { ignoreAtomics: true }),
); // -> True, expected False

The issue here is basically isModified('title', { ignoreAtomics: true }) only works if I pull the last element of the array.

@vkarpov15 vkarpov15 added this to the 7.6.12 milestone Apr 16, 2024
vkarpov15 added a commit that referenced this issue Apr 22, 2024
vkarpov15 added a commit that referenced this issue Apr 22, 2024
vkarpov15 added a commit that referenced this issue Apr 27, 2024
fix(array): avoid converting to `$set` when calling `pull()` on an element in the middle of the array
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