Files
MultiforaDB/tests/spec/operators/README.md
A.Shakhmatov e344a073a1 tests/spec: record the pipeline-style updates
An update document may be an *array* of aggregation stages instead of a
document of operators. The pinned crud corpus has five cases, one per command,
and not one of them is a refusal -- so which stages are allowed, what happens
to `_id`, and what an upsert does are all unmeasured there.

23 cases into the existing operator corpus rather than an eighth directory:
this is another shape of update document, and it shares the recorder, the
masking and the README.

Recorded red, 0/23. What it settled:

  - **`_id` always survives**, through `$replaceRoot: {newRoot: "$t"}` and
    through `$project: {_id: 0}` alike. A stage that sets it to a *different*
    value is ImmutableField (66); restating the same one is fine.
  - `$match`, `$group`, `$sort` and `$unwind` are real stages refused
    specifically here: InvalidOptions (72), "$X is not allowed to be used
    within an update". A name that is no stage at all is 40324, and two
    stages packed into one array element is 40323.
  - `arrayFilters` beside a pipeline is FailedToParse (9).
  - an upsert runs the pipeline over the document the filter implies.

Three shapes were authored and removed: `{b: 1, $set: {...}}`, an empty
pipeline, and a pipeline holding a non-document. The driver rejects all three
before they reach a server, so there is nothing to record.
2026-08-10 21:48:55 +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 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
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.