From 88ac010c3c15afcf11579f0531dc6ca7522ea27f Mon Sep 17 00:00:00 2001 From: "A.Shakhmatov" Date: Sun, 9 Aug 2026 22:30:36 +0300 Subject: [PATCH] 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. --- tests/spec/aggregate/README.md | 23 +- tests/spec/aggregate/document-stages.json | 1245 +++++++++++++++++ .../aggregate/sources/document-stages.json | 112 ++ 3 files changed, 1379 insertions(+), 1 deletion(-) create mode 100644 tests/spec/aggregate/document-stages.json create mode 100644 tests/spec/aggregate/sources/document-stages.json diff --git a/tests/spec/aggregate/README.md b/tests/spec/aggregate/README.md index 6188800..08c9cfb 100644 --- a/tests/spec/aggregate/README.md +++ b/tests/spec/aggregate/README.md @@ -64,6 +64,7 @@ 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: @@ -72,7 +73,27 @@ 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. Expressions are exercised through +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. diff --git a/tests/spec/aggregate/document-stages.json b/tests/spec/aggregate/document-stages.json new file mode 100644 index 0000000..55a91c5 --- /dev/null +++ b/tests/spec/aggregate/document-stages.json @@ -0,0 +1,1245 @@ +{ + "description": "document-stages", + "schemaVersion": "1.4", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "aggregate-corpus" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "aggregate-corpus", + "documents": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + }, + "t": [] + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ], + "tests": [ + { + "description": "$addFields adds a computed field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$addFields": { + "b": { + "$multiply": [ + "$a", + 10 + ] + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ], + "b": 10 + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + }, + "t": [], + "b": 20 + }, + { + "_id": 3, + "a": 3, + "t": "not an array", + "b": 30 + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + }, + "b": 40 + } + ] + } + ] + }, + { + "description": "$addFields overwrites an existing field in place", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$addFields": { + "a": { + "$add": [ + "$a", + 100 + ] + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 101, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "a": 102, + "n": { + "x": 30 + }, + "t": [] + }, + { + "_id": 3, + "a": 103, + "t": "not an array" + }, + { + "_id": 4, + "a": 104, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ] + }, + { + "description": "$addFields with a field whose expression is missing", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$addFields": { + "b": "$nope" + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + }, + "t": [] + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ] + }, + { + "description": "$addFields on a dotted path", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$addFields": { + "n.z": 1 + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20, + "z": 1 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30, + "z": 1 + }, + "t": [] + }, + { + "_id": 3, + "a": 3, + "t": "not an array", + "n": { + "z": 1 + } + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50, + "z": 1 + } + } + ] + } + ] + }, + { + "description": "$set is $addFields", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$set": { + "b": { + "$add": [ + "$a", + 1 + ] + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ], + "b": 2 + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + }, + "t": [], + "b": 3 + }, + { + "_id": 3, + "a": 3, + "t": "not an array", + "b": 4 + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + }, + "b": 5 + } + ] + } + ] + }, + { + "description": "$unset a top-level field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unset": "a" + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "n": { + "x": 30 + }, + "t": [] + }, + { + "_id": 3, + "t": "not an array" + }, + { + "_id": 4, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ] + }, + { + "description": "$unset several fields", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unset": [ + "a", + "t" + ] + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "n": { + "x": 10, + "y": 20 + } + }, + { + "_id": 2, + "n": { + "x": 30 + } + }, + { + "_id": 3 + }, + { + "_id": 4, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ] + }, + { + "description": "$unset a nested path", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unset": "n.y" + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10 + }, + "t": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + }, + "t": [] + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40 + } + } + ] + } + ] + }, + { + "description": "$replaceRoot with a subdocument", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$match": { + "n": { + "$exists": true + } + } + }, + { + "$replaceRoot": { + "newRoot": "$n" + } + }, + { + "$sort": { + "x": 1 + } + } + ] + }, + "expectResult": [ + { + "x": 10, + "y": 20 + }, + { + "x": 30 + }, + { + "x": 40, + "y": 50 + } + ] + } + ] + }, + { + "description": "$replaceRoot with a built document", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$replaceRoot": { + "newRoot": { + "id": "$_id", + "double": { + "$multiply": [ + "$a", + 2 + ] + } + } + } + }, + { + "$sort": { + "id": 1 + } + } + ] + }, + "expectResult": [ + { + "id": 1, + "double": 2 + }, + { + "id": 2, + "double": 4 + }, + { + "id": 3, + "double": 6 + }, + { + "id": 4, + "double": 8 + } + ] + } + ] + }, + { + "description": "$replaceRoot with a missing path is an error", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$replaceRoot": { + "newRoot": "$n" + } + } + ] + }, + "expectError": { + "isError": true, + "errorCode": 40228 + } + } + ] + }, + { + "description": "$replaceRoot with a non-document is an error", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$replaceRoot": { + "newRoot": "$a" + } + } + ] + }, + "expectError": { + "isError": true, + "errorCode": 40228 + } + } + ] + }, + { + "description": "$unwind an array field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$unwind": "$t" + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 1 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 2 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 3 + } + ] + } + ] + }, + { + "description": "$unwind drops empty arrays and missing fields", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unwind": "$t" + }, + { + "$sort": { + "_id": 1, + "t": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 1 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 2 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 3 + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + } + ] + } + ] + }, + { + "description": "$unwind a field that is not an array keeps it whole", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 3 + } + }, + { + "$unwind": "$t" + } + ] + }, + "expectResult": [ + { + "_id": 3, + "a": 3, + "t": "not an array" + } + ] + } + ] + }, + { + "description": "$unwind in its document form", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unwind": { + "path": "$t" + } + }, + { + "$sort": { + "_id": 1, + "t": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 1 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 2 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 3 + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + } + ] + } + ] + }, + { + "description": "$unwind preserving null and empty arrays", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unwind": { + "path": "$t", + "preserveNullAndEmptyArrays": true + } + }, + { + "$sort": { + "_id": 1, + "t": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 1 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 2 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 3 + }, + { + "_id": 2, + "a": 2, + "n": { + "x": 30 + } + }, + { + "_id": 3, + "a": 3, + "t": "not an array" + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + } + } + ] + } + ] + }, + { + "description": "$unwind with an index field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$unwind": { + "path": "$t", + "includeArrayIndex": "i" + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 1, + "i": 0 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 2, + "i": 1 + }, + { + "_id": 1, + "a": 1, + "n": { + "x": 10, + "y": 20 + }, + "t": 3, + "i": 2 + } + ] + } + ] + }, + { + "description": "$unwind of a path without a $ is an error", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unwind": "t" + } + ] + }, + "expectError": { + "isError": true, + "errorCode": 28818 + } + } + ] + }, + { + "description": "$project computes a field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$project": { + "b": { + "$add": [ + "$a", + 1 + ] + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "b": 2 + }, + { + "_id": 2, + "b": 3 + }, + { + "_id": 3, + "b": 4 + }, + { + "_id": 4, + "b": 5 + } + ] + } + ] + }, + { + "description": "$project renames with a path", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$project": { + "value": "$a" + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "value": 1 + }, + { + "_id": 2, + "value": 2 + }, + { + "_id": 3, + "value": 3 + }, + { + "_id": 4, + "value": 4 + } + ] + } + ] + }, + { + "description": "$project a nested inclusion", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$project": { + "n": { + "x": 1 + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "n": { + "x": 10 + } + }, + { + "_id": 2, + "n": { + "x": 30 + } + }, + { + "_id": 3 + }, + { + "_id": 4, + "n": { + "x": 40 + } + } + ] + } + ] + }, + { + "description": "$addFields then $match sees the new field", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$addFields": { + "b": { + "$multiply": [ + "$a", + 10 + ] + } + } + }, + { + "$match": { + "b": { + "$gt": 20 + } + } + }, + { + "$sort": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 3, + "a": 3, + "t": "not an array", + "b": 30 + }, + { + "_id": 4, + "a": 4, + "n": { + "x": 40, + "y": 50 + }, + "b": 40 + } + ] + } + ] + }, + { + "description": "$unwind then $group counts the elements", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$unwind": "$t" + }, + { + "$group": { + "_id": null, + "n": { + "$sum": 1 + } + } + } + ] + }, + "expectResult": [ + { + "_id": null, + "n": 4 + } + ] + } + ] + } + ] +} diff --git a/tests/spec/aggregate/sources/document-stages.json b/tests/spec/aggregate/sources/document-stages.json new file mode 100644 index 0000000..9552778 --- /dev/null +++ b/tests/spec/aggregate/sources/document-stages.json @@ -0,0 +1,112 @@ +{ + "_comment": [ + "Inputs only. Expectations are measured -- see record.js.", + "The stages that rewrite a document rather than select or reorder one:", + "$addFields/$set, $unset, $replaceRoot, $unwind, and $project's computed", + "fields. A $sort at the end wherever the order is not already fixed." + ], + "documents": [ + { "_id": 1, "a": 1, "n": { "x": 10, "y": 20 }, "t": [1, 2, 3] }, + { "_id": 2, "a": 2, "n": { "x": 30 }, "t": [] }, + { "_id": 3, "a": 3, "t": "not an array" }, + { "_id": 4, "a": 4, "n": { "x": 40, "y": 50 } } + ], + "cases": [ + { + "description": "$addFields adds a computed field", + "pipeline": [{ "$addFields": { "b": { "$multiply": ["$a", 10] } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$addFields overwrites an existing field in place", + "pipeline": [{ "$addFields": { "a": { "$add": ["$a", 100] } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$addFields with a field whose expression is missing", + "pipeline": [{ "$addFields": { "b": "$nope" } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$addFields on a dotted path", + "pipeline": [{ "$addFields": { "n.z": 1 } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$set is $addFields", + "pipeline": [{ "$set": { "b": { "$add": ["$a", 1] } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$unset a top-level field", + "pipeline": [{ "$unset": "a" }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$unset several fields", + "pipeline": [{ "$unset": ["a", "t"] }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$unset a nested path", + "pipeline": [{ "$unset": "n.y" }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$replaceRoot with a subdocument", + "pipeline": [{ "$match": { "n": { "$exists": true } } }, { "$replaceRoot": { "newRoot": "$n" } }, { "$sort": { "x": 1 } }] + }, + { + "description": "$replaceRoot with a built document", + "pipeline": [{ "$replaceRoot": { "newRoot": { "id": "$_id", "double": { "$multiply": ["$a", 2] } } } }, { "$sort": { "id": 1 } }] + }, + { + "description": "$replaceRoot with a missing path is an error", + "pipeline": [{ "$replaceRoot": { "newRoot": "$n" } }] + }, + { + "description": "$replaceRoot with a non-document is an error", + "pipeline": [{ "$replaceRoot": { "newRoot": "$a" } }] + }, + { + "description": "$unwind an array field", + "pipeline": [{ "$match": { "_id": 1 } }, { "$unwind": "$t" }] + }, + { + "description": "$unwind drops empty arrays and missing fields", + "pipeline": [{ "$unwind": "$t" }, { "$sort": { "_id": 1, "t": 1 } }] + }, + { + "description": "$unwind a field that is not an array keeps it whole", + "pipeline": [{ "$match": { "_id": 3 } }, { "$unwind": "$t" }] + }, + { + "description": "$unwind in its document form", + "pipeline": [{ "$unwind": { "path": "$t" } }, { "$sort": { "_id": 1, "t": 1 } }] + }, + { + "description": "$unwind preserving null and empty arrays", + "pipeline": [{ "$unwind": { "path": "$t", "preserveNullAndEmptyArrays": true } }, { "$sort": { "_id": 1, "t": 1 } }] + }, + { + "description": "$unwind with an index field", + "pipeline": [{ "$match": { "_id": 1 } }, { "$unwind": { "path": "$t", "includeArrayIndex": "i" } }] + }, + { + "description": "$unwind of a path without a $ is an error", + "pipeline": [{ "$unwind": "t" }] + }, + { + "description": "$project computes a field", + "pipeline": [{ "$project": { "b": { "$add": ["$a", 1] } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$project renames with a path", + "pipeline": [{ "$project": { "value": "$a" } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$project a nested inclusion", + "pipeline": [{ "$project": { "n": { "x": 1 } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$addFields then $match sees the new field", + "pipeline": [{ "$addFields": { "b": { "$multiply": ["$a", 10] } } }, { "$match": { "b": { "$gt": 20 } } }, { "$sort": { "_id": 1 } }] + }, + { + "description": "$unwind then $group counts the elements", + "pipeline": [{ "$unwind": "$t" }, { "$group": { "_id": null, "n": { "$sum": 1 } } }] + } + ] +}