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.
This commit is contained in:
A.Shakhmatov
2026-08-10 21:48:55 +03:00
parent 44788ae681
commit e344a073a1
3 changed files with 178 additions and 6 deletions

View File

@@ -1,8 +1,9 @@
# The update-operator corpus # The update-operator corpus
PLAN §3 lists eight update operators for M3 — `$setOnInsert`, `$addToSet`, What an update *document* may be, measured. PLAN §3 lists eight operators for
`$mul`, `$min`, `$max`, `$pop`, `$pullAll`, `$currentDate` — and the pinned M3 — `$setOnInsert`, `$addToSet`, `$mul`, `$min`, `$max`, `$pop`, `$pullAll`,
crud corpus says almost nothing about any of them. A probe running the `$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: identical update against mongod 8.3.7 and this server found:
- all eight answering `bad update`, code 2, one message for every question; - all eight answering `bad update`, code 2, one message for every question;
@@ -62,6 +63,7 @@ Recorded against mongod 8.3.7. Green:
``` ```
array-ops.json 26 pass 0 fail 0 skip 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 current-date.json 11 pass 0 fail 0 skip
modifiers.json 14 pass 0 fail 0 skip modifiers.json 14 pass 0 fail 0 skip
numeric.json 21 pass 0 fail 0 skip numeric.json 21 pass 0 fail 0 skip
@@ -103,7 +105,13 @@ None of this is guessable, and several rows contradict the obvious reading:
| `$currentDate` with anything but a bool or `{$type: date\|timestamp}` | BadValue (2) | | `$currentDate` with anything but a bool or `{$type: date\|timestamp}` | BadValue (2) |
| `$setOnInsert` writing `_id` on an insert | **allowed**, unlike `$set` | | `$setOnInsert` writing `_id` on an insert | **allowed**, unlike `$set` |
| an unknown modifier | FailedToParse (9), not BadValue | | 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) |
One case was authored and then removed: `{b: 1, $set: {c: 1}}` is rejected by Three cases were authored and then removed, all for the same reason: the
the driver before it reaches a server, so there is no server answer to record driver rejects `{b: 1, $set: {c: 1}}`, an empty pipeline and a pipeline
and the case would have asserted nothing. 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.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,163 @@
{
"_comment": [
"Inputs only. Expectations are measured -- see record.js.",
"Pipeline-style updates: an update document that is an *array* of",
"aggregation stages rather than a document of operators. The pinned crud",
"corpus has five cases, one per command, and none of them is a refusal --",
"so which stages are allowed, what happens to `_id`, and what an upsert",
"does are all unmeasured there.",
"Two shapes were authored and removed: an empty pipeline and an array",
"holding a non-document. The driver rejects both before they reach a",
"server ('Update document requires atomic operators'), so there is no",
"server answer to record."
],
"documents": [
{ "_id": 1, "x": 1, "y": 1, "t": { "u": { "v": 1 } } },
{ "_id": 2, "x": 2, "y": 1 }
],
"cases": [
{
"description": "$replaceRoot keeps the _id it was given",
"operation": "updateOne",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$replaceRoot": { "newRoot": "$t" } }]
}
},
{
"description": "$replaceWith is the same stage under its short name",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$replaceWith": "$t" }] }
},
{
"description": "$project keeps the _id it did not mention",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$project": { "x": 1 } }] }
},
{
"description": "$project excluding _id keeps it anyway",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$project": { "_id": 0, "x": 1 } }] }
},
{
"description": "$addFields adds to the document",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$addFields": { "foo": 1 } }] }
},
{
"description": "$set is $addFields under its other name",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$set": { "foo": "$x" } }] }
},
{
"description": "$unset of one field",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$unset": "y" }] }
},
{
"description": "$unset of several fields",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$unset": ["y", "t"] }] }
},
{
"description": "the stages run in order, each on the last one's output",
"operation": "updateOne",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$replaceRoot": { "newRoot": "$t" } }, { "$addFields": { "foo": 1 } }]
}
},
{
"description": "an expression reads the document the stage was handed",
"operation": "updateOne",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$addFields": { "z": { "$add": ["$x", "$y"] } } }]
}
},
{
"description": "a stage may restate the _id it already has",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$addFields": { "_id": 1 } }] }
},
{
"description": "a stage may not change the _id",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$addFields": { "_id": 9 } }] }
},
{
"description": "updateMany runs the pipeline on every match",
"operation": "updateMany",
"arguments": {
"filter": {},
"update": [{ "$project": { "x": 1 } }, { "$addFields": { "foo": 1 } }]
}
},
{
"description": "findOneAndUpdate with a pipeline",
"operation": "findOneAndUpdate",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$project": { "x": 1 } }, { "$addFields": { "foo": 1 } }],
"returnDocument": "after"
}
},
{
"description": "an upsert runs the pipeline on the document the filter implies",
"documents": [],
"operation": "updateOne",
"arguments": {
"filter": { "k": 1 },
"update": [{ "$addFields": { "a": 1 } }],
"upsert": true
}
},
{
"description": "a pipeline that writes nothing new still reports a match",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$addFields": { "x": 1 } }] }
},
{
"description": "$match is not allowed inside an update",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$match": { "x": 1 } }] }
},
{
"description": "$group is not allowed inside an update",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$group": { "_id": null } }] }
},
{
"description": "$sort is not allowed inside an update",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$sort": { "x": 1 } }] }
},
{
"description": "$unwind is not allowed inside an update",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$unwind": "$t" }] }
},
{
"description": "a stage name that is not a stage at all",
"operation": "updateOne",
"arguments": { "filter": { "_id": 1 }, "update": [{ "$bogus": {} }] }
},
{
"description": "two stages packed into one array element",
"operation": "updateOne",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$addFields": { "a": 1 }, "$unset": "y" }]
}
},
{
"description": "arrayFilters alongside a pipeline",
"operation": "updateOne",
"arguments": {
"filter": { "_id": 1 },
"update": [{ "$addFields": { "a": 1 } }],
"arrayFilters": [{ "i.b": 1 }]
}
}
]
}