diff options
author | Charlie Swanson <charlie.swanson@mongodb.com> | 2022-03-31 13:11:42 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-03-31 13:47:31 +0000 |
commit | ef615c3302e158865bd8afe1068dcc65abf653ae (patch) | |
tree | b18c512109434e2b92fddeb78d55d84a743e9de3 | |
parent | 1dd40ad6ed8aa8a636e7de003185444d04810f75 (diff) | |
download | mongo-ef615c3302e158865bd8afe1068dcc65abf653ae.tar.gz |
SERVER-64260 Beef up testing stressing projection semantics
-rw-r--r-- | jstests/core/projection.js | 120 | ||||
-rw-r--r-- | jstests/core/projection_semantics.js | 541 |
2 files changed, 541 insertions, 120 deletions
diff --git a/jstests/core/projection.js b/jstests/core/projection.js deleted file mode 100644 index 99addb31fee..00000000000 --- a/jstests/core/projection.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Tests for FieldMatcher (later renamed to Projection). - */ - -(function() { -'use strict'; -const collNamePrefix = 'jstests_projection_'; -let collCount = 0; - -let res; - -// -// Test cases originally in fm1.js. -// - -let t = db.getCollection(collNamePrefix + collCount++); -t.drop(); - -let doc = {_id: 0, foo: {bar: 1}}; -assert.commandWorked(t.insert(doc)); - -res = t.find({}, {foo: 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq(doc, res[0], tojson(res)); - -res = t.find({}, {'foo.bar': 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq(doc, res[0], tojson(res)); - -res = t.find({}, {'baz': 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq({_id: 0}, res[0], tojson(res)); - -res = t.find({}, {'baz.qux': 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq({_id: 0}, res[0], tojson(res)); - -res = t.find({}, {'foo.qux': 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq({_id: 0, foo: {}}, res[0], tojson(res)); - -// -// Test cases originally in fm2.js -// - -t = db.getCollection(collNamePrefix + collCount++); -t.drop(); - -doc = { - _id: 1, - one: {two: {three: 'four'}} -}; -assert.commandWorked(t.insert(doc)); - -res = t.find({}, {'one.two': 1}).toArray(); -assert.eq(1, res.length, tojson(res)); -assert.docEq(doc, res[0], tojson(res)); -assert.eq(1, Object.keySet(res[0].one).length, tojson(res)); - -// -// Test cases originally in fm3.js. -// - -t = db.getCollection(collNamePrefix + collCount++); -t.drop(); - -assert.commandWorked(t.insert({a: [{c: {e: 1, f: 1}}, {d: 2}, 'z'], b: 1})); - -res = t.findOne({}, {a: 1}); -assert.eq(res.a, [{c: {e: 1, f: 1}}, {d: 2}, 'z'], "one a"); -assert.eq(res.b, undefined, "one b"); - -res = t.findOne({}, {a: 0}); -assert.eq(res.a, undefined, "two a"); -assert.eq(res.b, 1, "two b"); - -res = t.findOne({}, {'a.d': 1}); -assert.eq(res.a, [{}, {d: 2}], "three a"); -assert.eq(res.b, undefined, "three b"); - -res = t.findOne({}, {'a.d': 0}); -assert.eq(res.a, [{c: {e: 1, f: 1}}, {}, 'z'], "four a"); -assert.eq(res.b, 1, "four b"); - -res = t.findOne({}, {'a.c': 1}); -assert.eq(res.a, [{c: {e: 1, f: 1}}, {}], "five a"); -assert.eq(res.b, undefined, "five b"); - -res = t.findOne({}, {'a.c': 0}); -assert.eq(res.a, [{}, {d: 2}, 'z'], "six a"); -assert.eq(res.b, 1, "six b"); - -res = t.findOne({}, {'a.c.e': 1}); -assert.eq(res.a, [{c: {e: 1}}, {}], "seven a"); -assert.eq(res.b, undefined, "seven b"); - -res = t.findOne({}, {'a.c.e': 0}); -assert.eq(res.a, [{c: {f: 1}}, {d: 2}, 'z'], "eight a"); -assert.eq(res.b, 1, "eight b"); - -// -// Test cases originally in fm4.js -// - -t = db.getCollection(collNamePrefix + collCount++); -t.drop(); - -assert.commandWorked(t.insert({_id: 3, a: 1, b: 1})); - -assert.docEq(t.findOne({}, {_id: 1}), {_id: 3}, "1"); -assert.docEq(t.findOne({}, {_id: 0}), {a: 1, b: 1}, "2"); - -assert.docEq(t.findOne({}, {_id: 1, a: 1}), {_id: 3, a: 1}, "3"); -assert.docEq(t.findOne({}, {_id: 0, a: 1}), {a: 1}, "4"); - -assert.docEq(t.findOne({}, {_id: 0, a: 0}), {b: 1}, "6"); -assert.docEq(t.findOne({}, {a: 0}), {_id: 3, b: 1}, "5"); - -assert.docEq(t.findOne({}, {_id: 1, a: 0}), {_id: 3, b: 1}, "7"); -})(); diff --git a/jstests/core/projection_semantics.js b/jstests/core/projection_semantics.js new file mode 100644 index 00000000000..7ed50ac798a --- /dev/null +++ b/jstests/core/projection_semantics.js @@ -0,0 +1,541 @@ +/** + * Tests the behavior of projection for dotted paths, including edge cases where the path only + * sometimes exists. + */ +(function() { +"use strict"; + +const coll = db[jsTestName()]; + +// Tests that when 'projection' is applied to 'input', we get 'expectedOutput'. +// Tests that this remains true if indexes are added, or if we use aggregation instead of find. +function testInputOutput({input, projection, expectedOutput, interestingIndexes = []}) { + coll.drop(); + assert.commandWorked(coll.insert(input)); + assert.docEq(coll.findOne({}, projection), expectedOutput); + for (let indexSpec of interestingIndexes) { + assert.commandWorked(coll.createIndex(indexSpec)); + assert.docEq(coll.find({}, projection).hint(indexSpec).toArray()[0], expectedOutput); + assert.commandWorked(coll.dropIndex(indexSpec)); + } + assert.docEq(coll.aggregate([{$project: projection}]).toArray()[0], expectedOutput); +} + +// The basics: what happens when I include a top-level field? +(function testTopLevelInclusion() { + // Test the basic "include a" projection. This should implicitly include the _id. + const testIncludeA = (input, output) => testInputOutput({ + input: input, + projection: {a: 1}, + expectedOutput: output, + interestingIndexes: [{_id: 1, a: 1}, {a: 1, _id: 1}] + }); + + // Test some basic "normal" cases. + testIncludeA({_id: 0, a: "demo", b: "other", x: "extra"}, {_id: 0, a: "demo"}); + testIncludeA({_id: 1, a: "demo", aWSuffix: "other", 'a.b': "extra"}, {_id: 1, a: "demo"}); + testIncludeA({_id: 2, a: null, b: "other", x: "extra"}, {_id: 2, a: null}); + + // Test including "a" when "a" is missing/not present. + // TODO SERVER-23229 this will return different results if there is a covering index, so here + // but not elsewhere we don't use any "interestingIndexes". + testInputOutput({ + input: {_id: 0, b: "other", x: "extra"}, + projection: {a: 1}, + expectedOutput: {_id: 0}, + interestingIndexes: [] + }); + testInputOutput( + {input: {_id: 0}, projection: {a: 1}, expectedOutput: {_id: 0}, interestingIndexes: []}); + + // Test a range of interesting values for "a". We expect everything to be preserved unmodified. + const testIdentityInclusionA = (input) => testInputOutput({ + input: input, + projection: {a: 1}, + expectedOutput: input, + interestingIndexes: [{_id: 1, a: 1}, {a: 1, _id: 1}] + }); + testIdentityInclusionA({_id: 1, a: null}); + testIdentityInclusionA({_id: 2, a: undefined}); + testIdentityInclusionA({_id: 3, a: {}}); + testIdentityInclusionA({_id: 4, a: []}); + testIdentityInclusionA({_id: 5, a: {x: 1, b: "scalar"}}); + testIdentityInclusionA({_id: 6, a: "scalar"}); + testIdentityInclusionA({_id: 7, a: {b: {}}}); + testIdentityInclusionA({_id: 8, a: [null]}); + testIdentityInclusionA({_id: 9, a: ["scalar"]}); + testIdentityInclusionA({_id: 10, a: [[]]}); + testIdentityInclusionA({_id: 11, a: [{}]}); + testIdentityInclusionA({_id: 12, a: [1, {}, 2]}); + testIdentityInclusionA({_id: 13, a: [[1, 2], [{}], 2]}); + testIdentityInclusionA({_id: 14, a: [{b: "scalar"}]}); + + // Now test with the same documents but excluding the "_id" field. + const testIncludeOnlyA = (input, output) => testInputOutput({ + input: input, + projection: {a: 1, _id: 0}, + expectedOutput: output, + // Can't use {a: "hashed"} here because it doesn't support array values. + // Could use {a: "$**"}, but it can't be hinted without a query predicate so we'll leave + // that coverage for elsewhere rather than add complexity to get a valid query predicate. + interestingIndexes: [{a: 1}, {a: -1}] + }); + + // The "basics" again. + testIncludeOnlyA({_id: 0, a: "demo", b: "other", x: "extra"}, {a: "demo"}); + testIncludeOnlyA({_id: 1, a: "demo", aWSuffix: "other", 'a.b': "extra"}, {a: "demo"}); + testIncludeOnlyA({_id: 2, a: null, b: "other", x: "extra"}, {a: null}); + + // Missing 'a' value again. + // TODO SERVER-23229 this will return different results if there is a covering index, so here + // but not elsewhere we don't use any "interestingIndexes". + testInputOutput({ + input: {_id: 0, b: "other", x: "extra"}, + projection: {a: 1, _id: 0}, + expectedOutput: {}, + interestingIndexes: [] + }); + testInputOutput( + {input: {_id: 0}, projection: {a: 1, _id: 0}, expectedOutput: {}, interestingIndexes: []}); + + // Just a couple of the cases above to confirm the same behavior just without the _id. + testIncludeOnlyA({_id: 1, a: null}, {a: null}); + testIncludeOnlyA({_id: 3, a: {}}, {a: {}}); + testIncludeOnlyA({_id: 4, a: []}, {a: []}); + testIncludeOnlyA({_id: 5, a: {x: 1, b: "scalar"}}, {a: {x: 1, b: "scalar"}}); + testIncludeOnlyA({_id: 7, a: {b: {}}}, {a: {b: {}}}); + testIncludeOnlyA({_id: 14, a: [{b: "scalar"}]}, {a: [{b: "scalar"}]}); +}()); + +// Now test one level of nesting - a single "dot" in the path. +(function testInclusionOneLevelOfNesting() { + // Test the basic "include a.b" projection. This should implicitly include the _id. + const testIncludeADotB = (input, output) => testInputOutput({ + input: input, + projection: {'a.b': 1}, + expectedOutput: output, + interestingIndexes: [{_id: 1, 'a.b': 1}, {'a.b': 1, _id: 1}] + }); + + // Test some basic "normal" cases. + // Test that it excludes extra fields at the root and at the sub-document. + testIncludeADotB({_id: 0, a: {b: "demo", y: "extra"}, x: "extra"}, {_id: 0, a: {b: "demo"}}); + // Test that (at least for now) the dotted path doesn't work great here. + testIncludeADotB({_id: 1, a: {b: "demo"}, 'a.b': "extra"}, {_id: 1, a: {b: "demo"}}); + // Test that '_id' within a sub-document is excluded. + testIncludeADotB({_id: 2, a: {b: "demo", _id: "extra"}}, {_id: 2, a: {b: "demo"}}); + + // Test array use case. + testIncludeADotB({_id: 3, a: [{b: 1, c: 1}, {b: 2, c: 2}], x: "extra"}, + {_id: 3, a: [{b: 1}, {b: 2}]}); + // Test that a missing field within an object in an array will show up as an empty object. + testIncludeADotB({_id: 4, a: [{b: 1, c: 1}, {c: 2}], x: "extra"}, {_id: 4, a: [{b: 1}, {}]}); + + // Test including "a.b" when "a.b" is missing/not present. + // + // TODO SERVER-23229 this will return different results if there is a covering index, so here + // but not elsewhere we don't use any "interestingIndexes". + // If there's a covered plan we will always construct {a: {b: null}} when we don't see an "a.b" + // value, which is not always correct. + // + // Interestingly, this bug disappears if the index is multikey ('a' or 'a.b' are arrays), since + // we will need to add a fetch to reconstruct the array anyway and will figure out the correct + // answer in the process. + const testADotBNoIndexes = (input, output) => testInputOutput( + {input: input, projection: {'a.b': 1}, expectedOutput: output, interestingIndexes: []}); + + testADotBNoIndexes({_id: 0, b: "other", x: "extra"}, {_id: 0}); + testADotBNoIndexes({_id: 1}, {_id: 1}); + testADotBNoIndexes({_id: 2, a: {}}, {_id: 2, a: {}}); + testADotBNoIndexes({_id: 3, a: "scalar"}, {_id: 3}); + + const testIncludeOnlyADotB = (input, output) => testInputOutput({ + input: input, + projection: {'a.b': 1, _id: 0}, + expectedOutput: output, + interestingIndexes: [{'a.b': 1}, {'a.b': -1}] + }); + + // The "basics" again - no _id this time. + testIncludeOnlyADotB({_id: 0, a: {b: "demo", y: "extra"}, x: "extra"}, {a: {b: "demo"}}); + testIncludeOnlyADotB({_id: 1, a: {b: "demo"}, 'a.b': "extra"}, {a: {b: "demo"}}); + testIncludeOnlyADotB({_id: 2, a: {b: "demo", _id: "extra"}}, {a: {b: "demo"}}); + + testIncludeOnlyADotB({_id: 3, a: [{b: 1, c: 1}, {b: 2, c: 2}], x: "extra"}, + {a: [{b: 1}, {b: 2}]}); + testIncludeOnlyADotB({_id: 4, a: [{b: 1, c: 1}, {c: 2}], x: "extra"}, {a: [{b: 1}, {}]}); + + // More cases where 'a.b' doesn't exist - but with arrays this time. + testIncludeOnlyADotB({_id: 4, a: [], x: "extra"}, {a: []}); + testIncludeOnlyADotB({_id: 5, a: [{}, {}], x: "extra"}, {a: [{}, {}]}); + testIncludeOnlyADotB({_id: 6, a: ["scalar", "scalar"], x: "extra"}, {a: []}); + testIncludeOnlyADotB({_id: 7, a: [null]}, {a: []}); + // This is an interesting case: the scalars are ignored but the shadow documents are preserved. + testIncludeOnlyADotB({_id: 8, a: ["scalar", {}, "scalar", {c: 1}], x: "extra"}, {a: [{}, {}]}); + // Further interest: the array within the array is preserved. + testIncludeOnlyADotB({_id: 9, a: [[]]}, {a: [[]]}); + // But not the scalar elements of it. + testIncludeOnlyADotB({_id: 10, a: [[1, 2, 3]]}, {a: [[]]}); + // But if there's a "b" again we see that. + testIncludeOnlyADotB({_id: 10, a: [[1, {b: 1}, {b: 2, c: 2}, "scalar"]]}, + {a: [[{b: 1}, {b: 2}]]}); + testIncludeOnlyADotB({ + _id: 10, + a: [ + ["x", {b: 1}, {b: 2, c: 2}, "x"], + [[{b: 1}]], + [{b: 1}, [{b: 2}], [[{b: [2]}]]], + ] + }, + { + a: [ + [{b: 1}, {b: 2}], + [[{b: 1}]], + [{b: 1}, [{b: 2}], [[{b: [2]}]]], + ] + }); + testIncludeOnlyADotB({_id: 11, a: [[], [[], [], [1], [{c: 1}]], {b: 1}]}, { + a: [ + [], + [[], [], [], [{}]], + {b: 1}, + ] + }); +}()); + +(function testInclusionLevelsOfNesting() { + // Test the basic "include a.b.c" projection. We've seen the implicit '_id' behavior above so + // we'll always project _id out for these tests. + const testIncludeADotBDotC = (input, output) => testInputOutput({ + input: input, + projection: {'a.b.c': 1, _id: 0}, + expectedOutput: output, + interestingIndexes: [{'a.b.c': 1}, {'a.b.c': -1}] + }); + + // Test some basic "normal" cases. + // Test that it excludes extra fields at the root and at the sub-document + testIncludeADotBDotC({a: {b: {c: "demo", z: "extra"}, y: "extra"}, x: "extra"}, + {a: {b: {c: "demo"}}}); + + // Test array use cases. + // 'a' is an array. + testIncludeADotBDotC({a: [{b: {c: 1, d: 1}}, {b: {c: 2, d: 2}}]}, + {a: [{b: {c: 1}}, {b: {c: 2}}]}); + // 'a.b' is an array. + testIncludeADotBDotC({a: {b: [{c: 1, d: 1}, {c: 2, d: 2}]}}, {a: {b: [{c: 1}, {c: 2}]}}); + // 'a' and 'a.b' are arrays. + testIncludeADotBDotC( + {a: [{b: [{c: 1, d: 1}, {c: 2, d: 2}]}, {b: [{c: 3, d: 3}, {c: 4, d: 4}]}]}, + {a: [{b: [{c: 1}, {c: 2}]}, {b: [{c: 3}, {c: 4}]}]}); + // 'a' and 'a.b' and 'a.b.c' are arrays. + testIncludeADotBDotC( + { + a: [ + {b: [{c: [1, 2, 3], d: 1}, {c: [2, 3, 4], d: 2}]}, + {b: [{c: [3, 4, 5], d: 3}, {c: [4, 5, 6], d: 4}]} + ] + }, + {a: [{b: [{c: [1, 2, 3]}, {c: [2, 3, 4]}]}, {b: [{c: [3, 4, 5]}, {c: [4, 5, 6]}]}]}); + + // Test that when the path is missing we preserve the structure. + testIncludeADotBDotC({ + a: [ + // No 'b' here. + {bPrime: [{c: "x"}, {c: "x"}]}, + // No 'c' anywhere + {b: [{cPrime: "x", d: 1}, {cPrime: "x", d: 2}]}, + // Only some 'c's + {b: [{c: 3, d: 3}, {cPrime: "x", d: 4}, {c: 5}]}, + // One more without a 'b' to prove we get trailing empty objects. + {bPrime: {somethingDifferent: "just because"}}, + ] + }, + { + a: [ + {/* bPrime was here */}, + {b: [{}, {} /* all cPrimes */]}, + {b: [{c: 3}, {/*cPrime was here */}, {c: 5}]}, + {/* bPrime again */} + ] + }); + + // Test including "a.b.c" when "a.b.c" is missing/not present. + // + // TODO SERVER-23229 this will return different results if there is a covering index, so here + // but not elsewhere we don't use any "interestingIndexes". + // If there's a covered plan we will always construct {a: {b: {c: null}}} when we don't see an + // "a.b.c" value, which is not always correct. + // + // Interestingly, this bug disappears if the index is multikey ('a.b.c' or any parent is an + // array), since we will need to add a fetch to reconstruct the array anyway and will figure out + // the correct answer in the process. + const testADotBDotCNoIndexes = (input, output) => testInputOutput({ + input: input, + projection: {'a.b.c': 1, _id: 0}, + expectedOutput: output, + interestingIndexes: [] + }); + + testADotBDotCNoIndexes({}, {}); + testADotBDotCNoIndexes({b: "other", x: "extra"}, {}); + testADotBDotCNoIndexes({a: "scalar"}, {}); + testADotBDotCNoIndexes({a: null}, {}); + testADotBDotCNoIndexes({a: {}}, {a: {}}); + testADotBDotCNoIndexes({a: {bPrime: {c: 1}}}, {a: {}}); + testADotBDotCNoIndexes({a: {b: "scalar"}}, {a: {}}); + testADotBDotCNoIndexes({a: {b: {x: 1, y: 1}}}, {a: {b: {}}}); + + const testIncludeOnlyADotBDotC = (input, output) => testInputOutput({ + input: input, + projection: {'a.b.c': 1, _id: 0}, + expectedOutput: output, + interestingIndexes: [{'a.b.c': 1}, {'a.b.c': -1}] + }); + + // More cases where 'a.b.c' doesn't exist - but with arrays this time. + + // The cases from above with just 'a.b' are mostly the same for 'a.b.c': + testIncludeOnlyADotBDotC({a: [], x: "extra"}, {a: []}); + testIncludeOnlyADotBDotC({a: [{}, {}], x: "extra"}, {a: [{}, {}]}); + testIncludeOnlyADotBDotC({a: ["scalar", "scalar"], x: "extra"}, {a: []}); + testIncludeOnlyADotBDotC({a: [null]}, {a: []}); + testIncludeOnlyADotBDotC({a: ["scalar", {}, "scalar", {c: 1}], x: "extra"}, {a: [{}, {}]}); + testIncludeOnlyADotBDotC({a: [[]]}, {a: [[]]}); + testIncludeOnlyADotBDotC({a: [[1, 2, 3]]}, {a: [[]]}); + // Now some with 'a.b' existing but no 'a.b.c'. + testIncludeOnlyADotBDotC({a: {b: []}}, {a: {b: []}}); + testIncludeOnlyADotBDotC({a: {b: ["scalar"]}}, {a: {b: []}}); + testIncludeOnlyADotBDotC({a: {b: [[]]}}, {a: {b: [[]]}}); + testIncludeOnlyADotBDotC({a: [{b: "scalar"}]}, {a: [{}]}); + testIncludeOnlyADotBDotC({a: [{b: []}]}, {a: [{b: []}]}); + testIncludeOnlyADotBDotC({a: [{b: ["scalar"]}]}, {a: [{b: []}]}); + testIncludeOnlyADotBDotC({a: [{b: [[]]}]}, {a: [{b: [[]]}]}); + testIncludeOnlyADotBDotC({a: [{b: {x: 1}}]}, {a: [{b: {}}]}); + testIncludeOnlyADotBDotC({a: [{b: [{}]}]}, {a: [{b: [{}]}]}); + testIncludeOnlyADotBDotC({a: [[1, {b: 1}, {b: 2, c: 2}, "scalar"]]}, {a: [[{}, {}]]}); + testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{}], 2]}, 2]}, {a: [{b: [[], [{}]]}]}); + testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}], 2]}, + {a: [[], [{b: [[], [{c: [[1, 2], [{}], 2]}]]}]]}); + testIncludeOnlyADotBDotC({ + a: [ + ["x", {b: 1}, {b: 2, c: 2}, "x"], + [[{b: 1}]], + [{b: 1}, [{b: 2}], [[{b: [2]}]]], + ] + }, + { + a: [ + [{}, {}], + [[{}]], + [{}, [{}], [[{b: []}]]], + ] + }); + testIncludeOnlyADotBDotC({a: [[], [[], [], [1], [{x: 1}]], {b: 1}]}, { + a: [ + [], + [[], [], [], [{}]], + {}, + ] + }); + + // Now some new cases where 'a.b.c' sometimes exists. + // One array path. + // 'a' is array. + testIncludeOnlyADotBDotC({a: [{b: {x: 1, c: "scalar"}}]}, {a: [{b: {c: "scalar"}}]}); + testIncludeOnlyADotBDotC({a: [1, {b: {c: {x: 1}}}, 2]}, {a: [{b: {c: {x: 1}}}]}); + testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: "scalar"}}, 2]}, {a: [{b: {c: "scalar"}}]}); + testIncludeOnlyADotBDotC({a: [[1, 2], [{b: {x: 1, c: "scalar"}}], 2]}, + {a: [[], [{b: {c: "scalar"}}]]}); + // 'b' is array. + testIncludeOnlyADotBDotC({a: {b: [{c: "scalar"}, {c: "scalar2"}]}}, + {a: {b: [{c: "scalar"}, {c: "scalar2"}]}}); + testIncludeOnlyADotBDotC({a: {b: [1, {c: "scalar"}, 2]}}, {a: {b: [{c: "scalar"}]}}); + testIncludeOnlyADotBDotC({a: {b: [[1, 2], [{c: "scalar"}], 2]}}, + {a: {b: [[], [{c: "scalar"}]]}}); + // 'c' is array. + testIncludeOnlyADotBDotC({a: {x: 1, b: {x: 1, c: ["scalar"]}}}, {a: {b: {c: ["scalar"]}}}); + testIncludeOnlyADotBDotC({a: {b: {c: [[1, 2], [{}], 2]}}}, {a: {b: {c: [[1, 2], [{}], 2]}}}); + + // Two arrays. + // 'b' and 'c' are arrays. + testIncludeOnlyADotBDotC({a: {x: 1, b: [1, {c: [[1, 2], [{}], 2]}, 2]}}, + {a: {b: [{c: [[1, 2], [{}], 2]}]}}); + testIncludeOnlyADotBDotC({a: {x: 1, b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}}, + {a: {b: [[], [{c: [[1, 2], [{}], 2]}]]}}); + // 'a' and 'b' are arrays. + testIncludeOnlyADotBDotC({a: [{b: [{c: "scalar"}]}]}, {a: [{b: [{c: "scalar"}]}]}); + testIncludeOnlyADotBDotC({a: [1, {b: [{c: {x: 1}}]}, 2]}, {a: [{b: [{c: {x: 1}}]}]}); + testIncludeOnlyADotBDotC({a: [1, {b: [1, {c: "scalar"}, 2]}, 2]}, {a: [{b: [{c: "scalar"}]}]}); + testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{c: "scalar"}], 2]}, 2]}, + {a: [{b: [[], [{c: "scalar"}]]}]}); + testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [1, {c: "scalar"}, 2]}], 2]}, + {a: [[], [{b: [{c: "scalar"}]}]]}); + testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: "scalar"}], 2]}], 2]}, + {a: [[], [{b: [[], [{c: "scalar"}]]}]]}); + // 'a' and 'c' are arrays. + testIncludeOnlyADotBDotC({a: [1, {b: {c: [1, {}, 2]}}, 2]}, {a: [{b: {c: [1, {}, 2]}}]}); + testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: [[]]}}, 2]}, {a: [{b: {c: [[]]}}]}); + testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: [1, {}, 2]}}, 2]}, {a: [{b: {c: [1, {}, 2]}}]}); + + // Three arrays. + testIncludeOnlyADotBDotC({a: [{b: [{c: ["scalar"]}]}]}, {a: [{b: [{c: ["scalar"]}]}]}); + testIncludeOnlyADotBDotC({a: [{b: [1, {c: ["scalar"]}, 2]}]}, {a: [{b: [{c: ["scalar"]}]}]}); + testIncludeOnlyADotBDotC({a: [{b: [[1, 2], [{c: ["scalar"]}], 2]}]}, + {a: [{b: [[], [{c: ["scalar"]}]]}]}); + testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{c: [1, {}, 2]}], 2]}, 2]}, + {a: [{b: [[], [{c: [1, {}, 2]}]]}]}); + testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}], 2]}, + {a: [[], [{b: [[], [{c: [[1, 2], [{}], 2]}]]}]]}); +}()); + +// Now test the exclusion semantics. This part is a lot smaller since a lot of the behaviors mirror +// inclusion projection. +(function testExclusionSemantics() { + // Test some basic top-level flat examples. + testInputOutput({ + input: {_id: 0, a: 1, b: 1, c: 1}, + projection: {a: 0}, + expectedOutput: {_id: 0, b: 1, c: 1} + }); + testInputOutput({ + input: {_id: 0, a: 1, b: 1, c: 1}, + projection: {a: 0, b: 0}, + expectedOutput: {_id: 0, c: 1} + }); + + // Test some dotted examples. + testInputOutput({ + input: {_id: 0, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}}, + projection: {"a.b": 0}, + expectedOutput: {_id: 0, a: {c: 1, d: 1}, x: {y: 1, z: 1}} + }); + // One notable difference between inclusion and exclusion projections is that parent's scalar + // values remain untouched during an exclusion projection. The "scalar" here remains. In an + // inclusion projection, these would disappear. + testInputOutput({ + input: {_id: 0, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": 0}, + expectedOutput: {_id: 0, a: ["scalar", {c: 1, d: 1}, {c: 2}, {}]} + }); + testInputOutput({ + input: {_id: 0, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": 0, "a.c": 0}, + expectedOutput: {_id: 0, a: [{d: 1}, {}, {}]} + }); + testInputOutput({ + input: {_id: 0, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": 0, "a.c": 0}, + expectedOutput: {_id: 0, a: [{d: 1}, {}, {}]} + }); + testInputOutput( + {input: {_id: 0, a: []}, projection: {"a.b": 0}, expectedOutput: {_id: 0, a: []}}); + testInputOutput({ + input: {_id: 0, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]}, + projection: {"a.b.c": 0}, + expectedOutput: {_id: 0, a: [[], [{b: [[1, 2], {d: 1}]}]]} + }); +}()); + +// Now test the semantics of projecting a computed field. Again this is fairly similar to inclusion +// projections but with a few interesting twists. +(function testComputedProjections() { + // Test some basic top-level flat examples. + testInputOutput({ + input: {_id: 0, a: 1, b: 1, c: 1}, + projection: {a: {$literal: 0}}, + expectedOutput: {_id: 0, a: 0} + }); + testInputOutput({ + input: {_id: 0, a: 1, b: 1, c: 1}, + projection: {a: {$literal: 0}, b: "string"}, + expectedOutput: {_id: 0, a: 0, b: "string"} + }); + + // Test some dotted examples. + testInputOutput({ + input: {_id: 0, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}}, + projection: {"a.b": "new value"}, + expectedOutput: {_id: 0, a: {b: "new value"}} + }); + // One notable difference for computed projections is that they overwrite scalars rather than + // ignoring or removing them. + testInputOutput({ + input: {_id: 0, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": "new value"}, + expectedOutput: + {_id: 0, a: [{b: "new value"}, {b: "new value"}, {b: "new value"}, {b: "new value"}]} + }); + testInputOutput({ + input: {_id: 0, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": "new value", "a.c": "new C"}, + expectedOutput: { + _id: 0, + a: [ + {b: "new value", c: "new C"}, + {b: "new value", c: "new C"}, + {b: "new value", c: "new C"} + ] + } + }); + testInputOutput({ + input: {_id: 0, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]}, + projection: {"a.b": "new value", "a.c": "new C"}, + expectedOutput: { + _id: 0, + a: [ + {b: "new value", c: "new C"}, + {b: "new value", c: "new C"}, + {b: "new value", c: "new C"} + ] + } + }); + testInputOutput({ + input: {_id: 0, a: []}, + projection: {"a.b": "new value"}, + expectedOutput: {_id: 0, a: []} + }); + // Computed projections will traverse through double arrays and preserve structure. For example, + // we preserve two brackets inside the existing 'b: [[1,2]]' and leave the empty array in 'a' + // untouched rather than replace it with {b: {c: "new value"}}. + testInputOutput({ + input: {_id: 0, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]}, + projection: {"a.b.c": "new value"}, + expectedOutput: + {_id: 0, a: [[], [{b: [[{c: "new value"}, {c: "new value"}], {c: "new value"}]}]]} + }); +}()); + +// Test some miscellaneous properties of projections. +(function testMiscellaneousProjections() { + // Test including and excluding _id only. + testInputOutput({input: {_id: 0}, projection: {_id: 1}, expectedOutput: {_id: 0}}); + testInputOutput({input: {_id: 0}, projection: {_id: 0}, expectedOutput: {}}); + testInputOutput({ + input: {_id: 0, a: 1, b: 1}, + projection: {_id: 0}, + expectedOutput: {a: 1, b: 1}, + }); + + // Test that you can specify nested paths with dots or as sub-objects and it'll mean the + // same thing. + testInputOutput({ + input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}}, + projection: {'measurements.temperature': 1, 'measurements.pressure': 1, _id: 0}, + expectedOutput: {measurements: {temperature: 20, pressure: 0.7}}, + interestingIndexes: [{'measurements.temperature': 1, 'measurements.pressure': 1}] + }); + testInputOutput({ + input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}}, + projection: {measurements: {temperature: 1, pressure: 1}, _id: 0}, + expectedOutput: {measurements: {temperature: 20, pressure: 0.7}}, + interestingIndexes: [{'measurements.temperature': 1, 'measurements.pressure': 1}] + }); + // Now with an exclusion projection. + testInputOutput({ + input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}}, + projection: {measurements: {humidity: 0, time: 0}, _id: 0}, + expectedOutput: {measurements: {temperature: 20, pressure: 0.7}}, + }); +}()); +}()); |