Files
MultiforaDB/tests/spec/aggregate/README.md
A.Shakhmatov fc611a2c64 commands: $project computes, renames and narrows
The last three cases of the corpus, which is now 70 pass / 0 fail -- every
answer byte-identical to mongod 8.3.7 across the accumulators, the expressions
and the document stages.

The fix turned out to need nothing from `query.project`, which `find` shares
and which I had expected to have to rewrite. A nested spec *is* a dotted path:
`{n: {x: 1}}` and `{"n.x": 1}` are the same projection, and dotted paths are
something the existing projection already narrows correctly. So `$project` is
flattened into inclusion/exclusion flags plus a list of computed fields, and
both halves reuse machinery that was already there -- `query.project` for the
flags, `set_path` from the document stages for the computed fields. A bare path
(`{value: "$a"}`) is a rename, which is a computed field like any other.

Both shapes used to read as *falsy*, which flipped the whole projection into
its exclusion branch and returned the entire document minus the field. That was
recorded during M2 as broken rather than unimplemented; this is the fix it was
waiting for.

One case `query.project` genuinely cannot express, so it is built directly: a
projection that only computes keeps `_id` and nothing else, and with no non-`_id`
flag that function reads the spec as an exclusion and returns everything. It
cost two failures and a `id_only` flag to find, which is what a recorded corpus
is for -- the answer is obvious once seen and not before.

`$project` now goes through the same `Rewrite` path as `$addFields`, `$unset`,
`$replaceRoot` and `$unwind`, so its own branch is gone. Its refusal shrank to
the one shape mongod also refuses, mixing inclusion with exclusion, judged on
the flattened flags so a nested spec is treated like a dotted one.

191/191 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz, e2e 49, e2e2
concurrent, e2e3 16, e2e4 17, e2e6 72, e2e7 86, crud corpus unchanged at
201/90/196.
2026-08-09 22:50:31 +03:00

4.7 KiB

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      24 pass    0 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 was recorded at 0 pass / 24 fail and is green. The whole corpus is: 70 cases, every answer byte-identical to mongod 8.3.7.

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