Files
MultiforaDB/tests/spec/operators/README.md
A.Shakhmatov 98fde82946 plan/spec: pipeline-style updates are done
M3's row and §6 updated: the operator corpus is 125 cases across seven files,
all green, and the pipeline work also gave `aggregate` the `$replaceWith`
stage it never had -- the compiler is shared between the two.

Full matrix at this commit: 249/249 unit tests in ReleaseFast and ReleaseSafe,
83/83 fuzz, operators 125/0, positional 51/0, aggregation 70/0, pinned crud
228/63/196, e2e and crash-fuzz green.
2026-08-10 22:01:11 +03:00

5.6 KiB

The update-operator corpus

What an update document may be, measured. PLAN §3 lists eight operators for M3 — $setOnInsert, $addToSet, $mul, $min, $max, $pop, $pullAll, $currentDate — and pipeline-style updates beside them; the pinned crud corpus says almost nothing about any of it. A probe running the identical update against mongod 8.3.7 and this server found:

  • all eight answering bad update, code 2, one message for every question;
  • $push's $slice, $position and $sort silently ignored. {$each: [3, 4], $slice: -3} appended both values, sliced nothing, and answered ok: 1 with modifiedCount: 1.

The second is the reason this directory exists rather than a list of TODOs. A missing operator is an error the client can see; a modifier that is parsed, accepted and then dropped is the same class of wrong answer the positional operators were — the client asked for one thing and was told it got it.

The one rule

Inputs are authored here; expectations are measured against a real mongod.

tests/spec/operators/
  sources/*.json      documents + operations, authored
  record.js           runs them against mongod, writes the expectations
  *.json              generated, unified format, do not hand-edit
mongod --port 27099 --dbpath <dir>
node tests/spec/operators/record.js --mongod-port 27099
node tests/spec/run.js --suite-dir tests/spec/operators

Same discipline as tests/spec/positional/ and tests/spec/aggregate/, and for the same reason: a corpus written end to end here can encode our own bugs as expectations and then agree with us forever.

What cannot be recorded as a value

Two things in this corpus are not predictable, and both are replaced by a $$type assertion rather than left out:

  • a $currentDate field is whatever the clock said. The case names it in volatile, per case, so a corpus that one day wants to pin a real stored date still can.
  • an upsert that inserts gets a generated ObjectId. That one is automatic: no source file authors an ObjectId, so the rule is unambiguous.

Everything else about the document — which fields exist, in what order, holding what — is still compared exactly. A case that dropped a field still fails.

Files are written with canonical extended JSON (relaxed: false), which the runner already parses that way. It is verbose and it is exact: $mul overflowing an int32 produces an int64, and a corpus that wrote 4000000000 as a bare number would not have said so.

Where it stands

Recorded against mongod 8.3.7. Green:

array-ops.json         26 pass   0 fail   0 skip
pipeline.json          23 pass   0 fail   0 skip
current-date.json      11 pass   0 fail   0 skip
modifiers.json         14 pass   0 fail   0 skip
numeric.json           21 pass   0 fail   0 skip
push-modifiers.json    21 pass   0 fail   0 skip
set-on-insert.json      9 pass   0 fail   0 skip

It was recorded red — 18 pass / 84 fail for the operators, 0 / 23 for the pipeline file — and driven green by eight commits. The 18 that passed then were the shapes this server already answered mongod's way, mostly refusals that happened to agree.

It also found a bug nothing else here had: an upsert never reported the _id it generated, so updateOne(..., {upsert: true}).upsertedId was null and findOneAndUpdate with returnDocument: after returned a document with no _id. This is the first corpus here that upserts into an empty collection and then looks at what came back.

What recording it settled

None of this is guessable, and several rows contradict the obvious reading:

mongod
$mul of a missing field writes 0, not the operand
$mul of a non-numeric field, or by one TypeMismatch (14)
$min/$max across types compares in BSON canonical order, so $min: {s: 5} on s: "b" writes 5
$min/$max of a missing field always writes
two operators writing one field ConflictingUpdateOperators (40)$min+$max, $set+$inc, $setOnInsert+$set
$addToSet of a document compares whole, field order included: {a:1,b:2} and {b:2,a:1} are two values
$addToSet of 2 and 2.0 one value
$pop of an empty or missing field no-op, not an error
$pop with an argument that is not ±1 FailedToParse (9); on a non-array field, TypeMismatch (14)
$pullAll with a non-array argument BadValue (2)
$push modifiers without $each not modifiers at all{$slice: 1} is pushed as a value
$push modifier order insert at $position, then $sort the whole array, then $slice
$position negative counted from the end
$currentDate with false still writes a date; the boolean's value is ignored
$currentDate with anything but a bool or {$type: date|timestamp} BadValue (2)
$setOnInsert writing _id on an insert allowed, unlike $set
an unknown modifier FailedToParse (9), not BadValue
a pipeline update's _id always survives, through $replaceRoot and through $project: {_id: 0}
a stage changing _id to something else ImmutableField (66)
$match, $group, $sort, $unwind in an update InvalidOptions (72) — real stages, refused here
a name that is no stage at all 40324; two stages in one array element, 40323
arrayFilters beside a pipeline FailedToParse (9)

Three cases were authored and then removed, all for the same reason: the driver rejects {b: 1, $set: {c: 1}}, an empty pipeline and a pipeline holding a non-document before any of them reaches a server, so there is no server answer to record and the cases would have asserted nothing.