tests/spec: a recorded corpus for the update operators

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.
This commit is contained in:
A.Shakhmatov
2026-08-10 21:02:51 +03:00
parent 1482df891b
commit eaf8d7c677
15 changed files with 999 additions and 3 deletions

View File

@@ -0,0 +1,122 @@
{
"_comment": [
"Inputs only. Expectations are measured -- see record.js.",
"`$mul`, `$min` and `$max`. The two comparison operators are not numeric",
"at all -- they compare in BSON canonical order, which is why a string",
"field and a number operand have a defined answer -- and the cases here",
"are shaped to say which of the three rules each one follows."
],
"documents": [{ "_id": 1, "a": 5, "s": "b", "d": 2.5 }],
"cases": [
{
"description": "$mul multiplies an int by an int",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "a": 2 } } }
},
{
"description": "$mul of a missing field",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "gone": 5 } } }
},
{
"description": "$mul mixes int and double",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "d": 2 } } }
},
{
"description": "$mul by zero",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "a": 0 } } }
},
{
"description": "$mul overflowing an int32",
"documents": [{ "_id": 1, "a": 2000000000 }],
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "a": 2 } } }
},
{
"description": "$mul of a non-numeric field",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "s": 2 } } }
},
{
"description": "$mul by a non-numeric operand",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "a": "x" } } }
},
{
"description": "$mul down a dotted path",
"documents": [{ "_id": 1, "n": { "a": 3 } }],
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "n.a": 4 } } }
},
{
"description": "$min writes when the operand is lower",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "a": 3 } } }
},
{
"description": "$min leaves the field when the operand is higher",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "a": 7 } } }
},
{
"description": "$min of a missing field always writes",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "gone": 7 } } }
},
{
"description": "$min against a field of another type",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "s": 5 } } }
},
{
"description": "$min compares an int and an equal double",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "a": 5.0 } } }
},
{
"description": "$max writes when the operand is higher",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$max": { "a": 7 } } }
},
{
"description": "$max leaves the field when the operand is lower",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$max": { "a": 3 } } }
},
{
"description": "$max of a missing field always writes",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$max": { "gone": 7 } } }
},
{
"description": "$max against a field of another type",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$max": { "s": 5 } } }
},
{
"description": "$max against a null field",
"documents": [{ "_id": 1, "a": null }],
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$max": { "a": 1 } } }
},
{
"description": "$min and $max on the same field in one update",
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "a": 3 }, "$max": { "a": 9 } } }
},
{
"description": "$min through an array index",
"documents": [{ "_id": 1, "t": [5, 5] }],
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$min": { "t.0": 1 } } }
},
{
"description": "$mul through a positional segment",
"documents": [{ "_id": 1, "t": [{ "a": 2 }, { "a": 3 }] }],
"operation": "updateOne",
"arguments": { "filter": {}, "update": { "$mul": { "t.$[].a": 10 } } }
}
]
}