Files
MultiforaDB/tests/spec/aggregate
A.Shakhmatov 88ac010c3c 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.
2026-08-09 22:30:36 +03:00
..

The aggregation corpus

mongodb/specifications has no aggregation suite. The thirteen aggregate-*.json files this project runs come from crud and test the aggregate command — cursors, read concern, the write stages, collation, let. They touch stages barely and expressions not at all: $lookup, $unwind, $facet, $addFields and $replaceRoot appear nowhere in the pinned corpus. That is PLAN amendment A6, and this directory is its consequence: M2.5 has to bring its own gate.

The one rule

Inputs are authored here; expectations are measured against a real mongod.

A corpus we write is a corpus that can encode our own bugs as expectations, and it would then agree with us forever. So sources/*.json holds documents and pipelines and nothing else, and record.js asks mongod 8.3.7 what each pipeline answers. It is the same discipline that corrected three assumptions in M1's session work and every error code in M2 — the alternative, in both cases, would have shipped.

Running it

node tests/spec/run.js --suite-dir tests/spec/aggregate

The same runner as the crud corpus, pointed elsewhere. Sharing it is the point: the entity model, the matchers, the skip accounting and expectEvents come for free, and a second runner would drift from the first exactly where it mattered.

--scorecard is refused with --suite-dir: tests/spec/scorecard.txt is the crud corpus's record and the milestones are compared against it.

Re-recording

mongod --port 27099 --dbpath /tmp/mongo-corpus &
node tests/spec/aggregate/record.js --mongod-port 27099

Writes <name>.json for every sources/<name>.json. The generated files are committed: they are the corpus, and regenerating them is how a disagreement with mongod gets re-measured rather than argued about.

Two things to know when adding cases:

  • End a $group pipeline with a $sort. Group output order is unspecified, and a case that depended on it would fail for the wrong reason on either server.
  • Errors record the code, not the message. Message text is mongod's to change between releases; a corpus that pinned it would break for the wrong reason.

Leave out any case whose answer depends on a server newer than the 4.4 this server reports — recording it from mongod 8.x and judging it against a 4.4 answer measures the version gap, not the engine.

Where it stands

Recorded against mongod 8.3.7. At the M2 tip it read 9 pass / 10 fail; with the accumulators in:

group-accumulators.json   19 pass    0 fail   0 skip
expressions.json          27 pass    0 fail   0 skip
document-stages.json       0 pass   24 fail   0 skip

group-accumulators found its first real disagreement on the way to 18: $avg over a group with no numeric value is null, not 0, and a divisor that counted documents rather than numbers would have passed every test anybody would think to write by hand.

expressions.json was recorded before the evaluator was written and read 1 pass / 26 fail against it; it is green now. document-stages.json is the next tier's spec, recorded and not implemented -- $addFields/$set, $unset, $replaceRoot, $unwind and $project's computed fields, none of which this server has, so it starts at zero.

What recording that settled:

mongod
$addFields whose expression is missing the field is not added at all
$addFields: {"n.z": 1} sets the nested path, keeps its siblings
$replaceRoot of a missing path or a non-document error 40228
$unwind of an empty array or a missing field 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 the subdocument, and a document without n keeps only _id

That last row is the query.project gap recorded during M2 -- 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. Expressions are exercised through $group, because _id and the accumulator arguments are the only expression positions that exist until $addFields and $project's computed fields land.

What recording it settled, none of which is guessable:

mongod
$add over a missing field or null null, not an error and not 0
$add over a string error 7157723
$divide by zero error 4848401
$mod of -5 by 4 -1 — the dividend's sign
$lt of a number and a string true — canonical type order
$and over -5 truthy
$not of a missing field true
$switch with no branch and no default error 40069
two operators in one expression document error 15983, not $group's 40238
$subtract with one operand error 16020