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.
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,$positionand$sortsilently ignored.{$each: [3, 4], $slice: -3}appended both values, sliced nothing, and answeredok: 1withmodifiedCount: 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
$currentDatefield is whatever the clock said. The case names it involatile, 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.