257 lines
9.1 KiB
JavaScript
257 lines
9.1 KiB
JavaScript
/**
|
|
* Ensure that $replaceRoot followed by $match returns the correct results after the optimization
|
|
* that swaps the two stages in SERVER-88220.
|
|
*/
|
|
|
|
import {arrayEq} from "jstests/aggregation/extras/utils.js";
|
|
|
|
const coll = db.replace_root_match;
|
|
coll.drop();
|
|
|
|
const nestedSubdocXHasSameTypeDiffValDoc = {
|
|
x: 1,
|
|
subDocument: {x: 6.98, y: 1, subDocument: {x: 1}},
|
|
};
|
|
const nestedSubdocXHasSameTypeSameValDoc = {
|
|
_id: 2, // We will later assert that this document is returned and we want a predictable '_id'
|
|
// for this one.
|
|
x: 2,
|
|
subDocument: {x: 2, y: 2, subDocument: {x: 2}},
|
|
};
|
|
const nestedSubdocXHasDiffTypeDoc = {
|
|
x: 3,
|
|
subDocument: {x: "big", y: "small", subDocument: {x: 3}},
|
|
};
|
|
const nestedSubdocXIsNestedDoc = {
|
|
x: 5,
|
|
subDocument: {x: "small", y: 5, subDocument: {x: {a: 2}}},
|
|
};
|
|
const subdocAIsObjectWithNumericStrField = {
|
|
subDocument: {a: {"0": 2}},
|
|
};
|
|
const subdocAIsArrayWithTwo = {
|
|
subDocument: {a: [2, 3]},
|
|
};
|
|
const subdocAIsArrayWithoutTwo = {
|
|
subDocument: {a: [1, 0]},
|
|
};
|
|
const subdocAIsArrayWithObject = {
|
|
subDocument: {a: [{b: 2}]},
|
|
};
|
|
const subdocAIsObject = {
|
|
subDocument: {a: {b: 3}},
|
|
};
|
|
const subdocAIsAnotherObject = {
|
|
subDocument: {a: {c: 4}},
|
|
};
|
|
const subdocAIsArrayOfObjectsWithNumericStrFields = {
|
|
subDocument: {a: [{"0": 4}, {"0": 2}]},
|
|
};
|
|
const subdocAIsArrayOfObjects = {
|
|
subDocument: {a: [{b: 2}, {b: 2}]},
|
|
};
|
|
const subdocAIsObjectWithEmptyArray = {
|
|
subDocument: {a: {b: []}},
|
|
};
|
|
const subdocIsDollarDottedPathWithNumericStrField = {
|
|
subDocument: {"$a.0": 2},
|
|
};
|
|
const subdocIsDottedPath = {
|
|
subDocument: {"a.b": 4},
|
|
};
|
|
const subdocIsArray = {
|
|
subDocument: [{a: 4}, {a: 5}],
|
|
};
|
|
const subdocsHaveDiffNames = {
|
|
x: 5,
|
|
subDocumentA: {x: "small", y: 5, subDocumentB: {x: {a: 2}}},
|
|
};
|
|
|
|
const docs = [
|
|
nestedSubdocXHasSameTypeDiffValDoc,
|
|
nestedSubdocXHasSameTypeSameValDoc,
|
|
nestedSubdocXHasDiffTypeDoc,
|
|
nestedSubdocXIsNestedDoc,
|
|
subdocAIsObjectWithNumericStrField,
|
|
subdocAIsArrayWithTwo,
|
|
subdocAIsArrayWithoutTwo,
|
|
subdocAIsArrayWithObject,
|
|
subdocAIsObject,
|
|
subdocAIsAnotherObject,
|
|
subdocAIsArrayOfObjectsWithNumericStrFields,
|
|
subdocAIsArrayOfObjects,
|
|
subdocAIsObjectWithEmptyArray,
|
|
subdocIsDollarDottedPathWithNumericStrField,
|
|
subdocIsDottedPath,
|
|
];
|
|
assert.commandWorked(coll.insert(docs));
|
|
|
|
const runTest = (pipeline, expected) => {
|
|
const actual = coll.aggregate(pipeline).toArray();
|
|
assert(arrayEq(expected, actual), {expected, actual});
|
|
};
|
|
|
|
// Same as runTest, but checks that the actual value is equal to the value of
|
|
// expected["subDocument"].
|
|
const runTestWithSubdocExpectedResult = (pipeline, expected) => {
|
|
expected.forEach((el, index) => {
|
|
expected[index] = el["subDocument"];
|
|
});
|
|
const actual = coll.aggregate(pipeline).toArray();
|
|
assert(arrayEq(expected, actual), {expected, actual});
|
|
};
|
|
|
|
const assertFail = (pipeline) => {
|
|
assert.throwsWithCode(() => coll.aggregate(pipeline), [8105800, 40228]);
|
|
};
|
|
|
|
{
|
|
// Simple filter on subField of subDocument - $replaceRoot
|
|
const pipeline = [{$replaceRoot: {newRoot: "$subDocument"}}, {$match: {x: 2}}];
|
|
const expected = [nestedSubdocXHasSameTypeSameValDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// Simple filter on subField of subDocument - $replaceWith
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {x: 6.98}}];
|
|
const expected = [nestedSubdocXHasSameTypeDiffValDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// Composite filter on subField of subDocument - $replaceWith
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {$or: [{x: "big"}, {x: "small"}]}}];
|
|
const expected = [nestedSubdocXHasDiffTypeDoc, nestedSubdocXIsNestedDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// Multiple matches after replaceWith
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {x: "small"}}, {$match: {y: 5}}];
|
|
const expected = [nestedSubdocXIsNestedDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// Multiple replaceWiths before match should fail because <replacementDocument> resolves to a
|
|
// missing document for some documents in the collection.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$replaceWith: "$subDocument"}, {$match: {"x.a": 2}}];
|
|
assertFail(pipeline);
|
|
}
|
|
{
|
|
// replaceWith and match refer to predicates with same name.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {"subDocument.x": 2}}];
|
|
const expected = [nestedSubdocXHasSameTypeSameValDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// replaceWith followed by match with $expr is correctly processed.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {$expr: {$eq: ["$x", 2]}}}];
|
|
const expected = [nestedSubdocXHasSameTypeSameValDoc];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// replaceWith on $$ROOT is correctly processed.
|
|
const pipeline = [{$replaceWith: "$$ROOT"}, {$match: {x: 2}}];
|
|
const expected = [nestedSubdocXHasSameTypeSameValDoc];
|
|
runTest(pipeline, expected);
|
|
}
|
|
{
|
|
// First stage matches on documents that contain the field "subDocument.subDocument.x", followed
|
|
// by multiple replaceWiths, followed by match.
|
|
const pipeline = [
|
|
{$match: {"subDocument.subDocument.x": {$exists: true}}},
|
|
{$replaceWith: "$subDocument"},
|
|
{$replaceWith: "$subDocument"},
|
|
{$match: {"x.a": 2}},
|
|
];
|
|
const expected = [nestedSubdocXIsNestedDoc["subDocument"]["subDocument"]];
|
|
runTest(pipeline, expected);
|
|
}
|
|
|
|
/**
|
|
* Edge cases of MQL semantics
|
|
*/
|
|
|
|
// $match on "a.0" contains implicit array traversal semantics, matching:
|
|
// 1) an object with field "a" and subfield "0"
|
|
// 2) an object with field "a" that contains an array value, whose value at index 0 is 2
|
|
// 3) an object with field "a" that contains an array of objects, containing an object with
|
|
// field "0" of value 2
|
|
{
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {"a.0": 2}}];
|
|
const expected = [
|
|
subdocAIsObjectWithNumericStrField,
|
|
subdocAIsArrayWithTwo,
|
|
subdocAIsArrayOfObjectsWithNumericStrFields,
|
|
];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// $match with $expr on values greater than "$a.b" will match on objects whose projected value
|
|
// on "$a.b" is greater than 2 in the MQL sort order. Note that documents with field "a" and an
|
|
// array value will match because $expr must return scalar values, and will evaluate {$project:
|
|
// "$a.b"} to an empty array or another array value. The scalar result [ ] is compared to 2.
|
|
// Also, note that a literal field "a.b" will not match.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {$expr: {$gt: ["$a.b", 2]}}}];
|
|
const expected = [
|
|
subdocAIsArrayWithTwo,
|
|
subdocAIsArrayWithoutTwo,
|
|
subdocAIsArrayWithObject,
|
|
subdocAIsArrayOfObjectsWithNumericStrFields,
|
|
subdocAIsArrayOfObjects,
|
|
subdocAIsObject,
|
|
subdocAIsObjectWithEmptyArray,
|
|
];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// match on $expr with $literal will match on a field that contains '$' in its name.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {$expr: {$eq: [{$getField: {$literal: "$a.0"}}, 2]}}}];
|
|
const expected = [subdocIsDollarDottedPathWithNumericStrField];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
{
|
|
// match on $expr with $getField will match on a dotted field path.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {$expr: {$gt: [{$getField: "a.b"}, 2]}}}];
|
|
const expected = [subdocIsDottedPath];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
|
|
assert.commandWorked(coll.insert(subdocIsArray));
|
|
{
|
|
// subDocument is not an object.
|
|
const pipeline = [{$replaceWith: "$subDocument"}, {$match: {a: 3}}];
|
|
assertFail(pipeline);
|
|
}
|
|
{
|
|
// First stage matches on a subDocument that is not an object. Note that {$type: "object"} will
|
|
// sometimes match on documents with array values at "field".
|
|
const pipeline = [{$match: {subDocument: {$type: "object"}}}, {$replaceWith: "$subDocument"}, {$match: {a: 3}}];
|
|
assertFail(pipeline);
|
|
}
|
|
{
|
|
// First stage matches on a subDocument that is not an object or array, followed by replaceWith,
|
|
// followed by match.
|
|
const pipeline = [
|
|
{
|
|
$match: {$and: [{subDocument: {$type: "object"}}, {subDocument: {$not: {$type: "array"}}}]},
|
|
},
|
|
{$replaceWith: "$subDocument"},
|
|
{$match: {a: 3}},
|
|
];
|
|
const expected = [subdocAIsArrayWithTwo];
|
|
runTestWithSubdocExpectedResult(pipeline, expected);
|
|
}
|
|
|
|
assert.commandWorked(coll.insert(subdocsHaveDiffNames));
|
|
{
|
|
// First stage matches on documents that contain the field "subDocumentA.subDocumentB.x",
|
|
// followed by multiple replaceWith stages, followed by match.
|
|
const pipeline = [
|
|
{$match: {"subDocumentA.subDocumentB.x": {$exists: true}}},
|
|
{$replaceWith: "$subDocumentA"},
|
|
{$replaceWith: "$subDocumentB"},
|
|
{$match: {"x.a": 2}},
|
|
];
|
|
const expected = [subdocsHaveDiffNames["subDocumentA"]["subDocumentB"]];
|
|
runTest(pipeline, expected);
|
|
}
|