PLAN §3 lists eight 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.
It also found the thing this directory exists for: `$push`'s `$slice`,
`$position` and `$sort` are **silently ignored**. `{$each: [3, 4], $slice: -3}`
appends both values, slices nothing, and answers ok: 1 with modifiedCount: 1.
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.
103 cases in six files, 18 pass / 84 fail -- red by construction, like
`tests/spec/aggregate/expressions.json` at 1/26 and the positional corpus at
15/36. Inputs authored in `sources/`, every expectation measured.
Two things here cannot be recorded as values, and both become a `$$type`
assertion rather than being left out: a `$currentDate` field is whatever the
clock said (named per case in `volatile`, so a real stored date can still be
pinned one day), and an upsert that inserts gets a generated ObjectId (that
one automatic -- no source authors an ObjectId). Everything else is compared
exactly. Files are canonical extended JSON, which the runner already parses
that way: `$mul` overflowing an int32 produces an int64, and writing
`4000000000` as a bare number would not have said so.
What recording it settled, none of it guessable:
- `$mul` of a missing field writes **0**, not the operand; of a non-numeric
field, or by one, TypeMismatch (14).
- `$min`/`$max` are not numeric operators. They compare in BSON canonical
order, so `$min: {s: 5}` on `s: "b"` writes 5, and a missing field is
always written.
- two operators writing one field is **ConflictingUpdateOperators (40)** --
`$min`+`$max`, `$set`+`$inc`, `$setOnInsert`+`$set`. A whole error class
this server does not have.
- `$addToSet` compares documents whole, **field order included**:
`{a:1,b:2}` and `{b:2,a:1}` are two values. But `2` and `2.0` are one.
- `$push` modifiers are only modifiers when `$each` is there: `{$slice: 1}`
alone is a value to push. With it, the order is position, then sort the
whole array, then slice.
- `$currentDate` with `false` still writes a date.
- `$setOnInsert` may write `_id` on an insert, where `$set` may not.
- an unknown modifier is FailedToParse (9), not BadValue.
One case was authored and then removed: `{b: 1, $set: {c: 1}}` never reaches a
server -- the driver rejects it -- so there was no answer to record and the
case would have asserted nothing.
104 lines
4.6 KiB
Markdown
104 lines
4.6 KiB
Markdown
# 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
|
|
```
|
|
|
|
```sh
|
|
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, run against the server before any of it was
|
|
implemented:
|
|
|
|
```
|
|
array-ops.json 2 pass 24 fail 0 skip
|
|
current-date.json 3 pass 8 fail 0 skip
|
|
modifiers.json 7 pass 7 fail 0 skip
|
|
numeric.json 0 pass 21 fail 0 skip
|
|
push-modifiers.json 6 pass 15 fail 0 skip
|
|
set-on-insert.json 0 pass 9 fail 0 skip
|
|
```
|
|
|
|
Red by construction. The 18 that pass are the shapes this server already
|
|
answers the way mongod does — mostly refusals that happen to agree, plus the
|
|
`$push` cases with no modifier on them.
|
|
|
|
## 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.
|