This method uses $concatArrays and $slice when the value array exists and is the minimum size needed. And when any of those conditions is not met, it uses $zip & $map to fill in missing values. So it has the benefit of not using $map when the requirements are met.
- Btw,
$map isn't a "slow" operation, at least not in this case or the one discussed in the comments.
- Document retrieval for the update is much much slower in comparison.
- I have posted a separate answer which uses
$map every time, like in cmghess's comment, but with concatArrays & slice.
- If nano-performance gains of not using using $map are necessary, then you should use multiple conditions with
$switch-case which individually optimise each of the cases mentioned below (exists, size ok, size not ok, null, does not exist, etc.)
This Update Pipeline handles these cases:
[
{ "_id": "curveId exists and is larger", "curveId": 12, "values": [1, 2, 3, 7, 8, 9] },
{ "_id": "curveId exists and values is exact", "curveId": 12, "values": [1, 2, 3, 4] },
{ "_id": "curveId exists but values is short by 1", "curveId": 12, "values": [1, 2, 3] },
{ "_id": "curveId exists but values is short by 2", "curveId": 12, "values": [1, 2] },
{ "_id": "curveId exists but values is empty", "curveId": 12, "values": [] },
{ "_id": "curveId exists but values is null", "curveId": 12, "values": null },
{ "_id": "curveId exists but values does not exist", "curveId": 12 },
{ "_id": "curveId 99 doesn't exist" }
]
Update pipeline with aggregation expressions:
db.collection.update({ curveId: 12 },
[
{
$set: {
values: {
$let: {
vars: {
// index & new value to set
idx: 3,
new_val: 1000
},
in: {
$cond: {
if: {
$and: [
{ $isArray: "$values" },
{ $lte: [ "$$idx", { $size: "$values" }] } // "lte" is correct here
]
},
then: {
$concatArrays: [
{ $slice: ["$values", "$$idx"] },
["$$new_val"],
{
$slice: [
"$values",
{ $add: ["$$idx", 1] },
{ $add: [{ $size: "$values" }, 1] }
]
}
]
},
else: {
$let: {
vars: {
vals_nulls: {
$map: {
input: {
$zip: {
inputs: [
{ $ifNull: ["$values", []] },
{ $range: [0, "$$idx"] }
],
useLongestLength: true
}
},
in: { $first: "$$this" }
}
}
},
in: {
$concatArrays: [
{ $slice: ["$$vals_nulls", "$$idx"] },
["$$new_val"],
{
$slice: [
"$$vals_nulls",
{ $add: ["$$idx", 1 ] },
{ $add: [{ $size: "$$vals_nulls" }, 1] }
]
}
]
}
}
}
}
}
}
}
}
}
],
{ upsert: true, multi: true }
)
Note:
- The
new_value and index to set only needs to be put in the first $set -> values -> $let part
- All other references to it are done are variables.
- Use your language's
let parameter if available instead of this; mongoose & playground don't have it
multi: true is only needed to demo all the updates scenarios in one execution
- Use
curveId: 99 (anything not 12) in the query to see the upsert behaviour when the document does not exist.
- Notice the repetition which occurs in the
else part with $values & $$vals_nulls.
Mongo Playground
Mongo Playground with index=0