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

findOneAndUpdate with upsert sets schema defaults on the filter #13962

Closed
2 tasks done
Kezzel opened this issue Oct 10, 2023 · 1 comment · Fixed by #13983
Closed
2 tasks done

findOneAndUpdate with upsert sets schema defaults on the filter #13962

Kezzel opened this issue Oct 10, 2023 · 1 comment · Fixed by #13983
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@Kezzel
Copy link

Kezzel commented Oct 10, 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

7.5.3

Node.js version

16.14.0

MongoDB server version

4.0 (documentDB)

Typescript version (if applicable)

No response

Description

When using findOneAndUpdate with upsert, the defaults of the schema is being applied to the filter.

The below steps to reproduce shows the problem. Inside castUpdate.js (

return { $setOnInsert: filter };
) the $setOnInsert is being set to the actual object of filter (not a copy).

Thus when later on applying defaults, the reference to filter makes it update both the $setOnInsert in the update AND the filter.

This caused the filter in findOneAndUpdate to NOT match the expected docs and thus create a new one.

The bug has been detected in 7.5.3, but the code suggests that this is still an issue in 8.0.0 (https://github.com/Automattic/mongoose/blob/e15940051f868f56c0dc9cf935e42b31703bb18b/lib/helpers/query/castUpdate.js#L129C3-L129C37).

A workaround exists with setting the $setOnInsert for ALL the default fields to the defaults your self, to avoid them being set in the filter.

Steps to Reproduce

const { Schema } = mongoose;

(async function(){
    console.log('connecting');
    var client = await mongoose.connect(`mongodb://localhost:10003/dev?tls=true&directConnection=true&retryWrites=false`,
    {
        user: process.env.DBUSER,
        pass: process.env.DBPASS,
    });
    console.log('connected');

    const schema = new Schema({
        my_field: Number,
        default_field: {type:String, default: 'default'}
    }, {
        collection: `test`,
        //disable versioning alltogether
        versionKey: false
    });
    
    schema.index({my_field:1});

    const Model = mongoose.model(`Test`, schema);
    //Make sure it is clean
    await Model.deleteMany({});

    //create a doc
    await Model.create({my_field:1, default_field:'NOT DEFAULT'});

    //get the doc based on a filter
    const filter = {
        my_field:1
    };
    let docs = await Model.find(filter);
    console.log(`Doc with a non-default`, docs);

    await Model.findOneAndUpdate(filter,{},{
        upsert:true
    });

    docs = await Model.find(filter);
    console.log(`Docs with a duplicate`,docs);

    console.log('done');
    process.exit();
})();

Expected Behavior

That the filter is NOT changed, and thus that the reproduction code would find the existing doc and not create a new one with a default.

@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Oct 10, 2023
@IslandRhythms
Copy link
Collaborator

const mongoose = require('mongoose')
const { Schema } = mongoose;

(async function(){
    console.log('connecting');
    var client = await mongoose.connect(`mongodb://localhost:27017/`,
    {
        user: process.env.DBUSER,
        pass: process.env.DBPASS,
    });
    console.log('connected');

    const schema = new Schema({
        my_field: Number,
        default_field: {type:String, default: 'default'}
    }, {
        collection: `test`,
        //disable versioning alltogether
        versionKey: false
    });
    
    schema.index({my_field:1});

    const Model = mongoose.model(`Test`, schema);
    //Make sure it is clean
    await Model.deleteMany({});

    //create a doc
    await Model.create({my_field:1, default_field:'NOT DEFAULT'});

    //get the doc based on a filter
    const filter = {
        my_field:1
    };
    let docs = await Model.find(filter);
    console.log(`Doc with a non-default`, docs);

    await Model.findOneAndUpdate(filter,{},{
        upsert:true
    });

    docs = await Model.find(filter);
    console.log(`Docs with a duplicate`,docs);

    console.log('done');
    process.exit();
})();

@vkarpov15 vkarpov15 modified the milestones: 7.6.2, 7.6.3 Oct 12, 2023
vkarpov15 added a commit that referenced this issue Oct 16, 2023
vkarpov15 added a commit that referenced this issue Oct 17, 2023
fix(update): avoid applying defaults on query filter when upserting with empty 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
3 participants