783 lines
32 KiB
JavaScript
783 lines
32 KiB
JavaScript
/**
|
|
* Tests the behavior of projection for dotted paths, including edge cases where the path only
|
|
* sometimes exists.
|
|
*
|
|
* @tags: [
|
|
* # We could potentially need to resume an index build in the event of a stepdown, which is not
|
|
* # yet implemented.
|
|
* does_not_support_stepdowns,
|
|
* not_allowed_with_signed_security_token,
|
|
* featureFlagSbeFull,
|
|
* # Time series collections do not support indexing array values in measurement fields.
|
|
* exclude_from_timeseries_crud_passthrough,
|
|
* ]
|
|
*/
|
|
|
|
const coll = db.projection_semantics;
|
|
coll.drop();
|
|
|
|
let globalIdCounter = 0;
|
|
|
|
// 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 = []}) {
|
|
if (!input.hasOwnProperty("_id")) {
|
|
input = Object.merge({_id: ++globalIdCounter}, input);
|
|
}
|
|
assert.commandWorked(coll.insert(input));
|
|
assert.docEq(
|
|
expectedOutput,
|
|
coll.find({_id: input._id}, projection).limit(1).hint({$natural: 1}).toArray()[0],
|
|
() => tojson(coll.find({_id: input._id}, projection).limit(1).hint({$natural: 1}).explain()),
|
|
);
|
|
|
|
for (let indexSpec of interestingIndexes) {
|
|
assert.commandWorkedOrFailedWithCode(coll.createIndex(indexSpec), ErrorCodes.IndexAlreadyExists);
|
|
assert.docEq(expectedOutput, coll.find({_id: input._id}, projection).hint(indexSpec).toArray()[0], () =>
|
|
tojson(coll.find({_id: input._id}, projection).hint(indexSpec).explain("executionStats")),
|
|
);
|
|
}
|
|
|
|
assert.docEq(
|
|
expectedOutput,
|
|
coll.aggregate([{$match: {_id: input._id}}, {$project: projection}], {hint: {$natural: 1}}).toArray()[0],
|
|
);
|
|
}
|
|
|
|
// 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: ++globalIdCounter, a: "demo", b: "other", x: "extra"}, {_id: globalIdCounter, a: "demo"});
|
|
testIncludeA(
|
|
{_id: ++globalIdCounter, a: "demo", aWSuffix: "other", "a.b": "extra"},
|
|
{_id: globalIdCounter, a: "demo"},
|
|
);
|
|
testIncludeA({_id: ++globalIdCounter, a: null, b: "other", x: "extra"}, {_id: globalIdCounter, 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: ++globalIdCounter, b: "other", x: "extra"},
|
|
projection: {a: 1},
|
|
expectedOutput: {_id: globalIdCounter},
|
|
interestingIndexes: [],
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter},
|
|
projection: {a: 1},
|
|
expectedOutput: {_id: globalIdCounter},
|
|
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: ++globalIdCounter, a: null});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: undefined});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: {}});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: []});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: {x: 1, b: "scalar"}});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: "scalar"});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: {b: {}}});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: [null]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: ["scalar"]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: [[]]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: [{}]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: [1, {}, 2]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, a: [[1, 2], [{}], 2]});
|
|
testIdentityInclusionA({_id: ++globalIdCounter, 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: ++globalIdCounter, a: "demo", b: "other", x: "extra"}, {a: "demo"});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, a: "demo", aWSuffix: "other", "a.b": "extra"}, {a: "demo"});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, 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: ++globalIdCounter, b: "other", x: "extra"},
|
|
projection: {a: 1, _id: 0},
|
|
expectedOutput: {},
|
|
interestingIndexes: [],
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter},
|
|
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: ++globalIdCounter, a: null}, {a: null});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, a: {}}, {a: {}});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, a: []}, {a: []});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, a: {x: 1, b: "scalar"}}, {a: {x: 1, b: "scalar"}});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, a: {b: {}}}, {a: {b: {}}});
|
|
testIncludeOnlyA({_id: ++globalIdCounter, 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: ++globalIdCounter, a: {b: "demo", y: "extra"}, x: "extra"},
|
|
{_id: globalIdCounter, a: {b: "demo"}},
|
|
);
|
|
// Test that (at least for now) the dotted path doesn't work great here.
|
|
testIncludeADotB({_id: ++globalIdCounter, a: {b: "demo"}, "a.b": "extra"}, {_id: globalIdCounter, a: {b: "demo"}});
|
|
// Test that '_id' within a sub-document is excluded.
|
|
testIncludeADotB({_id: ++globalIdCounter, a: {b: "demo", _id: "extra"}}, {_id: globalIdCounter, a: {b: "demo"}});
|
|
|
|
// Test array use case.
|
|
testIncludeADotB(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
a: [
|
|
{b: 1, c: 1},
|
|
{b: 2, c: 2},
|
|
],
|
|
x: "extra",
|
|
},
|
|
{_id: globalIdCounter, 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: ++globalIdCounter, a: [{b: 1, c: 1}, {c: 2}], x: "extra"},
|
|
{_id: globalIdCounter, 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: ++globalIdCounter, b: "other", x: "extra"}, {_id: globalIdCounter});
|
|
testADotBNoIndexes({_id: ++globalIdCounter}, {_id: globalIdCounter});
|
|
testADotBNoIndexes({_id: ++globalIdCounter, a: {}}, {_id: globalIdCounter, a: {}});
|
|
testADotBNoIndexes({_id: ++globalIdCounter, a: "scalar"}, {_id: globalIdCounter});
|
|
|
|
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: ++globalIdCounter, a: {b: "demo", y: "extra"}, x: "extra"}, {a: {b: "demo"}});
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: {b: "demo"}, "a.b": "extra"}, {a: {b: "demo"}});
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: {b: "demo", _id: "extra"}}, {a: {b: "demo"}});
|
|
|
|
testIncludeOnlyADotB(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
a: [
|
|
{b: 1, c: 1},
|
|
{b: 2, c: 2},
|
|
],
|
|
x: "extra",
|
|
},
|
|
{a: [{b: 1}, {b: 2}]},
|
|
);
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, 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: ++globalIdCounter, a: [], x: "extra"}, {a: []});
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: [{}, {}], x: "extra"}, {a: [{}, {}]});
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: ["scalar", "scalar"], x: "extra"}, {a: []});
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: [null]}, {a: []});
|
|
// This is an interesting case: the scalars are ignored but the shadow documents are preserved.
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: ["scalar", {}, "scalar", {c: 1}], x: "extra"}, {a: [{}, {}]});
|
|
// Further interest: the array within the array is preserved.
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[]]}, {a: [[]]});
|
|
// But not the scalar elements of it.
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[1, 2, 3]]}, {a: [[]]});
|
|
// But if there's a "b" again we see that.
|
|
testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[1, {b: 1}, {b: 2, c: 2}, "scalar"]]}, {a: [[{b: 1}, {b: 2}]]});
|
|
testIncludeOnlyADotB(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
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: ++globalIdCounter, a: [[], [[], [], [1], [{c: 1}]], {b: 1}]},
|
|
{
|
|
a: [[], [[], [], [], [{}]], {b: 1}],
|
|
},
|
|
);
|
|
|
|
// Test the case where two paths in a projection go through the same parent object.
|
|
const testIncludeOnlyADotBAndADotC = (input, output) =>
|
|
testInputOutput({
|
|
input: input,
|
|
projection: {"a.b": 1, "a.c": 1, _id: 0},
|
|
expectedOutput: output,
|
|
interestingIndexes: [{a: 1}, {"a.b": 1}, {"a.c": 1}, {"a.b": 1, "a.c": 1}, {"a.b": 1, "a.c": -1}],
|
|
});
|
|
testIncludeOnlyADotBAndADotC(
|
|
{_id: ++globalIdCounter, a: {b: "scalar", c: "scalar", d: "extra"}},
|
|
{a: {b: "scalar", c: "scalar"}},
|
|
);
|
|
testIncludeOnlyADotBAndADotC(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
a: [
|
|
{b: 1, c: 2, d: 3},
|
|
{b: 4, c: 5, d: 6},
|
|
],
|
|
},
|
|
{
|
|
a: [
|
|
{b: 1, c: 2},
|
|
{b: 4, c: 5},
|
|
],
|
|
},
|
|
);
|
|
|
|
// Array cases where one or both of the paths don't exist.
|
|
testIncludeOnlyADotBAndADotC(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
a: [
|
|
{b: 1, c: 2},
|
|
{b: 3, d: 4},
|
|
],
|
|
},
|
|
{a: [{b: 1, c: 2}, {b: 3}]},
|
|
);
|
|
testIncludeOnlyADotBAndADotC(
|
|
{
|
|
_id: ++globalIdCounter,
|
|
a: [
|
|
{c: 1, d: 2},
|
|
{b: 3, d: 4},
|
|
],
|
|
},
|
|
{a: [{c: 1}, {b: 3}]},
|
|
);
|
|
testIncludeOnlyADotBAndADotC({_id: ++globalIdCounter, a: []}, {a: []});
|
|
testIncludeOnlyADotBAndADotC(
|
|
{_id: ++globalIdCounter, a: [{b: 1, c: 2}, "extra", {b: 3, c: 4}]},
|
|
{
|
|
a: [
|
|
{b: 1, c: 2},
|
|
{b: 3, c: 4},
|
|
],
|
|
},
|
|
);
|
|
|
|
// Non-array cases where one or both of the paths don't exist.
|
|
//
|
|
// 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" in test cases.
|
|
const testIncludeADotBAndCNoIndexes = (input, output) =>
|
|
testInputOutput({
|
|
input: input,
|
|
projection: {"a.b": 1, "a.c": 1, _id: 0},
|
|
expectedOutput: output,
|
|
interestingIndexes: [],
|
|
});
|
|
|
|
testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {b: "scalar", d: "extra"}}, {a: {b: "scalar"}});
|
|
testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {c: "scalar", d: "extra"}}, {a: {c: "scalar"}});
|
|
testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {d: "extra"}}, {a: {}});
|
|
})();
|
|
|
|
(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: ++globalIdCounter, a: 1, b: 1, c: 1},
|
|
projection: {a: 0},
|
|
expectedOutput: {_id: globalIdCounter, b: 1, c: 1},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
|
|
projection: {a: 0, b: 0},
|
|
expectedOutput: {_id: globalIdCounter, c: 1},
|
|
});
|
|
|
|
// Test some dotted examples.
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}},
|
|
projection: {"a.b": 0},
|
|
expectedOutput: {_id: globalIdCounter, 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: ++globalIdCounter, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": 0},
|
|
expectedOutput: {_id: globalIdCounter, a: ["scalar", {c: 1, d: 1}, {c: 2}, {}]},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": 0, "a.c": 0},
|
|
expectedOutput: {_id: globalIdCounter, a: [{d: 1}, {}, {}]},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": 0, "a.c": 0},
|
|
expectedOutput: {_id: globalIdCounter, a: [{d: 1}, {}, {}]},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: []},
|
|
projection: {"a.b": 0},
|
|
expectedOutput: {_id: globalIdCounter, a: []},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]},
|
|
projection: {"a.b.c": 0},
|
|
expectedOutput: {_id: globalIdCounter, 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: ++globalIdCounter, a: 1, b: 1, c: 1},
|
|
projection: {a: {$literal: 0}},
|
|
expectedOutput: {_id: globalIdCounter, a: 0},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
|
|
projection: {a: {$literal: 0}, b: "string"},
|
|
expectedOutput: {_id: globalIdCounter, a: 0, b: "string"},
|
|
});
|
|
|
|
// Test some dotted examples.
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}},
|
|
projection: {"a.b": "new value"},
|
|
expectedOutput: {_id: globalIdCounter, a: {b: "new value"}},
|
|
});
|
|
// One notable difference for computed projections is that they overwrite scalars rather than
|
|
// ignoring or removing them.
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": "new value"},
|
|
expectedOutput: {
|
|
_id: globalIdCounter,
|
|
a: [{b: "new value"}, {b: "new value"}, {b: "new value"}, {b: "new value"}],
|
|
},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": "new value", "a.c": "new C"},
|
|
expectedOutput: {
|
|
_id: globalIdCounter,
|
|
a: [
|
|
{b: "new value", c: "new C"},
|
|
{b: "new value", c: "new C"},
|
|
{b: "new value", c: "new C"},
|
|
],
|
|
},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
|
|
projection: {"a.b": "new value", "a.c": "new C"},
|
|
expectedOutput: {
|
|
_id: globalIdCounter,
|
|
a: [
|
|
{b: "new value", c: "new C"},
|
|
{b: "new value", c: "new C"},
|
|
{b: "new value", c: "new C"},
|
|
],
|
|
},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, a: []},
|
|
projection: {"a.b": "new value"},
|
|
expectedOutput: {_id: globalIdCounter, 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: ++globalIdCounter, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]},
|
|
projection: {"a.b.c": "new value"},
|
|
expectedOutput: {
|
|
_id: globalIdCounter,
|
|
a: [[], [{b: [[{c: "new value"}, {c: "new value"}], {c: "new value"}]}]],
|
|
},
|
|
});
|
|
})();
|
|
|
|
// Test that a numeric path component never describes an array index if used in a projection. We
|
|
// have the $slice operator for that functionality.
|
|
(function testNumericPathComponentsTreatedOnlyAsObjectKeyNames() {
|
|
const kProjection = {_id: 0, "a.2": 1};
|
|
const kIndexes = [{a: 1}];
|
|
testInputOutput({
|
|
input: {a: {"2": 99}, b: 1},
|
|
projection: kProjection,
|
|
expectedOutput: {a: {"2": 99}},
|
|
interestingIndexes: kIndexes,
|
|
});
|
|
|
|
testInputOutput({
|
|
input: {a: [0, 1, 2, 3, 4]},
|
|
projection: kProjection,
|
|
expectedOutput: {a: []},
|
|
interestingIndexes: kIndexes,
|
|
});
|
|
|
|
testInputOutput({
|
|
input: {a: [{x: 1}, {y: 1}, {z: 1}]},
|
|
projection: kProjection,
|
|
expectedOutput: {a: [{}, {}, {}]},
|
|
interestingIndexes: kIndexes,
|
|
});
|
|
|
|
testInputOutput({
|
|
input: {a: [0, 1, 2, 3, 4, {obj: 123}, {"2": 123}]},
|
|
projection: kProjection,
|
|
expectedOutput: {a: [{}, {"2": 123}]},
|
|
interestingIndexes: kIndexes,
|
|
});
|
|
})();
|
|
|
|
// Test some miscellaneous properties of projections.
|
|
(function testMiscellaneousProjections() {
|
|
// Test including and excluding _id only.
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter},
|
|
projection: {_id: 1},
|
|
expectedOutput: {_id: globalIdCounter},
|
|
interestingIndexes: [{_id: 1}],
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter},
|
|
projection: {_id: 0},
|
|
expectedOutput: {},
|
|
});
|
|
testInputOutput({
|
|
input: {_id: ++globalIdCounter, 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}},
|
|
});
|
|
})();
|