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.
114 lines
4.8 KiB
Markdown
114 lines
4.8 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 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 |
|