[SERVER-3946] Update with $addToSet/$push with a query on same field is broken during insert Created: 26/Sep/11  Updated: 06/Dec/22  Resolved: 21/Apr/17

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

Type: Improvement Priority: Major - P3
Reporter: Eliot Horowitz (Inactive) Assignee: Backlog - Query Team (Inactive)
Resolution: Done Votes: 5
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Depends
Duplicate
is duplicated by SERVER-14391 Upsert with $all query and $addToSet ... Closed
is duplicated by SERVER-15245 findAndModify fail to addToSet a fiel... Closed
Related
is related to SERVER-13843 Upsert fails with error when query do... Backlog
is related to SERVER-27707 findAndModify breaks when I use the q... Closed
Assigned Teams:
Query
Participants:

 Description   

> db.foo.update( { x : "a" },  { $addToSet : { x : "b" } } , true )

Cannot apply $addToSet modifier to non-array



 Comments   
Comment by David Storch [ 21/Apr/17 ]

Hi all,

After some discussion on related ticket SERVER-27707, we have decided to resolve this ticket as Works as Designed. When an {upsert: true} update results in an insert, the semantics are that all top-level equality predicates should seed the document to insert. The update modifiers then get applied to this seeded document in order to determine the final resulting document. For the example

db.foo.update({x : "a"},  {$addToSet: {x: "b"}} , {upsert: true});

this means that we will first create the document {x: "a"}, since there was a single equality predicate on x. Then, we will attempt to apply {$addToSet: {x: "b"}} to this document. This is an error, since $addToSet can only be done against array fields.

In order to avoid this problem, you should wrap your equality predicate in an $elemMatch:

> db.foo.drop();
false
> db.foo.update({x: {$elemMatch: {$eq: "a"}}}, {$addToSet: {x: "b"}}, {upsert: true});
WriteResult({
	"nMatched" : 0,
	"nUpserted" : 1,
	"nModified" : 0,
	"_id" : ObjectId("58fa53f0413c00d1fd440de2")
})

Since queries like this always expect the field x to be an array, I imagine that the $elemMatch version of the problematic query is sufficient for nearly all use cases. You may get into trouble if your schema has a field which can be either an array or a scalar, but such mixed array/scalar fields are not advisable. In a mixed array/scalar schema, an $addToSet query such as the one reported here will already fail if the upsert operation happens to match a document containing a scalar.

Please let me know if you have any questions or concerns.

Best,
Dave

Comment by David Storch [ 24/Feb/17 ]

Hi zoellner@qollaboration.com, yes, single-element $in behavior has changed for upsert in 3.4. This is being tracked by SERVER-27707. We have a proposed fix in code review but are discussing internally how to move forward. Although this behavior has changed from previous versions, it is not obvious that the predicates {x: 99} and {x: {$in: [99]} should mean different things. It may make more sense for these two predicates to be different spellings of the same logical operation. Please watch SERVER-27707 for updates. In the meantime, your $elemMatch workaround seems like the right thing to do—$elemMatch does not suffer from the same problem as $in, since {x: {$elemMatch: {$eq: 99}}} is not identical in meaning to {x: 99}.

Comment by Andreas Zoellner [ 24/Feb/17 ]

The above "working" version doesn't work for us. Might be due to updates in 3.2/3.4.

The best workaround for us right now seems to be to use $elemMatch in the query:

This works on 3.4.1:

db.foo.update({ x : {$elemMatch:{$eq: "a"}} }, { $addToSet : { x : "b" }} , true )

Comment by Constantin Guay [ 26/Dec/14 ]

Tested in version 2.6.6:

db.foo.update({ x : {$all:["a"]} }, { $addToSet : { x : "b" }} , true )

does not work either.

I had to use:

db.foo.update({ x : {$in:["a"]} }, { $addToSet : { x : "b" }} , true )

To make it works.

Comment by Suriel Bendahan [ 25/Nov/13 ]

As a workaround you can use the $all modifier in the query supplying it with a single valued array matching your lookup:

db.foo.update({ x : {$all:["a"]} }, { $addToSet : { x : "b" }} , true )

Comment by Simon Schenk [ 27/Sep/12 ]

Are there any plans, when this issue will be fixed? Its been a real pain for me, as working around it requires a query and then a separate update/insert and that combination affects performance really badly.

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