Files
MultiforaDB/tests/spec/operators/README.md
A.Shakhmatov 44788ae681 plan/spec: the operator corpus goes green
102/102. PLAN's M3 row now names both corpora as the gate, because neither the
pinned crud suite nor e2e3/e2e4 can see this work: the eight operators are
barely in the pinned corpus and `$push`'s modifiers are not in it at all.

§6 records what the corpus found that was nobody's operator -- an upsert never
reporting the `_id` it generated -- and the three things left open on purpose:
`$bit`, a multi-field `$sort` key inside `$push`, and the 64-path bound on the
conflict check.

Full matrix at this commit: 247/247 unit tests in ReleaseFast and ReleaseSafe,
83/83 fuzz, operators 102/0, positional 51/0, aggregation 70/0, pinned crud
218/73/196 unmoved, e2e and crash-fuzz green.
2026-08-10 21:33:45 +03:00

5.0 KiB

The update-operator corpus

PLAN §3 lists eight update operators for M3 — $setOnInsert, $addToSet, $mul, $min, $max, $pop, $pullAll, $currentDate — and the pinned crud corpus says almost nothing about any of them. 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
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 against a server with none of these operators — and driven green by seven 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

One case was authored and then removed: {b: 1, $set: {c: 1}} is rejected by the driver before it reaches a server, so there is no server answer to record and the case would have asserted nothing.