Files
MultiforaDB/tests/spec/aggregate/README.md
A.Shakhmatov 76afa75efe commands: the $group accumulators
Nine of them -- `$sum`, `$avg`, `$min`, `$max`, `$first`, `$last`, `$push`,
`$addToSet`, `$count` -- and the corpus goes 9 pass / 10 fail to 18 pass /
1 fail. The one left is the compound `_id`, which needs the expression
evaluator.

Landed before that evaluator, against the tier order the design review set
out, and the corpus is why: every one of these takes a single value per
document, a path or a constant, so nine of its ten failures turned out to be
reachable without one. `classify_expr` already produced exactly that value.

What the recording caught, which is the argument for measuring expectations
rather than writing them:

  - `$avg` over a group with no numeric value is **null**, not `0`. A divisor
    that counted documents rather than numbers would pass every test anybody
    would think to write by hand, and be wrong on the one group that matters.
  - `$min`/`$max` compare across types in canonical BSON order, so the maximum
    of `30`, `7` and `"not a number"` is the string.
  - `$push` skips an absent field but would push an explicit null, so "resolved
    to nothing" and "resolved to null" cannot be the same value internally --
    which is why the accumulators take `?bson.Value` and not `.null`.
  - `$first`/`$last` follow input order, including when the value is absent:
    `$last` of a missing field is null, not the last present one.

`AccState` is one struct rather than a union: the fields are small and every
site already switches on the kind, so a union would add a tag test where a
switch was going to be anyway. Its arrays are the gpa's, the values inside them
the reply arena's -- they outlive the group and travel with the documents.

`numeric_value` is the int32-or-double narrowing MongoDB reports, shared now
between the accumulators and `cmd_aggregate`'s count fast path. It was written
twice before; a divergence between them would make `countDocuments` disagree
with the pipeline it is a shortcut for.

`$avg` and `$push` came out of the Tier 0 refusal test, replaced by
`$stdDevPop` and `$mergeObjects`. The refusal is a property of what is missing
rather than of a list, and the test should read that way.

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

2.9 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   18 pass    1 fail   0 skip

The one that remains is the compound _id, which needs the expression evaluator and is the next tier. The corpus found its first real disagreement on the way there: $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.