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

112 lines
4.7 KiB
Markdown

# 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
```sh
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
```sh
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 |