tests/spec: record Tier 2's spec -- the stages that rewrite a document

24 cases for `$addFields`/`$set`, `$unset`, `$replaceRoot`, `$unwind` and
`$project`'s computed fields, recorded from mongod 8.3.7 before any of them is
written. The file starts at 0 pass / 24 fail, which is the honest number for
five stages this server does not have.

Eight things the recording settled, none of them guessable from the manual:

    $addFields whose expression is missing   the field is not added at all
    $addFields: {"n.z": 1}                   sets the nested path, keeps siblings
    $replaceRoot of missing or non-document  error 40228
    $unwind of an empty array or missing     the document is dropped
    $unwind of a non-array                   the document is kept whole
    $unwind path without a $                 error 28818
    includeArrayIndex                        0-based
    $project: {n: {x: 1}}                    narrows it; a document without
                                             `n` keeps only `_id`

That last one is the `query.project` gap recorded during M2 as broken rather
than unimplemented -- a nested inclusion currently reads as falsy and returns
the whole document minus the field. It now has a measured expectation to be
fixed against, in the corpus, rather than a note in a commit message.

Worth stating before the implementation: the design review expected Tier 2 to
need a per-stage iterator, on the grounds that `$unwind` is 1->N and "there is
no way to express that in a window over the input". That was true of the window
as it stood, and stopped being true when `$project` was made to rebuild the
stream -- a stage that rebuilds can emit any number of documents it likes. So
Tier 2 looks like five stages rather than a rewrite. The corpus is what will
say whether that holds.
This commit is contained in:
A.Shakhmatov
2026-08-09 22:30:36 +03:00
parent 37cfa863ee
commit 88ac010c3c
3 changed files with 1379 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
{
"_comment": [
"Inputs only. Expectations are measured -- see record.js.",
"The stages that rewrite a document rather than select or reorder one:",
"$addFields/$set, $unset, $replaceRoot, $unwind, and $project's computed",
"fields. A $sort at the end wherever the order is not already fixed."
],
"documents": [
{ "_id": 1, "a": 1, "n": { "x": 10, "y": 20 }, "t": [1, 2, 3] },
{ "_id": 2, "a": 2, "n": { "x": 30 }, "t": [] },
{ "_id": 3, "a": 3, "t": "not an array" },
{ "_id": 4, "a": 4, "n": { "x": 40, "y": 50 } }
],
"cases": [
{
"description": "$addFields adds a computed field",
"pipeline": [{ "$addFields": { "b": { "$multiply": ["$a", 10] } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$addFields overwrites an existing field in place",
"pipeline": [{ "$addFields": { "a": { "$add": ["$a", 100] } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$addFields with a field whose expression is missing",
"pipeline": [{ "$addFields": { "b": "$nope" } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$addFields on a dotted path",
"pipeline": [{ "$addFields": { "n.z": 1 } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$set is $addFields",
"pipeline": [{ "$set": { "b": { "$add": ["$a", 1] } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$unset a top-level field",
"pipeline": [{ "$unset": "a" }, { "$sort": { "_id": 1 } }]
},
{
"description": "$unset several fields",
"pipeline": [{ "$unset": ["a", "t"] }, { "$sort": { "_id": 1 } }]
},
{
"description": "$unset a nested path",
"pipeline": [{ "$unset": "n.y" }, { "$sort": { "_id": 1 } }]
},
{
"description": "$replaceRoot with a subdocument",
"pipeline": [{ "$match": { "n": { "$exists": true } } }, { "$replaceRoot": { "newRoot": "$n" } }, { "$sort": { "x": 1 } }]
},
{
"description": "$replaceRoot with a built document",
"pipeline": [{ "$replaceRoot": { "newRoot": { "id": "$_id", "double": { "$multiply": ["$a", 2] } } } }, { "$sort": { "id": 1 } }]
},
{
"description": "$replaceRoot with a missing path is an error",
"pipeline": [{ "$replaceRoot": { "newRoot": "$n" } }]
},
{
"description": "$replaceRoot with a non-document is an error",
"pipeline": [{ "$replaceRoot": { "newRoot": "$a" } }]
},
{
"description": "$unwind an array field",
"pipeline": [{ "$match": { "_id": 1 } }, { "$unwind": "$t" }]
},
{
"description": "$unwind drops empty arrays and missing fields",
"pipeline": [{ "$unwind": "$t" }, { "$sort": { "_id": 1, "t": 1 } }]
},
{
"description": "$unwind a field that is not an array keeps it whole",
"pipeline": [{ "$match": { "_id": 3 } }, { "$unwind": "$t" }]
},
{
"description": "$unwind in its document form",
"pipeline": [{ "$unwind": { "path": "$t" } }, { "$sort": { "_id": 1, "t": 1 } }]
},
{
"description": "$unwind preserving null and empty arrays",
"pipeline": [{ "$unwind": { "path": "$t", "preserveNullAndEmptyArrays": true } }, { "$sort": { "_id": 1, "t": 1 } }]
},
{
"description": "$unwind with an index field",
"pipeline": [{ "$match": { "_id": 1 } }, { "$unwind": { "path": "$t", "includeArrayIndex": "i" } }]
},
{
"description": "$unwind of a path without a $ is an error",
"pipeline": [{ "$unwind": "t" }]
},
{
"description": "$project computes a field",
"pipeline": [{ "$project": { "b": { "$add": ["$a", 1] } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$project renames with a path",
"pipeline": [{ "$project": { "value": "$a" } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$project a nested inclusion",
"pipeline": [{ "$project": { "n": { "x": 1 } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$addFields then $match sees the new field",
"pipeline": [{ "$addFields": { "b": { "$multiply": ["$a", 10] } } }, { "$match": { "b": { "$gt": 20 } } }, { "$sort": { "_id": 1 } }]
},
{
"description": "$unwind then $group counts the elements",
"pipeline": [{ "$unwind": "$t" }, { "$group": { "_id": null, "n": { "$sum": 1 } } }]
}
]
}