Chapter 05 · Updates, Array Mutations, Upserts, findAndModify, and Bulk Writes
Array Updates with $, $[], $[identifier], arrayFilters, $push, $addToSet, and $pull
Mutate AtlasMart arrays precisely with positional operators, filtered array updates, bounded push patterns, duplicate-aware set semantics, and observable before/after evidence.
Learning outcomes
AtlasMart carts contain arrays of line-item subdocuments. The application must increment one SKU, normalize every line, update only low-price active items, add tags without accidental duplicates, append bounded audit history, and remove saved items. These operations look similar in JavaScript but have different binding rules and cardinality.
Distinguish the first-match positional $, all-elements $[], and filtered $[identifier] update operators.
Use arrayFilters to make the element predicate explicit and verify which elements changed.
Contrast $push with $addToSet and explain what “duplicate” means for scalar or document values.
Use $push modifiers to bound an embedded queue rather than allowing an array to grow forever.
Diagnose a positional update that has no valid array-element binding.
Mandatory examples use MongoDB Community Server
8.3.8 in the pinned
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
image, a disposable standalone mongod published
only on 127.0.0.1:27038, and mongosh 2.10.0.
PyMongo examples target the 4.17 line where driver behavior
matters. Authentication and TLS are intentionally disabled
only inside this isolated loopback lab; do not copy that
posture to a shared or remotely reachable server. Default
standalone read/write concern semantics are used, no
replica-set/sharding guarantees are claimed, and the reset
path removes atlasmart-mongo-ch05-l2. Filtered
positional array updates are still one-document writes. Their
atomicity is at the containing document, not a guarantee that
an independently stored product, inventory row, or order was
changed with the cart.
1. Positional syntax answers “which elements?”
The positional $ placeholder targets the first
array element that corresponds to the query condition. The
all-positional $[] targets every element of the
array. The filtered positional form
$[identifier] targets every element satisfying a
separately supplied arrayFilters predicate. Treat
these as three different cardinality contracts rather than
interchangeable syntax.
| Form | Targets | Common mistake |
|---|---|---|
| items.$ | First element bound by the query condition | Using it without a query predicate that identifies an array element. |
| items.$[] | All elements | Applying a transformation intended for one subset to every line. |
| items.$[low] | All elements matching arrayFilters | Assuming the outer document filter also serves as the element filter. |
The filtered identifier must be declared exactly once in
arrayFilters. MongoDB also restricts certain
operators inside array filters, including $expr,
$text, and $where. Upsert with
positional array operators has additional exact-equality
requirements on the array field when the operation would insert
a new document.
2. $push and $addToSet solve different problems
$push appends and therefore permits duplicates.
That is appropriate for an ordered event history where two
equal-looking events can still be distinct occurrences.
$addToSet adds a value only if an equal value is
not already present. It does not retroactively remove duplicates
that already exist, and it does not sort the array. For document
values, equality is structural; if your business concept of
uniqueness is “one item per SKU” rather than exact BSON document
equality, an embedded array is not automatically a uniqueness
constraint.
It only suppresses insertion of an exactly equal value. Two item documents with the same SKU but different quantity or field order/shape can still be distinct BSON values. If SKU uniqueness is an invariant, model it explicitly—for example as one keyed subdocument per SKU, a separate collection with a unique index, or a single mutation that first matches the SKU and updates it.
3. Keep arrays bounded when growth is not the product
$push supports modifiers such as
$each, $sort, and $slice.
Together they can implement a bounded “latest N” embedded
history. This is useful when only recent entries belong in the
aggregate. It is not a substitute for a full audit log if older
entries must remain durable and queryable.
4. Run the positional and array-mutation lab
docker rm -f atlasmart-mongo-ch05-l2 2>/dev/null || truedocker run --name atlasmart-mongo-ch05-l2 -p 127.0.0.1:27038:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27038/atlasmart?directConnection=true" --quiet --eval 'printjson({version:db.version(), hello:db.hello().isWritablePrimary})'
db.cart_mutations.drop();db.cart_mutations.insertOne({ _id:"cart-100", customerId:"cust-7", items:[ {sku:"cam-100",qty:1,priceCents:12500,state:"active"}, {sku:"battery-9",qty:2,priceCents:1900,state:"active"}, {sku:"strap-2",qty:1,priceCents:1200,state:"saved"} ], tags:["camera"], audit:[]});printjson({before:db.cart_mutations.findOne({_id:"cart-100"})});// $ updates the first array element bound by the query predicate.let r=db.cart_mutations.updateOne({_id:"cart-100","items.sku":"cam-100"},{$inc:{"items.$.qty":1}});printjson({step:"$ positional",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $[] updates every element.r=db.cart_mutations.updateOne({_id:"cart-100"},{$set:{"items.$[].checked":true}});printjson({step:"$[] all",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $[identifier] updates every element that satisfies arrayFilters.r=db.cart_mutations.updateOne({_id:"cart-100"},{$inc:{"items.$[low].qty":1}},{arrayFilters:[{"low.priceCents":{$lt:2000},"low.state":"active"}]});printjson({step:"$[low]",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $push can add duplicates; $addToSet prevents adding an equal value that already exists.db.cart_mutations.updateOne({_id:"cart-100"},{$push:{tags:"camera"}});db.cart_mutations.updateOne({_id:"cart-100"},{$addToSet:{tags:"camera"}});db.cart_mutations.updateOne({_id:"cart-100"},{$addToSet:{tags:"priority"}});printjson({step:"push-vs-addToSet",tags:db.cart_mutations.findOne({_id:"cart-100"}).tags});// Bounded queue pattern: append, sort newest first, keep three entries.for(let n=1;n<=4;n++){db.cart_mutations.updateOne({_id:"cart-100"},{$push:{audit:{$each:[{seq:n,event:"touch"}],$sort:{seq:-1},$slice:3}}})}printjson({step:"bounded-$push",audit:db.cart_mutations.findOne({_id:"cart-100"}).audit});r=db.cart_mutations.updateOne({_id:"cart-100"},{$pull:{items:{state:"saved"}}});printjson({step:"$pull",matched:r.matchedCount,modified:r.modifiedCount,after:db.cart_mutations.findOne({_id:"cart-100"})});// Wrong positional target: query does not bind items.sku, so positional $ cannot be used safely.try{db.cart_mutations.updateOne({_id:"cart-100"},{$inc:{"items.$.qty":1}})}catch(e){printjson({expectedError:e.codeName||e.name,message:e.message})}
Expected evidence
The camera quantity changes through $; every item
gains checked:true through $[]; only
active low-price items are incremented through the named filter;
the first $push creates a duplicate
camera tag while $addToSet refuses
another exact duplicate; the audit array remains at three
entries; and $pull removes every item whose
embedded document matches the condition.
Verification checklist
- The fixture has multiple items so element cardinality is observable.
- Every mutation prints matched/modified counts or exact before/after state.
-
The named array filter contains both price and state
conditions in one filter document for identifier
low. - The duplicate-tag experiment demonstrates that $push and $addToSet are not synonyms.
- The bounded audit queue never exceeds three elements.
- The deliberately unbound positional update errors and leaves the document unchanged.
Check your understanding
- What makes items.$ choose an element?
- How does $[] differ from $[identifier]?
- $addToSet already found duplicate tags. Does it remove duplicates that existed before the operation?
- Why can an ever-growing embedded event array become a modeling problem?
- What is the atomicity boundary of these array changes?
Review the answers
The query predicate must bind a matching element in that array; $ is a placeholder for the first matching bound element.
$[] changes every array element; $[identifier] changes only elements satisfying the corresponding arrayFilters condition.
No. It only decides whether to add the requested value; it does not deduplicate the existing array.
The document grows in size and write/read cost, can approach the BSON document limit, and mixes independently retained history into the aggregate.
The containing MongoDB document. Other documents are not part of the same atomic write unless a transaction is deliberately used.
docker rm -f atlasmart-mongo-ch05-l2
Lesson 3 shifts from element selection to document creation:
what exactly MongoDB inserts when an update uses
upsert:true, and why concurrency must be
constrained by an actual uniqueness rule.
Authoritative references
- MongoDB release notes — Current stable server series and patch history.
- MongoDB 8.3 release notes — 8.3.8 is the latest released 8.3 patch at review time; 8.3.9 is upcoming.
- mongosh release notes — mongosh 2.10.0 was released August 13, 2026.
- PyMongo release notes — Current PyMongo 4.17 line and driver changes.
- Array update operators — Official index of $, $[], $[identifier], $push, $addToSet, $pull, and modifiers.
- $ positional update operator — Binding and first-match positional semantics.
- $[] all positional operator — All-element semantics and upsert equality requirements.
- $[identifier] filtered positional operator — arrayFilters behavior, restrictions, nested arrays, and upsert constraints.