Skip to content

fix(array): compare dates by value in indexOf() so pull() removes them - #16535

Open
giaBaoJS wants to merge 1 commit into
Automattic:masterfrom
giaBaoJS:fix/array-pull-dates
Open

giaBaoJS wants to merge 1 commit into
Automattic:masterfrom
giaBaoJS:fix/array-pull-dates

Conversation

@giaBaoJS

Copy link
Copy Markdown
Contributor

Summary

pull() on a [Date] array does not remove the date from the document, even though the $pullAll it registers does remove it from the database.

pull() looks for matches with this.indexOf.call(values, mem), and MongooseArray#indexOf() compares with ==. That works for numbers and strings, and ObjectIds are converted to strings first, but two Date objects are only == when they are the same instance. The value passed to pull() is cast to a new Date, so it never matches.

The local array and the database then disagree after save(). It gets worse when the same array is also pushed to before saving: $pullAll and $push can't be combined, so the atomics fall back to $set of the local array, and the date that was pulled is written back.

addToSet() already handles this case with its own date branch that compares +d === val, so addToSet() and pull() currently disagree about whether two equal dates are the same value. This PR does the same timestamp comparison in indexOf(). pull() and remove() pick it up through their existing call, and includes() / indexOf() now find dates by value too, the same way they already find ObjectIds by value.

Not changed here: Decimal128, Double, UUID and Buffer arrays also compare by reference, in both addToSet() and pull(). Unlike dates, neither method has an equality rule for those types today, so that needs a per-type decision rather than making the two methods agree. Happy to follow up if you want that.

Examples

const Test = mongoose.model('Test', new Schema({ dates: [Date] }));
const { _id } = await Test.create({ dates: [new Date('2026-01-01'), new Date('2026-02-01')] });

const doc = await Test.findById(_id);
doc.dates.pull(new Date('2026-01-01'));
doc.dates.length; // 2 before, 1 after

doc.dates.push(new Date('2026-03-01'));
await doc.save();
(await Test.findById(_id).lean()).dates;
// before: [2026-01-01, 2026-02-01, 2026-03-01]
// after:  [2026-02-01, 2026-03-01]

Added two tests in test/types.array.test.js: one under pull() that covers both the local array and the saved result after pull() + push(), and one under indexOf() for indexOf() / includes() on dates, including fromIndex and a Mixed array holding the same timestamp as a number (which must still not match a Date). Both fail on master and pass with this change.

MongooseArray#pull() finds matches with MongooseArray#indexOf(), which
compares with `==`. Two Date objects are only `==` when they are the
same instance, so pulling a date left it in the local array while the
$pullAll still removed it on save. If the array was also pushed to
before saving, the atomics fall back to $set and the pulled date was
written back.

addToSet() already compares dates by timestamp; do the same in
indexOf(), which also makes includes() work for dates.

This branch has not been deployed

No deployments
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.

1 participant