[SERVER-34973] Upsert querying an array, then pushing to this array with simple values Created: 14/May/18 Updated: 27/Oct/23 Resolved: 21/May/18 |
|
| Status: | Closed |
| Project: | Core Server |
| Component/s: | Write Ops |
| Affects Version/s: | 3.6.4 |
| Fix Version/s: | None |
| Type: | Bug | Priority: | Major - P3 |
| Reporter: | Arthur Darcet | Assignee: | Asya Kamsky |
| Resolution: | Works as Designed | Votes: | 0 |
| Labels: | None | ||
| Remaining Estimate: | Not Specified | ||
| Time Spent: | Not Specified | ||
| Original Estimate: | Not Specified | ||
| Issue Links: |
|
||||||||||||||||
| Operating System: | ALL | ||||||||||||||||
| Steps To Reproduce: | db.empty.update({"list": "value"}, {"$push": {list: "value", {upsert: 1}); db.empty.update({"list": {"$elemMatch": "value", {"$push": {list: "value"}}, {upsert: 1});}} db.empty.update({"list": {"$in": ["value"], {"$push": {list: "value"}}, {upsert: 1});}} db.empty.update({"list": {$in: ["value", "___wrong"], {"$push": {list: "value"}}, {upsert: 1});}} |
||||||||||||||||
| Participants: | |||||||||||||||||
| Description |
|
As stated in the docs:
So of course, this fails: db.empty.update({"list": "value"}, {"$push": {list: "value", {upsert: 1}); with The field 'list' must be an array but is of type string in document {no id} The workaround, suggested in https://jira.mongodb.org/browse/SERVER-20203 is to add an $elemMatch to the query, so that the upserted document is not "pre-filled" with a scalar value for the list key. But this does not work here: $elemMatch refuses to match single values, so: db.empty.update({"list": {"$elemMatch": "value", {"$push": {list: "value"}}, {upsert: 1});}} yields $elemMatch needs an Object
If I remember correctly, a working workaround used to be: db.empty.update({"list": {"$in": ["value"], {"$push": {list: "value"}}, {upsert: 1});}} in 3.2 or 3.4 ; but $in with single value arrays are considered equality matches in 3.6 (i'm guessing here), and this last option fails with The field 'list' must be an array but is of type string in document {no id} again. The only way I found to do this is db.empty.update({"list": {$in: ["value", "___wrong"], {"$push": {list: "value"}}, {upsert: 1});}} but this seems very inefficient and I would like to have a cleaner option |
| Comments |
| Comment by Arthur Darcet [ 22/May/18 ] | |||||||||||||||
|
Yes thank you, the $eq solution works | |||||||||||||||
| Comment by Asya Kamsky [ 21/May/18 ] | |||||||||||||||
|
I'm closing this issue as I believe your use case will work with syntax I showed. If that's not the case please reopen (and then likely it will be a duplicate of | |||||||||||||||
| Comment by Asya Kamsky [ 21/May/18 ] | |||||||||||||||
|
I think maybe you misunderstood the error message here:
That's saying that you are not giving the right syntax to $elemMatch, which expects the query to be an object. You can use this syntax to get $elemMatch to work:
| |||||||||||||||
| Comment by Arthur Darcet [ 21/May/18 ] | |||||||||||||||
|
Hello @asya, Thank you for your response. Please let me clarify my request a bit, I probably did not include enough context: My use-case is quite simple: I have objects identified by several strings, and I would like to set an attribute on a given identifier - creating it first if it does not yet exist. The model is just:
On all the documents in the collection, `identifiers` is an array. And I need to ensure there is no duplicates, ie any identifier is only present in one document at most. Trying to set atomically `attribute: 100` on an identifier that is not yet in the DB means doing an upsert, but this upsert does not work because if I match {identifiers: "id3"}, then the upsert defaults the value of `identifiers` to a string and not an array, which would not work.
The workaround given in But of course $elemMatch does not work with scalar values, so this workaround is not available here.
Another workaround that did work until 3.6 is too wrap the query in an $in, using {identifiers: {$in: ["id3"]}} ; but since the 3.6, this is single-value $in is optimised away, and then the upsert fill the new document with a string value for identifiers
Which leaves us only one option: adding a second, completely wrong value in the query array for $in. This is inefficient, and ugly…
I think my use-case would indeed be covered by
but my use-case seems simple enough, and i think it shouldn't need to use a feature so complex… (+ this won't be available in the stable release for some time…) | |||||||||||||||
| Comment by Asya Kamsky [ 18/May/18 ] | |||||||||||||||
|
This is working as expected. It is never going to be correct to try to $push anything to a field which is not of type array. > But this does not work here: $elemMatch refuses to match single values, so: It cannot work, because when list is a single value (i.e. not an array) you cannot $push to it. Your other workaround with additional "never present" value in $in array works for an upsert, but it will give the same error if "value" already exists as a single value (the case you are worried about missing):
I suspect that you want to treat single value list as an array with a single element, but currently there is no support for such behavior (converting the type of a field during an update, or treating a string value as if it were an array with a single value that you can $push to). If I'm correct and your use case is for update to perform a different transformation when matched value is an array vs. when it's another type then that would be similar to conditional updates, tracked in
|