[SERVER-42084] Easy syntax for $setIfModified behavior Created: 04/Jul/19  Updated: 06/Dec/22

Status: Backlog
Project: Core Server
Component/s: Write Ops
Affects Version/s: 4.2.0
Fix Version/s: None

Type: Improvement Priority: Major - P3
Reporter: Ben Rotz Assignee: Backlog - Query Optimization
Resolution: Unresolved Votes: 12
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Duplicate
is duplicated by SERVER-51208 New field `$setOnUpdate` will solve s... Closed
Assigned Teams:
Query Optimization
Participants:
Case:

 Description   

see SERVER-13578. This behavior is "possible" using update pipeline syntax released in 4.2, but it becomes extremely verbose for this common use case.

Example 

db.runCommand(
 {
 update: "test",
 updates: [
 {
 q: { a: 1 },
 u: [
 { $set: { a: 1, b: 2,
created_at: {$cond: [
 {$eq:[ {$type:"$_id"}, "missing" ] },
 ISODate(),
 "$created_at"
 ] },
updated_at: {$cond: [
 {$or: [
 $ne:["$a", 1],
 $ne:["$b", 2],
 ]},
 ISODate(),
 "$updated_at"
 ] }
} },
 ],
 upsert: true
 }
 ],
 writeConcern: { w: "majority", wtimeout: 5000 }
 }
)

 

You can see that as the complexity of the document grows, the more verbose the command becomes to get this behavior. It would be nice to get this behavior with an easy to use operator (e.g. $setIfModified)



 Comments   
Comment by Richard Scarrott [ 12/Apr/22 ]

I just found out the hard way that aggregation $set is nothing like a regular $set update so as an FYI to anybody else considering using the above code, it's worth first knowing the following two updates are VERY different:

// Collection
[
  {
    "_id": "123",
    "options": {
      "size": "Large",
      "color": "Red"
    }
  }
] 

This (https://mongoplayground.net/p/Iw9e1UaTyue):

db.collection.update(
  { _id: "123"},
  {
    $set: {
      _id: "123",
      options: {
        size: "Large"
      }
    }
  }
);
 
// Output
[
  {
    "_id": "123",
    "options": {
      "size": "Large"
    }
  }
] 

Is NOTHING like this (https://mongoplayground.net/p/-KQKL2L6Q0w):

db.collection.update(
  { _id: "123"},
  [
    {
      $set: {
        _id: "123",
        options: {
          size: "Large"
        }
      }
    }
  ]
);
 
// Output
[
  {
    "_id": "123",
    "options": {
      "size": "Large",
      "color": "Red" // <-- 😱?
    }
  }
]

The docs say:

The $set stage is an alias for $addFields.

So this behaviour is weird in the context of an update... but documented.

Comment by Asya Kamsky [ 23/Jul/21 ]

The code given as example in the description isn't quite correct (it sets updatedAt after the fields are updated which means the updatedAt field will never get set. It turns out there is simpler syntax available to check if that field needs to be set.

Assumption: new fields to be set are in document newDoc and in my example it is {{ a:1, b:10 }}.
Existing document has updatedAt field which should only be updated if any fields are set to new value.

Assuming we are updating document with _id equal to 1:

db.coll.update(
     {_id:1}, 
     [
       {$set:{
          updatedAt:{$cond:{
                   if: {$eq: [newDoc, {a:"$a", b:"$b"}]}, 
                   then: "$updatedAt", 
                   else:  "$$NOW"
          }}
       }},
       {$set:newDoc}
     ]
) 

The first time I run this on existing document which needs to be updated I get an update with resultant doc being:

{ "_id" : 1, "updatedAt" : ISODate("2021-07-23T16:54:49.481Z"), "a" : 1, "b" : 10 }

The second time I run it the update is a no-op.

Generated at Thu Feb 08 04:59:33 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.