summaryrefslogtreecommitdiff
path: root/jstests/aggregation
diff options
context:
space:
mode:
authorLucas de Castro Borges <lucas@gnuabordo.com.br>2025-02-18 17:02:53 -0300
committerLucas de Castro Borges <lucas@gnuabordo.com.br>2025-02-18 17:02:53 -0300
commit959575a5ca598bf5f37fb5cebe7ed1d80d3d71f7 (patch)
treeacc8d60aedb12b70048e676e8a7349deb0010db8 /jstests/aggregation
parent76588293975fc059cf076779e4283e6ffaf8afff (diff)
New upstream version 6.0.20upstream
Diffstat (limited to 'jstests/aggregation')
-rw-r--r--jstests/aggregation/accumulators/top_bottom_top_n_bottom_n.js13
-rw-r--r--jstests/aggregation/bugs/hash_lookup_spill_large_and_small_documents_correctly.js3
-rw-r--r--jstests/aggregation/bugs/server95350.js34
-rw-r--r--jstests/aggregation/documents_merge.js112
-rw-r--r--jstests/aggregation/expressions/expression_set_field_null_chars.js96
-rw-r--r--jstests/aggregation/expressions/log_pow_exp.js (renamed from jstests/aggregation/bugs/server18427.js)205
-rw-r--r--jstests/aggregation/extras/utils.js13
-rw-r--r--jstests/aggregation/group_by_objectid.js36
-rw-r--r--jstests/aggregation/ifnull.js12
-rw-r--r--jstests/aggregation/sources/documents/basics.js (renamed from jstests/aggregation/sources/documents.js)0
-rw-r--r--jstests/aggregation/sources/documents/subpipeline_validation.js215
-rw-r--r--jstests/aggregation/sources/lookup/lookup_absorb_match.js104
-rw-r--r--jstests/aggregation/sources/lookup/lookup_collation.js45
-rw-r--r--jstests/aggregation/sources/lookup/lookup_foreign_collation.js121
-rw-r--r--jstests/aggregation/sources/redact/root_redact.js28
-rw-r--r--jstests/aggregation/sources/setWindowFields/comprehensive_parse.js51
-rw-r--r--jstests/aggregation/sources/setWindowFields/time.js92
-rw-r--r--jstests/aggregation/sources/unionWith/unionWith_explain.js30
-rw-r--r--jstests/aggregation/unwind.js53
-rw-r--r--jstests/aggregation/unwind_sort.js27
20 files changed, 1097 insertions, 193 deletions
diff --git a/jstests/aggregation/accumulators/top_bottom_top_n_bottom_n.js b/jstests/aggregation/accumulators/top_bottom_top_n_bottom_n.js
index d0844a4acac..2556850752a 100644
--- a/jstests/aggregation/accumulators/top_bottom_top_n_bottom_n.js
+++ b/jstests/aggregation/accumulators/top_bottom_top_n_bottom_n.js
@@ -392,4 +392,17 @@ const testOperatorText = (op) => {
// most relevant results first.
testOperatorText("$bottomN");
testOperatorText("$topN");
+
+// Test constant output and sortBy.
+assert(coll.drop());
+assert.commandWorked(coll.insertMany([{a: 1}, {a: 2}, {a: 3}]));
+const testConstantOutputAndSort = (op) => {
+ const results =
+ coll.aggregate([{$group: {_id: null, result: {[op]: {n: 3, output: "abc", sortBy: {}}}}}])
+ .toArray();
+ assert.eq(results.length, 1, results);
+ assert.docEq(results[0], {_id: null, result: ["abc", "abc", "abc"]}, results);
+};
+testConstantOutputAndSort("$topN");
+testConstantOutputAndSort("$bottomN");
})();
diff --git a/jstests/aggregation/bugs/hash_lookup_spill_large_and_small_documents_correctly.js b/jstests/aggregation/bugs/hash_lookup_spill_large_and_small_documents_correctly.js
index de8f79bbea7..9d10fd95997 100644
--- a/jstests/aggregation/bugs/hash_lookup_spill_large_and_small_documents_correctly.js
+++ b/jstests/aggregation/bugs/hash_lookup_spill_large_and_small_documents_correctly.js
@@ -1,7 +1,4 @@
// Regression test to check that different document sizes work correctly with $lookup.
-// @tags: [
-// requires_fcv_71,
-// ]
(function() {
'use strict';
diff --git a/jstests/aggregation/bugs/server95350.js b/jstests/aggregation/bugs/server95350.js
new file mode 100644
index 00000000000..b1f939cb483
--- /dev/null
+++ b/jstests/aggregation/bugs/server95350.js
@@ -0,0 +1,34 @@
+/**
+ * SERVER-95350: Fix jstests/aggregation/api_version_stage_allowance_checks.js.
+ */
+(function() {
+"use strict";
+
+load("jstests/libs/collection_drop_recreate.js"); // For assertDropAndRecreateCollection.
+
+const collName = 'test';
+assertDropAndRecreateCollection(db, collName);
+
+let docs = [];
+for (let i = 0; i < 10; ++i) {
+ docs.push({x: i, y: i, z: i});
+}
+db[collName].insertMany(docs);
+
+assert.commandWorked(db.runCommand({
+ explain: {
+ aggregate: collName,
+ pipeline: [{
+ $mergeCursors: {
+ sort: {y: 1, z: 1},
+ compareWholeSortKey: false,
+ remotes: [],
+ nss: "test.mergeCursors",
+ allowPartialResults: false,
+ }
+ }],
+ cursor: {},
+ readConcern: {},
+ },
+}));
+})();
diff --git a/jstests/aggregation/documents_merge.js b/jstests/aggregation/documents_merge.js
new file mode 100644
index 00000000000..85744f0191e
--- /dev/null
+++ b/jstests/aggregation/documents_merge.js
@@ -0,0 +1,112 @@
+/**
+ * This is the test for $documents stage along with $merge stage in an aggregation pipeline,
+ * including verifying the bug in SERVER-85892 is addressed when the spec 'whenMatched' is not
+ * empty.
+ *
+ * @tags: [
+ * assumes_against_mongod_not_mongos, # not yet supported until 7.2 with SERVER-65534
+ * ]
+ */
+
+load("jstests/aggregation/extras/merge_helpers.js"); // For withEachMergeMode and
+ // dropWithoutImplicitRecreate.
+
+const outColl = db[`${jsTest.name()}_out`];
+const outCollName = outColl.getName();
+const expectedTotalDocs = 100;
+
+function assertDocsInsertedCorrectly(docs, pipeline) {
+ const msg = `Failed with pipeline: ${JSON.stringify(pipeline, null, 2)}`;
+
+ assert.eq(expectedTotalDocs, docs.length, msg);
+ for (let i = 0; i < expectedTotalDocs; i++) {
+ assert.eq(docs[i].x, i, msg);
+ }
+}
+
+const documentsStage = {
+ $documents: {$map: {input: {$range: [0, expectedTotalDocs]}, in : {x: "$$this"}}}
+};
+
+function testFn(pipeline, assertFn) {
+ // Creates an index as $merge requires a unique index with the 'on' identifier field. Then
+ // inserts a document allowed to be matched.
+ dropWithoutImplicitRecreate(outCollName);
+ assert.commandWorked(outColl.createIndex({x: 1}, {unique: true}));
+ assert.commandWorked(outColl.insert({x: 10}));
+
+ assert.doesNotThrow(() => db.aggregate(pipeline));
+ let res = outColl.find({}, {_id: 0}).sort({x: 1}).toArray();
+ assertDocsInsertedCorrectly(res, pipeline);
+ assertFn(res);
+}
+
+{ // Tests $merge with non-empty pipeline along with let in whenMatched spec.
+ const pipeline = [
+ documentsStage,
+ {
+ $merge: {
+ into: outCollName,
+ let : {num: 123},
+ whenMatched: [{$set: {number: "$$num"}}],
+ on: "x"
+ }
+ }
+ ];
+
+ testFn(pipeline, res => {
+ assert.eq(res.filter(elem => elem.number === 123).length, 1);
+ });
+}
+
+{ // Tests $merge with non-empty pipeline in whenMatched spec.
+ const pipeline = [
+ documentsStage,
+ {$merge: {into: outCollName, whenMatched: [{$set: {new: true}}], on: "x"}}
+ ];
+
+ testFn(pipeline, res => {
+ assert.eq(res.filter(elem => elem.new === true).length, 1);
+ });
+}
+
+// Tests each combination of merge modes.
+withEachMergeMode(({whenMatchedMode, whenNotMatchedMode}) => {
+ const expectErrorCode = whenMatchedMode === "fail"
+ ? ErrorCodes.DuplicateKey
+ : whenNotMatchedMode === "fail" ? ErrorCodes.MergeStageNoMatchingDocument : null;
+
+ // Creates an index as $merge requires a unique index with the 'on' identifier field. Then
+ // inserts a document allowed to be matched.
+ dropWithoutImplicitRecreate(outCollName);
+ assert.commandWorked(outColl.createIndex({x: 1}, {unique: true}));
+ assert.commandWorked(outColl.insert({x: 10, old: true}));
+
+ const pipeline = [
+ documentsStage,
+ {
+ $merge: {
+ into: outCollName,
+ whenMatched: whenMatchedMode,
+ whenNotMatched: whenNotMatchedMode,
+ on: "x",
+ }
+ }
+ ];
+
+ if (expectErrorCode) {
+ assert.throwsWithCode(() => db.aggregate(pipeline), expectErrorCode);
+ return;
+ }
+
+ assert.doesNotThrow(() => db.aggregate(pipeline));
+ let res = outColl.find({}, {_id: 0}).sort({x: 1}).toArray();
+ if (whenNotMatchedMode == "discard") {
+ assert.eq(outColl.count(), 1);
+ } else {
+ assertDocsInsertedCorrectly(res, pipeline);
+ }
+
+ // Asserts if the old document is replaced when 'whenMatchedMode' is "replace".
+ assert.eq(res.filter(elem => elem.old === true).length, whenMatchedMode == "replace" ? 0 : 1);
+});
diff --git a/jstests/aggregation/expressions/expression_set_field_null_chars.js b/jstests/aggregation/expressions/expression_set_field_null_chars.js
new file mode 100644
index 00000000000..4232bc033ef
--- /dev/null
+++ b/jstests/aggregation/expressions/expression_set_field_null_chars.js
@@ -0,0 +1,96 @@
+/**
+ * Tests that $setField handles null chars in the 'field' argument correctly.
+ */
+(function() {
+"use strict";
+
+const coll = db[jsTestName()];
+coll.drop();
+
+assert.commandWorked(coll.insert({_id: 1, foo: "bar"}));
+
+// Asserts $setField and $unsetField with the given 'field' argument fail with one of the given
+// 'codes' when used in various commands.
+function assertSetFieldFailsWithCode({field, codes}) {
+ const setFieldExpressions = [
+ {$setField: {field, input: {}, value: true}},
+ {$unsetField: {field, input: {}}},
+ ];
+
+ for (const setFieldExpression of setFieldExpressions) {
+ const errorMsg = tojson(setFieldExpression);
+
+ assert.commandFailedWithCode(
+ db.runCommand({find: coll.getName(), projection: {field: setFieldExpression}}),
+ codes,
+ errorMsg);
+ assert.commandFailedWithCode(db.runCommand({
+ aggregate: coll.getName(),
+ pipeline: [{$project: {field: setFieldExpression}}, {$out: coll.getName() + "-2"}],
+ cursor: {}
+ }),
+ codes,
+ errorMsg);
+ assert.commandFailedWithCode(db.runCommand({
+ aggregate: coll.getName(),
+ pipeline: [{
+ $merge: {
+ into: coll.getName(),
+ whenMatched: [{$replaceWith: setFieldExpression}],
+ whenNotMatched: "discard"
+ }
+ }],
+ cursor: {}
+ }),
+ codes,
+ errorMsg);
+ assert.commandFailedWithCode(db.runCommand({
+ update: coll.getName(),
+ updates: [{q: {_id: 1}, u: [{$replaceWith: setFieldExpression}], multi: false}]
+ }),
+ codes,
+ errorMsg);
+ assert.commandFailedWithCode(db.runCommand({
+ findAndModify: coll.getName(),
+ query: {_id: 1},
+ update: [{$replaceWith: setFieldExpression}]
+ }),
+ codes,
+ errorMsg);
+ }
+}
+
+const invalidFieldNames = [
+ // Starts with null chars.
+ "\x00a",
+ // Ends with null chars.
+ "a\x00",
+ // All null chars.
+ "\x00",
+ "\x00\x00\x00",
+ // Null chars somewhere in the middle.
+ "a\x00\x01\x08a",
+ "a\x00\x02\x08b",
+ "a\x00\x01\x18b",
+ "a\x00\x01\x28c",
+ "a\x00\x01\x03d\x00\xff\xff\xff\xff\x00\x08b",
+];
+
+// Test each field name directly, as part of a field reference, wrapped in $const/$literal,
+// wrapped in a const-foldable expression as well as some combinations of these.
+for (const field of invalidFieldNames) {
+ assertSetFieldFailsWithCode({field, codes: [9534700, 9423101]});
+ assertSetFieldFailsWithCode({field: {$const: field}, codes: [9534700, 9423101]});
+ assertSetFieldFailsWithCode({field: {$literal: field}, codes: [9534700, 9423101]});
+ assertSetFieldFailsWithCode({field: "$" + field, codes: [16411, 9423101]});
+ assertSetFieldFailsWithCode({field: "$foo." + field, codes: [16411, 9423101]});
+ assertSetFieldFailsWithCode({field: "foo." + field, codes: [9534700, 9423101]});
+ assertSetFieldFailsWithCode({field: {$const: "$" + field}, codes: [9534700, 9423101]});
+ assertSetFieldFailsWithCode({field: {$const: "$foo." + field}, codes: [9534700, 9423101]});
+ // Sanity check: non-literal expressions are not allowed even when they could be const
+ // folded.
+ assertSetFieldFailsWithCode({field: {$concat: [field]}, codes: [4161106, 9423101]});
+ assertSetFieldFailsWithCode({field: {$toUpper: field}, codes: [4161106, 9423101]});
+ assertSetFieldFailsWithCode({field: {$toLower: field}, codes: [4161106, 9423101]});
+}
+})();
diff --git a/jstests/aggregation/bugs/server18427.js b/jstests/aggregation/expressions/log_pow_exp.js
index a633632ab3d..b74cf4fed93 100644
--- a/jstests/aggregation/bugs/server18427.js
+++ b/jstests/aggregation/expressions/log_pow_exp.js
@@ -10,13 +10,29 @@ var coll = db.log_exponential_expressions;
coll.drop();
assert.commandWorked(coll.insert({_id: 0, a: 8, b: 2}));
-var decimalE = NumberDecimal("2.718281828459045235360287471352662");
-var decimal1overE = NumberDecimal("0.3678794411714423215955237701614609");
+const doubleE = 2.7182818284590452;
+const decimalE = NumberDecimal("2.718281828459045235360287471352662");
+const decimal1overE = NumberDecimal("0.3678794411714423215955237701614609");
+
+// Given a double, is it an integer?
+function isInteger(n) {
+ return !n.toString().includes('.');
+}
+
+function isNumberDecimal(n) {
+ return n.toString().includes('NumberDecimal');
+}
// Helper for testing that op returns expResult.
-function testOp(op, expResult) {
- var pipeline = [{$project: {_id: 0, result: op}}];
- assert.eq(coll.aggregate(pipeline).toArray(), [{result: expResult}]);
+function testOp(op, expResult, failMsg) {
+ const pipeline = [{$project: {_id: 0, result: op}}];
+ const result = coll.aggregate(pipeline).toArray();
+ assert.eq(result.length, 1);
+ if (expResult === null || isNaN(expResult) || isNumberDecimal(expResult)) {
+ assert.eq(result[0].result, expResult, failMsg);
+ } else {
+ assert.close(result[0].result, expResult, failMsg, 12 /*places*/);
+ }
}
// $log, $log10, $ln.
@@ -26,11 +42,180 @@ function testOp(op, expResult) {
testOp({$log: [10, 10]}, 1);
testOp({$log10: [10]}, 1);
testOp({$ln: [Math.E]}, 1);
-// - NumberDecimal
-testOp({$log: [NumberDecimal("10"), NumberDecimal("10")]}, NumberDecimal("1"));
-testOp({$log10: [NumberDecimal("10")]}, NumberDecimal("1"));
-// The below answer is actually correct: the input is an approximation of E
-testOp({$ln: [decimalE]}, NumberDecimal("0.9999999999999999999999999999999998"));
+
+// Different double and NumberDecimal inputs, verified manually.
+const logTestCases = [
+ // Base 8
+ {input: 1, base: 8, doubleResult: 0, decResult: NumberDecimal("0E+33")},
+ {
+ input: 2.5,
+ base: 8,
+ doubleResult: 0.4406426982957875,
+ decResult: NumberDecimal("0.4406426982957874492901064764964633")
+ },
+ {
+ input: 7,
+ base: 8,
+ doubleResult: 0.9357849740192015,
+ decResult: NumberDecimal("0.9357849740192013691473231057439436")
+ },
+ {input: 8, base: 8, doubleResult: 1, decResult: NumberDecimal("1")},
+ {
+ input: 64,
+ base: 8,
+ doubleResult: 2,
+ decResult: NumberDecimal("2.000000000000000000000000000000000")
+ },
+ {
+ input: 65,
+ base: 8,
+ doubleResult: 2.0074559376761516,
+ decResult: NumberDecimal("2.007455937676151502755710694582028")
+ },
+
+ // Base 9, a more unusual base.
+ {input: 1, base: 9, doubleResult: 0, decResult: NumberDecimal("0E+33")},
+ {
+ input: 2.5,
+ base: 9,
+ doubleResult: 0.41702188357323483,
+ decResult: NumberDecimal("0.4170218835732348650487566466679398")
+ },
+ {
+ input: 4,
+ base: 9,
+ doubleResult: 0.6309297535714574,
+ decResult: NumberDecimal("0.6309297535714574370995271143427609")
+ },
+ {input: 9, base: 9, doubleResult: 1, decResult: NumberDecimal("1")},
+ {
+ input: 10,
+ base: 9,
+ doubleResult: 1.0479516371446924,
+ decResult: NumberDecimal("1.047951637144692302148283761010701")
+ },
+ {input: 81, base: 9, doubleResult: 2, decResult: NumberDecimal("2")},
+ {
+ input: 82,
+ base: 9,
+ doubleResult: 2.0055843597957064,
+ decResult: NumberDecimal("2.005584359795706389324272570756155")
+ },
+
+ // Base 12.77, an even MORE unusual base.
+ {input: 1, base: 12.77, doubleResult: 0, decResult: NumberDecimal("0E+33")},
+ {
+ input: 2.5,
+ base: 12.77,
+ doubleResult: 0.3597390013391846,
+ decResult: NumberDecimal("0.3597390013391846393309152124110717")
+ },
+ {input: 12.77, base: 12.77, doubleResult: 1, decResult: NumberDecimal("1")},
+ {
+ input: 13,
+ base: 12.77,
+ doubleResult: 1.0070082433896357,
+ decResult: NumberDecimal("1.007008243389635671677137269228113")
+ },
+ {
+ input: 163.0729,
+ base: 12.77,
+ doubleResult: 2,
+ decResult: NumberDecimal("2.000000000000000000000000000000000")
+ },
+ {
+ input: 170,
+ base: 12.77,
+ doubleResult: 2.016332738676606,
+ decResult: NumberDecimal("2.016332738676605709114981994718173")
+ },
+];
+for (const test of logTestCases) {
+ // If we can cast our input, base (or both) to integer types, test them as well.
+ const inputs = [test.input, NumberDecimal(test.input.toString())];
+ if (isInteger(test.input)) {
+ inputs.push(NumberInt(test.input), NumberLong(test.input));
+ }
+ const bases = [test.base, NumberDecimal(test.base.toString())];
+ if (isInteger(test.base)) {
+ bases.push(NumberInt(test.base), NumberLong(test.base));
+ }
+ for (const input of inputs) {
+ for (const base of bases) {
+ const hasDecimalInput = isNumberDecimal(input) || isNumberDecimal(base);
+ testOp(
+ {$log: [input, base]}, hasDecimalInput ? test.decResult : test.doubleResult, test);
+ }
+ }
+}
+
+// Base 10, using $log10
+const log10TestCases = [
+ {input: 1, doubleResult: 0, decResult: NumberDecimal("0")},
+ {
+ input: 2.5,
+ doubleResult: 0.3979400086720376,
+ decResult: NumberDecimal("0.3979400086720376095725222105510140")
+ },
+ {input: 10, doubleResult: 1, decResult: NumberDecimal("1")},
+ {
+ input: 11,
+ doubleResult: 1.041392685158225,
+ decResult: NumberDecimal("1.041392685158225040750199971243024")
+ },
+ {input: 100, doubleResult: 2, decResult: NumberDecimal("2")},
+ {
+ input: 101,
+ doubleResult: 2.0043213737826426,
+ decResult: NumberDecimal("2.004321373782642574275188178222938")
+ },
+];
+for (const test of log10TestCases) {
+ // If the input is an integer anyway, test with our integer types as well.
+ if (isInteger(test.input)) {
+ testOp({$log10: NumberInt(test.input)}, test.doubleResult, test);
+ testOp({$log10: NumberLong(test.input)}, test.doubleResult, test);
+ }
+ testOp({$log10: test.input}, test.doubleResult, test);
+ testOp({$log10: NumberDecimal(test.input.toString())}, test.decResult, test);
+}
+
+// Base `e`, using $ln.
+const lnTestCases = [
+ {input: 1, doubleResult: 0, decResult: NumberDecimal("0")},
+ // `e` is about 2.7, so this should be close to 1.
+ {
+ input: 2.5,
+ doubleResult: 0.9162907318741551,
+ decResult: NumberDecimal("0.9162907318741550651835272117680110")
+ },
+ {
+ input: 7,
+ doubleResult: 1.9459101490553132,
+ decResult: NumberDecimal("1.945910149055313305105352743443180")
+ },
+ {
+ input: 10,
+ doubleResult: 2.302585092994046,
+ decResult: NumberDecimal("2.302585092994045684017991454684364")
+ },
+];
+for (const test of lnTestCases) {
+ if (isInteger(test.input)) {
+ testOp({$ln: NumberInt(test.input)}, test.doubleResult, test);
+ testOp({$ln: NumberLong(test.input)}, test.doubleResult, test);
+ }
+ testOp({$ln: test.input}, test.doubleResult, test);
+ testOp({$ln: NumberDecimal(test.input.toString())}, test.decResult, test);
+}
+
+// We represent `e` differently with double and NumberDecimal, so test that here.
+testOp({$ln: doubleE}, 1);
+testOp({$ln: 1 / doubleE}, -1);
+// The below answer is actually correct: the input is an approximation of E.
+testOp({$ln: decimalE}, NumberDecimal("0.9999999999999999999999999999999998"));
+testOp({$ln: decimal1overE}, NumberDecimal("-0.9999999999999999999999999999999998"));
+
// All types converted to doubles.
testOp({$log: [NumberLong("10"), NumberLong("10")]}, 1);
testOp({$log10: [NumberLong("10")]}, 1);
diff --git a/jstests/aggregation/extras/utils.js b/jstests/aggregation/extras/utils.js
index 6face031533..febd852c253 100644
--- a/jstests/aggregation/extras/utils.js
+++ b/jstests/aggregation/extras/utils.js
@@ -342,6 +342,19 @@ function assertErrCodeAndErrMsgContains(coll, pipe, code, expectedMessage) {
}
/**
+ * Assert that an aggregation ran on admin DB fails with a specific code and the error message
+ * contains the given string. Note that 'code' can be an array of possible codes.
+ */
+function assertAdminDBErrCodeAndErrMsgContains(coll, pipe, code, expectedMessage) {
+ const response = assert.commandFailedWithCode(
+ coll.getDB().adminCommand({aggregate: 1, pipeline: pipe, cursor: {}}), code);
+ assert.neq(
+ -1,
+ response.errmsg.indexOf(expectedMessage),
+ "Error message did not contain '" + expectedMessage + "', found:\n" + tojson(response));
+}
+
+/**
* Assert that an aggregation fails with any code and the error message contains the given
* string.
*/
diff --git a/jstests/aggregation/group_by_objectid.js b/jstests/aggregation/group_by_objectid.js
new file mode 100644
index 00000000000..4d7b638b6e5
--- /dev/null
+++ b/jstests/aggregation/group_by_objectid.js
@@ -0,0 +1,36 @@
+// Tests that $group aggregation works with group key of type ObjectId.
+// @tags: [
+// # Some in memory variants will error because this test uses too much memory. As such, we do not
+// # run this test on in-memory variants.
+// requires_persistence,
+// ]
+const collName = jsTestName();
+const coll = db[collName];
+coll.drop();
+
+const bigStr = Array(100 * 1000).toString(); // ~ 100KB of ','
+const bigStr2 = bigStr + "2";
+const nDocs = 1000;
+const nGroups = 10;
+
+let objectIds = [];
+for (let i = 0; i < nGroups; i++) {
+ objectIds.push(new ObjectId());
+}
+
+const bulk = coll.initializeUnorderedBulkOp();
+for (let i = 1; i <= nDocs; i++) {
+ bulk.insert({b: objectIds[i % nGroups], bigStr: bigStr, b2: bigStr2, c: i});
+}
+assert.commandWorked(bulk.execute());
+
+const pipeline = [
+ {
+ $sort: {
+ "c": NumberInt(-1),
+ }
+ },
+ {$group: {"_id": "$b", "doc": {"$first": "$$ROOT"}}},
+];
+
+assert.commandWorked(db.runCommand({aggregate: collName, pipeline: pipeline, cursor: {}}));
diff --git a/jstests/aggregation/ifnull.js b/jstests/aggregation/ifnull.js
index f147111a259..86d4ad2f85d 100644
--- a/jstests/aggregation/ifnull.js
+++ b/jstests/aggregation/ifnull.js
@@ -15,7 +15,8 @@ assert.commandWorked(t.insertOne({
my_null: null,
my_undefined: undefined,
my_obj: {},
- my_list: []
+ my_list: [],
+ my_nested: {zero: 0, null: null, undefined: undefined}
}));
function assertError(expectedErrorCode, ifNullSpec) {
@@ -23,8 +24,12 @@ function assertError(expectedErrorCode, ifNullSpec) {
}
function assertResult(expectedResult, ifNullSpec) {
- const res = t.aggregate({$project: {_id: 0, a: {$ifNull: ifNullSpec}}}).toArray()[0];
+ let res = t.aggregate({$project: {_id: 0, a: {$ifNull: ifNullSpec}}}).toArray()[0];
assert.docEq({a: expectedResult}, res);
+ res = t.aggregate({
+ $group: {_id: 0, a: {$push: {$let: {vars: {x: {$ifNull: ifNullSpec}}, in : "$$x"}}}}
+ }).toArray()[0];
+ assert.docEq({_id: 0, a: [expectedResult]}, res);
}
// Wrong number of args.
@@ -33,6 +38,7 @@ assertError(1257300, ['$one']);
assertError(1257300, ['$my_null']);
// First arg non null.
+assertResult(0, ['$my_nested.zero', '$one']);
assertResult(1, ['$one', '$two']);
assertResult(2, ['$two', '$one']);
assertResult(false, ['$my_false', '$one']);
@@ -48,6 +54,7 @@ assertResult(1, ['$one', '$two', null]);
assertResult(2, ['$two', '$my_undefined', null]);
// First arg null.
+assertResult(0, ['$my_nested.null', '$my_nested.zero']);
assertResult(2, ['$my_null', '$two']);
assertResult(1, ['$my_null', '$one']);
assertResult(null, ['$my_null', '$my_null']);
@@ -59,6 +66,7 @@ assertResult(null, ['$my_null', '$my_null', null]);
assertResult(undefined, ['$my_null', '$my_null', undefined]);
// First arg undefined.
+assertResult(0, ['$my_nested.undefined', '$my_nested.zero']);
assertResult(2, ['$my_undefined', '$two']);
assertResult(1, ['$my_undefined', '$one']);
assertResult(null, ['$my_undefined', '$my_null']);
diff --git a/jstests/aggregation/sources/documents.js b/jstests/aggregation/sources/documents/basics.js
index ed90e9ac7aa..ed90e9ac7aa 100644
--- a/jstests/aggregation/sources/documents.js
+++ b/jstests/aggregation/sources/documents/basics.js
diff --git a/jstests/aggregation/sources/documents/subpipeline_validation.js b/jstests/aggregation/sources/documents/subpipeline_validation.js
new file mode 100644
index 00000000000..7d4ae87584f
--- /dev/null
+++ b/jstests/aggregation/sources/documents/subpipeline_validation.js
@@ -0,0 +1,215 @@
+// Tests the validation logic for combinations of "collectionless" stages like $documents with or
+// around sub-pipelines. For the cases that should be legal, we mostly just care the the command
+// succeeds. However, we will use 'resultsEq' to test correct semantics while we are here, gaining
+// more coverage.
+// TODO SERVER-94226 consider extending this test to cases like $currentOp and $queryStats as well.
+// This test uses stages like $documents which are not permitted inside a $facet stage.
+// @tags: [do_not_wrap_aggregations_in_facets]
+(function() {
+"use strict";
+load("jstests/aggregation/extras/utils.js"); // For 'resultsEq.'
+load("jstests/libs/fixture_helpers.js"); // For 'isMongos.'
+
+const coll = db[jsTestName()];
+coll.drop();
+
+const targetCollForMerge = db["target_coll"];
+targetCollForMerge.drop();
+assert.commandWorked(coll.insert({_id: 0, arr: [{}, {}]}));
+
+{
+ // Tests for an aggregation over a collection (i.e. {aggregate: "collName"} commands) with a
+ // $documents stage used in a sub-pipeline. Each of these cases should be legal, which is most
+ // of the value of the assertion. We will use 'resultsEq' to test correct semantics while we are
+ // here.
+
+ // $lookup.
+ assert(resultsEq(coll.aggregate([
+ {$lookup: {
+ let: {documents: "$arr"},
+ pipeline: [
+ {$documents: "$$documents"},
+ ],
+ as: "duplicated"
+ }},
+ ]).toArray(), [{_id: 0, arr: [{}, {}], duplicated: [{}, {}]}]));
+
+ // $unionWith.
+ assert(resultsEq(coll.aggregate([
+ {
+ $unionWith: {
+ pipeline: [
+ {$documents: [{_id: "gen"}]},
+ ],
+ }
+ },
+ ])
+ .toArray(),
+ [{_id: 0, arr: [{}, {}]}, {_id: "gen"}]));
+
+ // Both, and more nesting.
+ assert(resultsEq(coll.aggregate([{
+ $unionWith: {
+ coll: coll.getName(),
+ pipeline: [{
+ $lookup: {
+ pipeline: [
+ {$documents: []},
+ {$unionWith: {coll: coll.getName(), pipeline: []}}
+ ],
+ as: "nest"
+ }
+ }]
+ }
+ }])
+ .toArray(),
+ [
+ {_id: 0, arr: [{}, {}]},
+ {_id: 0, arr: [{}, {}], nest: [{_id: 0, arr: [{}, {}]}]}
+ ]));
+}
+
+{
+ // Tests for a db-level aggregate (i.e. {aggregate: 1} commands) with sub-pipelines on regular
+ // collections.
+
+ // $facet
+ assert(resultsEq(db.aggregate([
+ {
+ $documents: [
+ {x: 1, y: 1, val: 1},
+ {x: 2, y: 2, val: 1},
+ {x: 3, y: 1, val: 2},
+ {x: 2, y: 2, val: 1}
+ ]
+ },
+ {
+ $facet: {
+ sumByX: [{$group: {_id: "$x", sum: {$sum: "$val"}}}],
+ sumByY: [{$group: {_id: "$y", sum: {$sum: "$val"}}}]
+ }
+ }
+ ]).toArray(),
+ [{
+ sumByX: [{_id: 1, sum: 1}, {_id: 2, sum: 2}, {_id: 3, sum: 2}],
+ sumByY: [{_id: 1, sum: 3}, {_id: 2, sum: 2}]
+ }]));
+
+ if (!FixtureHelpers.isMongos(db)) {
+ // This doesn't work on mongos in v7.0 and earlier - waiting for SERVER-65534.
+
+ // $lookup.
+ assert(resultsEq(db.aggregate([
+ {$documents: [{x: 1, arr: [{x: 2}]}, {y: 1, arr: []}]},
+ {$lookup: {
+ let: {documents: "$arr"},
+ pipeline: [
+ {$documents: "$$documents"},
+ ],
+ as: "duplicated"
+ }},
+ ]).toArray(),
+ [
+ {x: 1, arr: [{x: 2}], duplicated: [{x: 2}]},
+ {y: 1, arr: [], duplicated: []}
+ ]));
+
+ // $merge.
+ assert.doesNotThrow(() => db.aggregate([
+ {
+ $documents: [
+ {_id: 2, x: "foo"},
+ {_id: 4, x: "bar"},
+ ]
+ },
+ {
+ $merge: {
+ into: targetCollForMerge.getName(),
+ on: "_id",
+ whenMatched: [{$set: {x: {$setUnion: ["$x", "$$new.x"]}}}]
+ }
+ }
+ ]));
+ assert(resultsEq(targetCollForMerge.find({}, {_id: 1}).toArray(), [{_id: 2}, {_id: 4}]));
+
+ // $unionWith
+ assert(resultsEq(db.aggregate([
+ {$documents: [{_id: 2}, {_id: 4}]},
+ {$unionWith: {coll: coll.getName(), pipeline: []}}
+ ]).toArray(),
+ [{_id: 2}, {_id: 4}, {_id: 0, arr: [{}, {}]}]));
+
+ // All of the above, plus nesting.
+ const results =
+ db.aggregate([
+ {$documents: [{_id: "first"}]},
+ {
+ $unionWith: {
+ pipeline: [
+ {$documents: [{_id: "uw"}]},
+ {$unionWith: {pipeline: [{$documents: [{_id: "uw_2"}]}]}},
+ {
+ $facet: {
+ allTogether:
+ [{$group: {_id: null, all: {$addToSet: "$_id"}}}],
+ countEach: [{$group: {_id: "$_id", count: {$sum: 1}}}],
+ }
+ },
+ {
+ $lookup:
+ {pipeline: [{$documents: [{x: "lu1"}, {x: "lu2"}]}], as: "xs"}
+ },
+ {$set: {xs: {$map: {input: "$xs", in : "$$this.x"}}}}
+ ]
+ },
+ },
+ ]).toArray();
+ assert(resultsEq(results,
+ [
+ {_id: "first"},
+ {
+ allTogether: [{_id: null, all: ["uw", "uw_2"]}],
+ countEach: [{_id: "uw", count: 1}, {_id: "uw_2", count: 1}],
+ xs: ["lu1", "lu2"]
+ }
+ ]),
+ results);
+ }
+}
+
+// Test for invalid combinations.
+
+// To use $documents inside a $lookup, there must not be a "from" argument.
+// As of SERVER-94144, this does not throw on 7.0 and older branches.
+assert.doesNotThrow(
+ () => coll.aggregate([{$lookup: {from: "foo", pipeline: [{$documents: []}], as: "lustuff"}}]));
+assert.doesNotThrow(
+ () => coll.aggregate([
+ {$lookup: {
+ from: "foo",
+ let: {docs: "$arr"},
+ pipeline: [
+ {$documents: "$$docs"},
+ {$lookup: {
+ from: "foo",
+ let: {x: "$x", y: "$y"},
+ pipeline: [
+ {$match: {$expr: {$and: [
+ {$eq: ["$x", "$$x"]},
+ {$eq: ["$y", "$$y"]}
+ ]}}}
+ ],
+ as: "doesnt_matter"
+ }}
+ ],
+ as: "lustuff"
+ }}]));
+
+// To use $documents inside a $unionWith, there must not be a "coll" argument.
+// As of SERVER-94144, this does not throw on 7.0 and older branches.
+assert.doesNotThrow(
+ () => coll.aggregate([{$unionWith: {coll: "foo", pipeline: [{$documents: []}]}}]));
+
+// Cannot use $documents inside of $facet.
+assert.throwsWithCode(() => coll.aggregate([{$facet: {test: [{$documents: []}]}}]), 40600);
+})();
diff --git a/jstests/aggregation/sources/lookup/lookup_absorb_match.js b/jstests/aggregation/sources/lookup/lookup_absorb_match.js
index be589615f5c..4220cbcb71a 100644
--- a/jstests/aggregation/sources/lookup/lookup_absorb_match.js
+++ b/jstests/aggregation/sources/lookup/lookup_absorb_match.js
@@ -136,7 +136,7 @@ expected =
[{_id: "dog", locationId: "doghouse", location: {_id: "doghouse", coordinates: [25.0, 60.0]}}];
assert.eq(result, expected);
-// Test that a $match with $jsonSchema works as expected although ineligable for absorbtion by a
+// Test that a $match with $jsonSchema works as expected although ineligible for absorbtion by a
// $lookup.
result = testDB.animals
.aggregate([
@@ -167,7 +167,7 @@ expected =
[{_id: "bull", locationId: "bullpen", location: {_id: "bullpen", coordinates: [-25.0, -60.0]}}];
assert.eq(result, expected);
-// Test that a more complex $match with $jsonSchema works as expected although ineligable for
+// Test that a more complex $match with $jsonSchema works as expected although ineligible for
// absorbtion by a $lookup.
result = testDB.animals
.aggregate([
@@ -194,8 +194,100 @@ expected =
[{_id: "bull", locationId: "bullpen", location: {_id: "bullpen", coordinates: [-25.0, -60.0]}}];
assert.eq(result, expected);
-// Test that a $match with $alwaysTrue works as expected although ineligable for absorbtion by a
-// $lookup.
+// Test that $match with a $jsonSchema property that will internally translate to a match
+// expression node that has a path that is prefixed by the 'as' field in the lookup and that has
+// children that can operate on that path (in this case, $_internalSchemaAllElemMatchFromIndex)
+// works as expected although ineligible for absorbtion by a $lookup. Note that the jsonSchema below
+// ensures that all elements of 'location.coordinates' are above 0 since the 'items' field is an
+// object.
+result = testDB.animals
+ .aggregate([
+ {
+ $lookup: {
+ from: "locations",
+ localField: "locationId",
+ foreignField: "_id",
+ as: "location"
+ }
+ },
+ {$unwind: "$location"},
+ {
+ $match: {
+ $jsonSchema: {
+ properties: {"location.coordinates": {items: {minimum: 0}}}
+ }
+ }
+ },
+ {$project: {"location.extra": false, "colors": false}}
+ ])
+ .toArray();
+
+expected =
+ [{_id: "dog", locationId: "doghouse", location: {_id: "doghouse", coordinates: [25.0, 60.0]}}];
+assert.eq(result, expected);
+
+// Test that $match with a $jsonSchema property that will internally translate to a match
+// expression node that has a path that is prefixed by the 'as' field in the lookup and that has
+// children that can operate on that path (in this case, $_internalSchemaMatchArrayIndex) works as
+// expected although ineligible for absorbtion by a $lookup. Note that the jsonSchema below ensures
+// that the first element of 'location.coordinates' is above 0 since the 'items' field is an array.
+result = testDB.animals
+ .aggregate([
+ {
+ $lookup: {
+ from: "locations",
+ localField: "locationId",
+ foreignField: "_id",
+ as: "location"
+ }
+ },
+ {$unwind: "$location"},
+ {
+ $match: {
+ $jsonSchema: {
+ properties: {"location.coordinates": {items: [{minimum: 0}]}}
+ }
+ }
+ },
+ {$project: {"location.extra": false, "colors": false}}
+ ])
+ .toArray();
+
+expected =
+ [{_id: "dog", locationId: "doghouse", location: {_id: "doghouse", coordinates: [25.0, 60.0]}}];
+assert.eq(result, expected);
+
+// Test that $match with a $jsonSchema property that will internally translate to a match
+// expression node that has a path that is prefixed by the 'as' field in the lookup and that has
+// children that can operate that path (in this case, $_internalSchemaObjectMatch) works as expected
+// although ineligible for absorbtion by a $lookup.
+result = testDB.animals
+ .aggregate([
+ {
+ $lookup: {
+ from: "locations",
+ localField: "locationId",
+ foreignField: "_id",
+ as: "location"
+ }
+ },
+ {$unwind: "$location"},
+ {
+ $match: {
+ $jsonSchema: {
+ properties: {"location.extra": {type: 'object', properties: {"breeds": {type: 'string'}}}}
+ }
+ }
+ },
+ {$project: {"location.extra": false, "colors": false}}
+ ])
+ .toArray();
+expected =
+ [{_id: "bull", locationId: "bullpen", location: {_id: "bullpen", coordinates: [-25.0, -60.0]}}];
+assert.eq(result, expected);
+
+// Test that a $match with $alwaysTrue works as expected although ineligible for absorbtion
+// by a $lookup. The $sort is to guarantee records are returned in the expected order.
result = testDB.animals
.aggregate([
{
@@ -220,7 +312,7 @@ expected = [
];
assert.eq(result, expected);
-// Test that a $match with $alwaysFalse works as expected although ineligable for absorbtion by a
+// Test that a $match with $alwaysFalse works as expected although ineligible for absorbtion by a
// $lookup.
result = testDB.animals
.aggregate([
@@ -242,7 +334,7 @@ result = testDB.animals
expected = [];
assert.eq(result, expected);
-// Test that a $match with $expr works as expected although ineligable for absorbtion by a $lookup.
+// Test that a $match with $expr works as expected although ineligible for absorbtion by a $lookup.
result = testDB.animals
.aggregate([
{
diff --git a/jstests/aggregation/sources/lookup/lookup_collation.js b/jstests/aggregation/sources/lookup/lookup_collation.js
index 50ec057ae79..36f18b7e44c 100644
--- a/jstests/aggregation/sources/lookup/lookup_collation.js
+++ b/jstests/aggregation/sources/lookup/lookup_collation.js
@@ -3,14 +3,11 @@
* when performing comparisons on a foreign collection with a different default collation. Exercises
* the fix for SERVER-43350.
*
- * Collation can be set at three different levels for $lookup stage
+ * Collation can be set at two different levels for $lookup stage
* 1. on the local collection (collation on the foreign collection is always ignored)
- * 2. on the $lookup stage via '_internalCollation' property
- * 3. on the aggregation command via 'collation' property in options
+ * 2. on the aggregation command via 'collation' property in options
*
- * The three settings have the following precedence:
- * 1. '_internalCollation' overrides all others
- * 2. 'collation' option overrides local collection's collation
+ * The 'collation' command option overrides local collection's collation.
*/
load("jstests/aggregation/extras/utils.js"); // For anyEq.
load("jstests/libs/analyze_plan.js"); // For getAggPlanStages, getWinningPlan.
@@ -137,29 +134,6 @@ let explain;
}
})();
-// Collation set on $lookup stage with '_internalCollation' should override collation of the local
-// collection and on the command.
-(function testStageCollationPrecedence() {
- for (let lookupInto of [lookupWithPipeline, lookupNoPipeline]) {
- let lookupStage = lookupInto(collAa);
- lookupStage.$lookup._internalCollation = caseInsensitive;
- results = collAa.aggregate([lookupStage], {collation: caseSensitive}).toArray();
- assertArrayEq({
- actual: results,
- expected: resultCaseInsensitive,
- extraErrorMsg: " Case-insensitive collation on stage, running: " + tojson(lookupInto)
- });
-
- lookupStage.$lookup._internalCollation = caseSensitive;
- results = collAA.aggregate([lookupStage], {collation: caseInsensitive}).toArray();
- assertArrayEq({
- actual: results,
- expected: resultCaseSensistive,
- extraErrorMsg: " Case-sensitive collation on stage, running: " + tojson(lookupInto)
- });
- }
-})();
-
// In presense of indexes lookup might choose a different strategy for the join, that relies on the
// index (INLJ). It should respect the effective collation of $lookup.
(function testCollationWithIndexes() {
@@ -213,19 +187,6 @@ let explain;
explain = collAa.explain().aggregate([lookupInto(collAa_indexed)],
{collation: {locale: "fr"}, allowDiskUse: false});
assertNestedLoopJoinStrategy(explain);
-
- // Stage-level collation overrides collection-level and command-level collations.
- let lookupStage = lookupInto(collAa_indexed);
- lookupStage.$lookup._internalCollation = caseInsensitive;
- results = collAa.aggregate([lookupStage], {collation: caseSensitive}).toArray();
- assertArrayEq({
- actual: results,
- expected: resultCaseInsensitive,
- extraErrorMsg: " Case-insensitive collation on stage, foreign is indexed, running: " +
- tojson(lookupInto)
- });
- explain = collAa.explain().aggregate([lookupStage], {collation: caseSensitive});
- assertIndexJoinStrategy(explain);
}
})();
})();
diff --git a/jstests/aggregation/sources/lookup/lookup_foreign_collation.js b/jstests/aggregation/sources/lookup/lookup_foreign_collation.js
index 6937fe42854..7d6e9a4578e 100644
--- a/jstests/aggregation/sources/lookup/lookup_foreign_collation.js
+++ b/jstests/aggregation/sources/lookup/lookup_foreign_collation.js
@@ -96,12 +96,10 @@ function setup() {
// localColl: Local Collection
// foreignColl: Foreign Collection
// commandCollation: Collation set on the aggregate command. Pass null for default collation.
- // lookupCollation: Collation specified in the $lookup stage. Pass null for default
- // collation. expectedResults: Results expected from the aggregate invocation
+ // expectedResults: Results expected from the aggregate invocation
//
- function assertExpectedResultSet(
- localColl, foreignColl, commandCollation, lookupCollation, expectedResults) {
- const lookupWithPipeline = {$lookup: {from: foreignColl.getName(),
+ function assertExpectedResultSet(localColl, foreignColl, commandCollation, expectedResults) {
+ const lookupWithPipeline = {$lookup: {from: foreignColl.getName(),
as: "foreignMatch",
let: {l_id: "$_id"},
pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}}]}};
@@ -111,11 +109,6 @@ function setup() {
foreignField: "_id",
as: "foreignMatch"}};
- if (lookupCollation) {
- lookupWithPipeline.$lookup._internalCollation = lookupCollation;
- lookupWithLocalForeignField.$lookup._internalCollation = lookupCollation;
- }
-
const aggOptions = {};
if (commandCollation) {
aggOptions.collation = commandCollation;
@@ -129,112 +122,20 @@ function setup() {
}
// Baseline test, confirming simple binary comparison when no collation has been specified on
- // the command, $lookup stage or collections.
- assertExpectedResultSet(localColl, foreignColl, null, null, resultSetCaseSensitive);
+ // the command or the collection.
+ assertExpectedResultSet(localColl, foreignColl, null, resultSetCaseSensitive);
// When a collation has been specified on the $lookup stage, it will always be used to join
// local and foreign collections.
for (const local of [localColl, localCaseInsensitiveColl]) {
for (const foreign of [foreignColl, foreignCaseInsensitiveColl]) {
- for (const command of [null, simpleCollation, caseInsensitiveCollation]) {
- // Case insensitive collation specified in the $lookup stage results in a case
- // insensitive join.
- assertExpectedResultSet(
- local, foreign, command, caseInsensitiveCollation, resultSetCaseInsensitive);
-
- // Simple collation specified in the $lookup stage results in a case sensitive join.
- assertExpectedResultSet(
- local, foreign, command, simpleCollation, resultSetCaseSensitive);
- }
+ // Case insensitive collation results in a case insensitive join.
+ assertExpectedResultSet(
+ local, foreign, caseInsensitiveCollation, resultSetCaseInsensitive);
+
+ // Simple collation results in a case sensitive join.
+ assertExpectedResultSet(local, foreign, simpleCollation, resultSetCaseSensitive);
}
}
})();
-
-(function testNestedLookupStagesWithDifferentCollations() {
- setup();
-
- const lookupWithPipeline = {$lookup: {from: foreignColl.getName(),
- as: "foreignMatch",
- let: {l_id: "$_id"},
- pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}},
- {$lookup: {from: localColl.getName(),
- as: "foreignMatch2",
- let: {l_id: "$_id"},
- pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}}],
- _internalCollation: simpleCollation}}],
- _internalCollation: caseInsensitiveCollation}};
-
- const resultSet = [
- {_id: "a", foreignMatch: [{_id: "a", "foreignMatch2": [{"_id": "a"}]}]},
- {_id: "b", foreignMatch: [{_id: "B", "foreignMatch2": []}]},
- {_id: "c", foreignMatch: [{_id: "c", "foreignMatch2": [{"_id": "c"}]}]},
- {_id: "d", foreignMatch: [{_id: "D", "foreignMatch2": []}]},
- {_id: "e", foreignMatch: [{_id: "e", "foreignMatch2": [{"_id": "e"}]}]}
- ];
-
- const results = localColl.aggregate([lookupWithPipeline]).toArray();
- assert(anyEq(results, resultSet), tojson(results));
-})();
-
-(function testMatchOnUnwoundAsFieldAbsorptionOptimization() {
- setup();
-
- // A $lookup stage with a collation that differs from the collection and command collation
- // will not absorb a $match on unwound results.
- let pipeline = [{$lookup: {from: foreignColl.getName(),
- as: "foreignMatch",
- let: {l_id: "$_id"},
- pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}}],
- _internalCollation: caseInsensitiveCollation}},
- {$unwind: "$foreignMatch"},
- {$match: {"foreignMatch._id": "b"}}];
-
- let results = localColl.aggregate(pipeline).toArray();
- assert.eq(0, results.length);
-
- let explain = localColl.explain().aggregate(pipeline);
- let lastStage = explain.stages[explain.stages.length - 1];
- assert(lastStage.hasOwnProperty("$match"), tojson(explain));
- assert.eq({$match: {"foreignMatch._id": {$eq: "b"}}},
- lastStage,
- "The $match stage should not be optimized into the $lookup stage" + tojson(explain));
-
- // A $lookup stage with a collation that matches the command collation will absorb a $match
- // stage.
- pipeline = [{$lookup: {from: foreignColl.getName(),
- as: "foreignMatch",
- let: {l_id: "$_id"},
- pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}}],
- _internalCollation: caseInsensitiveCollation}},
- {$unwind: "$foreignMatch"},
- {$match: {"foreignMatch._id": "b"}}];
-
- let expectedResults = [{"_id": "b", "foreignMatch": {"_id": "B"}}];
-
- results = localColl.aggregate(pipeline, {collation: caseInsensitiveCollation}).toArray();
- assert(anyEq(results, expectedResults), tojson(results));
-
- explain = localColl.explain().aggregate(pipeline, {collation: caseInsensitiveCollation});
- lastStage = explain.stages[explain.stages.length - 1];
- assert(lastStage.hasOwnProperty("$lookup"), tojson(explain));
-
- // A $lookup stage with a collation that matches the local collection collation will absorb
- // a $match stage.
- pipeline = [{$lookup: {from: foreignColl.getName(),
- as: "foreignMatch",
- let: {l_id: "$_id"},
- pipeline: [{$match: {$expr: {$eq: ["$_id", "$$l_id"]}}}],
- _internalCollation: caseInsensitiveCollation}},
- {$unwind: "$foreignMatch"},
- {$match: {"foreignMatch._id": "b"}}];
-
- expectedResults = [{"_id": "b", "foreignMatch": {"_id": "B"}}];
-
- results = localCaseInsensitiveColl.aggregate(pipeline).toArray();
- assert(anyEq(results, expectedResults), tojson(results));
-
- explain = localCaseInsensitiveColl.explain().aggregate(pipeline);
- lastStage = explain.stages[explain.stages.length - 1];
- assert(lastStage.hasOwnProperty("$lookup"), tojson(explain));
-})();
})();
diff --git a/jstests/aggregation/sources/redact/root_redact.js b/jstests/aggregation/sources/redact/root_redact.js
new file mode 100644
index 00000000000..a1851e54726
--- /dev/null
+++ b/jstests/aggregation/sources/redact/root_redact.js
@@ -0,0 +1,28 @@
+// Usage of $$ROOT variable can lead to Document cache reallocation and possible memory corruption.
+// Test is checking that this is not the case.
+(function() {
+"use strict";
+
+const bigStr = 'X'.repeat(1024);
+const doc = {
+ _id: 0,
+ d: true,
+ sub: {d: false, str: bigStr},
+ str: bigStr,
+};
+
+db.test.drop();
+assert.commandWorked(db.test.insertOne(doc));
+
+const pipeline = [{
+ $redact: {
+ $cond: {
+ if: "$d",
+ then: "$$DESCEND",
+ else: {$cond: {if: {$objectToArray: "$$ROOT"}, then: "$$KEEP", else: "$$PRUNE"}}
+ }
+ }
+}];
+
+assert.eq([doc], db.test.aggregate(pipeline).toArray());
+})();
diff --git a/jstests/aggregation/sources/setWindowFields/comprehensive_parse.js b/jstests/aggregation/sources/setWindowFields/comprehensive_parse.js
index dbe509e2c59..4dd4cc90565 100644
--- a/jstests/aggregation/sources/setWindowFields/comprehensive_parse.js
+++ b/jstests/aggregation/sources/setWindowFields/comprehensive_parse.js
@@ -65,6 +65,7 @@ const windows = {
// The list of sort definitions to test.
const sortBys = {
none: null,
+ expr: {partitionSeq: {$meta: "randVal"}},
asc: {partitionSeq: 1},
desc: {partitionSeq: -1},
asc_date: {date: 1},
@@ -104,18 +105,11 @@ function constructQuery(wf, window, sortBy, partitionBy) {
}
// Given an element of each of the lists above, what is the expected
-// result. The output should be 'SKIP', 'OK' or the expected integer
+// result. The output should be 'OK' or the expected integer
// error code.
function expectedResult(wfType, windowType, sortType, partitionType) {
// Static errors all come first.
- // Skip range windows over dates or that are over descending windows.
- if (windowType.endsWith('range')) {
- if (sortType.endsWith('date') || sortType.startsWith('desc')) {
- return 'SKIP';
- }
- }
-
// Derivative and integral require an ascending sort
// and an explicit window.
if (wfType.startsWith('derivative')) {
@@ -129,6 +123,11 @@ function expectedResult(wfType, windowType, sortType, partitionType) {
return ErrorCodes.FailedToParse;
}
+ // '$derivative requires a non-expression sortBy'.
+ if (sortType == "expr") {
+ return ErrorCodes.FailedToParse;
+ }
+
} else if (wfType.startsWith('integral')) {
// Integral requires a sort.
if (sortType == 'none') {
@@ -140,6 +139,11 @@ function expectedResult(wfType, windowType, sortType, partitionType) {
return ErrorCodes.FailedToParse;
}
+ // '$integral requires a non-expression sortBy'.
+ if (sortType == "expr") {
+ return ErrorCodes.FailedToParse;
+ }
+
} else if (wfType.startsWith('expMovingAvg')) {
// $expMovingAvg doesn't accept a window.
if (windowType != 'none') {
@@ -179,9 +183,23 @@ function expectedResult(wfType, windowType, sortType, partitionType) {
}
// Range based windows require a sort over a single field.
- if (windowType.endsWith('range') && (sortType == 'none' || sortType == 'multi')) {
- // 'Range-based window require sortBy a single field'.
- return 5339902;
+ if (windowType.endsWith('range')) {
+ if (sortType == 'none' || sortType == 'multi') {
+ // 'Range-based window require sortBy a single field'.
+ return 5339902;
+ }
+ if (sortType == 'expr') {
+ // 'Range-based bounds require a non-expression sortBy'
+ return 8947400;
+ }
+ if (sortType.startsWith('desc')) {
+ // 'Range-based bounds require an ascending sortBy'.
+ return 8947401;
+ }
+ if (sortType.endsWith('date') && !partitionType.endsWith('array')) {
+ // 'For windows that involve date or time ranges, a unit must be provided.'
+ return 5429413;
+ }
}
if (partitionType === 'static_array') {
@@ -238,10 +256,6 @@ function* makeTests() {
expectedResult: expectedResult(wfType, windowType, sortType, partitionType)
};
- if (test.expectedResult == 'SKIP') {
- continue;
- }
-
yield test;
}
}
@@ -251,13 +265,16 @@ function* makeTests() {
// Run all the combinations generated in makeTests.
for (const test of makeTests()) {
+ const errorMsg = "Command was: " + tojson(test.query);
if (test.expectedResult == ErrorCodes.OK) {
assert.commandWorked(
- coll.runCommand({aggregate: coll.getName(), pipeline: [test.query], cursor: {}}));
+ coll.runCommand({aggregate: coll.getName(), pipeline: [test.query], cursor: {}}),
+ errorMsg);
} else {
assert.commandFailedWithCode(
coll.runCommand({aggregate: coll.getName(), pipeline: [test.query], cursor: {}}),
- test.expectedResult);
+ test.expectedResult,
+ errorMsg);
}
}
})();
diff --git a/jstests/aggregation/sources/setWindowFields/time.js b/jstests/aggregation/sources/setWindowFields/time.js
index e4f7d7cfa6c..7ef72c7243b 100644
--- a/jstests/aggregation/sources/setWindowFields/time.js
+++ b/jstests/aggregation/sources/setWindowFields/time.js
@@ -196,4 +196,96 @@ error = assert.throws(() => {
run([range('unbounded', 'unbounded')]);
});
assert.commandFailedWithCode(error, 5429513);
+
+// Test with small bound over large data values
+coll.drop();
+assert.commandWorked(coll.insert([
+ {_id: 0, time: new Date("2023-07-01T20:53:50.932Z"), "str": "ric"},
+ {_id: 1, time: new Date("2023-07-02T20:57:42.383Z"), "str": "alc"},
+ {_id: 2, time: new Date("2023-07-04T01:39:14.265Z"), "str": ""},
+ {_id: 3, time: new Date("2023-07-04T04:01:29.983Z"), "str": "ric"},
+ {_id: 4, time: new Date("2023-07-05T15:08:22.541Z"), "str": ""},
+ {_id: 5, time: new Date("2023-07-05T22:56:30.949Z"), "str": ""},
+]));
+
+const res = coll.aggregate([
+ {
+ $setWindowFields: {
+ partitionBy: "$str",
+ sortBy: {"any": -1},
+ output: {"num": {$sum: {$pow: [9, 20]}}}
+ }
+ },
+ {
+ $setWindowFields: {
+ sortBy: {"num": 1},
+ output: {"res": {$max: "$num", window: {range: [-4, "current"]}}}
+ }
+ },
+ {$project: {res: 1, _id: 0}},
+ ])
+ .toArray();
+
+assert.sameMembers(res, [
+ {"res": 12157665459056929000},
+ {"res": 24315330918113858000},
+ {"res": 24315330918113858000},
+ {"res": 36472996377170790000},
+ {"res": 36472996377170790000},
+ {"res": 36472996377170790000}
+]);
+
+// Test with large bound over large data values
+const res2 =
+ coll.aggregate([
+ {
+ $setWindowFields: {
+ partitionBy: "$str",
+ sortBy: {"any": -1},
+ output: {"num": {$sum: {$pow: [8, 20]}}}
+ }
+ },
+ {
+ $setWindowFields: {
+ sortBy: {"num": 1},
+ output:
+ {"res": {$max: "$num", window: {range: [-{$pow: [9, 20]}, "current"]}}}
+ }
+ },
+ {$project: {res: 1, _id: 0}},
+ ])
+ .toArray();
+
+assert.sameMembers(res2, [
+ {"res": 1152921504606847000},
+ {"res": 2305843009213694000},
+ {"res": 2305843009213694000},
+ {"res": 3458764513820541000},
+ {"res": 3458764513820541000},
+ {"res": 3458764513820541000}
+]);
+
+// Test with large bound over small data values
+const res3 =
+ coll.aggregate([
+ {
+ $setWindowFields: {
+ partitionBy: "$str",
+ sortBy: {"any": -1},
+ output: {"num": {$sum: {$pow: [2, 2]}}}
+ }
+ },
+ {
+ $setWindowFields: {
+ sortBy: {"num": 1},
+ output:
+ {"res": {$max: "$num", window: {range: [-{$pow: [9, 20]}, "current"]}}}
+ }
+ },
+ {$project: {res: 1, _id: 0}},
+ ])
+ .toArray();
+
+assert.sameMembers(res3,
+ [{"res": 4}, {"res": 8}, {"res": 8}, {"res": 12}, {"res": 12}, {"res": 12}]);
})();
diff --git a/jstests/aggregation/sources/unionWith/unionWith_explain.js b/jstests/aggregation/sources/unionWith/unionWith_explain.js
index 60d6a7ae4a4..a600b7fbe0f 100644
--- a/jstests/aggregation/sources/unionWith/unionWith_explain.js
+++ b/jstests/aggregation/sources/unionWith/unionWith_explain.js
@@ -105,8 +105,18 @@ function assertExplainEq(union, regular) {
executionStatsIngoredFields),
buildErrorString(unionStats, regularStats, "executionStages"));
} else if ("stages" in regular) {
- assert(arrayEqWithIgnoredFields(union, regular.stages, stagesIgnoredFields),
- buildErrorString(union, regular, "stages"));
+ // For explains run with the runCommand({explain: ...}) format.
+ if (regular.stages.length > 1 && "$cursor" in regular.stages[0] &&
+ "executionStats" in regular.stages[0]["$cursor"]) {
+ assert(
+ arrayEqWithIgnoredFields(union,
+ regular.stages,
+ [...stagesIgnoredFields, ...executionStatsIngoredFields]),
+ buildErrorString(union, regular, "stages with executionStats"));
+ } else {
+ assert(arrayEqWithIgnoredFields(union, regular.stages, stagesIgnoredFields),
+ buildErrorString(union, regular, "stages"));
+ }
} else if ("queryPlanner" in regular) {
assert.eq(union.length, 1, "Expected single union stage");
const unionCursor = union[0].$cursor;
@@ -123,7 +133,7 @@ function assertExplainEq(union, regular) {
function assertExplainMatch(unionExplain, regularExplain) {
const unionStage = getUnionWithStage(unionExplain);
- assert(unionStage);
+ assert(unionStage, unionExplain);
const unionSubExplain = unionStage.$unionWith.pipeline;
assertExplainEq(unionSubExplain, regularExplain);
}
@@ -133,6 +143,20 @@ function testPipeline(pipeline) {
{explain: true});
let queryResult = collB.aggregate(pipeline, {explain: true});
assertExplainMatch(unionResult, queryResult);
+
+ // Alternative explain invocation. This is a regression test for SERVER-89344.
+ if (!FixtureHelpers.isMongos(db)) {
+ unionResult = db.runCommand({
+ explain: {
+ "aggregate": collA.getName(),
+ "pipeline": [{$unionWith: {coll: collB.getName(), pipeline: pipeline}}],
+ "cursor": {}
+ }
+ });
+ queryResult = db.runCommand(
+ {explain: {"aggregate": collB.getName(), "pipeline": pipeline, "cursor": {}}});
+ assertExplainMatch(unionResult, queryResult);
+ }
}
testPipeline([{$addFields: {bump: true}}]);
diff --git a/jstests/aggregation/unwind.js b/jstests/aggregation/unwind.js
index ffd2a3da9c6..cf01560cc0c 100644
--- a/jstests/aggregation/unwind.js
+++ b/jstests/aggregation/unwind.js
@@ -15,3 +15,56 @@ assert.eq(4, res.length);
assert.eq([1, 2, 3, 4], res.map(function(z) {
return z.x;
}));
+
+// Test unwinding an empty path
+coll = db.agg_unwind_empty_path;
+coll.drop();
+coll.insert({_id: 1});
+coll.insert({_id: 2, "": [1, 2]});
+coll.insert({_id: 3, "": {"": [3, 4, 5]}});
+
+assert.throwsWithCode(() => coll.aggregate([{$unwind: {path: "$"}}]).toArray(), 40352);
+assert.throwsWithCode(() => coll.aggregate([{$unwind: {path: "$."}}]).toArray(), 40353);
+
+// Test includeArrayIndex
+coll = db.agg_unwind_with_index;
+coll.drop();
+coll.insert({_id: 1});
+coll.insert({_id: 2, array: [1, 2]});
+coll.insert({_id: 3, obj: {array: [3, 4, 5]}});
+coll.insert({_id: 4, obj: {subObj: {array: [6, 7, 8, 9]}}});
+
+assert.eq(
+ coll.aggregate([{$unwind: {path: "$array", includeArrayIndex: "idx"}}]).toArray(),
+ [{"_id": 2, "array": 1, "idx": NumberLong(0)}, {"_id": 2, "array": 2, "idx": NumberLong(1)}]);
+
+assert.eq(coll.aggregate([{$unwind: {path: "$array", includeArrayIndex: "array"}}]).toArray(),
+ [{"_id": 2, "array": NumberLong(0)}, {"_id": 2, "array": NumberLong(1)}]);
+
+assert.eq(coll.aggregate([{$unwind: {path: "$obj.array", includeArrayIndex: "idx"}}]).toArray(), [
+ {"_id": 3, "obj": {"array": 3}, "idx": NumberLong(0)},
+ {"_id": 3, "obj": {"array": 4}, "idx": NumberLong(1)},
+ {"_id": 3, "obj": {"array": 5}, "idx": NumberLong(2)}
+]);
+
+assert.eq(coll.aggregate([{$unwind: {path: "$obj.array", includeArrayIndex: "obj"}}]).toArray(), [
+ {"_id": 3, "obj": NumberLong(0)},
+ {"_id": 3, "obj": NumberLong(1)},
+ {"_id": 3, "obj": NumberLong(2)}
+]);
+
+assert.eq(
+ coll.aggregate([{$unwind: {path: "$obj.array", includeArrayIndex: "obj.array"}}]).toArray(), [
+ {"_id": 3, "obj": {"array": NumberLong(0)}},
+ {"_id": 3, "obj": {"array": NumberLong(1)}},
+ {"_id": 3, "obj": {"array": NumberLong(2)}}
+ ]);
+
+assert.eq(coll.aggregate([{$unwind: {path: "$obj.subObj.array", includeArrayIndex: "obj.subObj"}}])
+ .toArray(),
+ [
+ {"_id": 4, "obj": {"subObj": NumberLong(0)}},
+ {"_id": 4, "obj": {"subObj": NumberLong(1)}},
+ {"_id": 4, "obj": {"subObj": NumberLong(2)}},
+ {"_id": 4, "obj": {"subObj": NumberLong(3)}}
+ ]);
diff --git a/jstests/aggregation/unwind_sort.js b/jstests/aggregation/unwind_sort.js
new file mode 100644
index 00000000000..858320bec0d
--- /dev/null
+++ b/jstests/aggregation/unwind_sort.js
@@ -0,0 +1,27 @@
+// Test that we can sort on fields, produces by unwind
+
+const coll = db.agg_unwind_sort;
+coll.drop();
+assert.commandWorked(coll.insertOne({a: [3, 4, 5]}));
+assert.commandWorked(coll.insertOne({a: [1, 2]}));
+
+let result = coll.aggregate([{$unwind: "$a"}, {$sort: {a: 1}}]).toArray();
+assert.eq([1, 2, 3, 4, 5], result.map(function(z) {
+ return z.a;
+}));
+result = coll.aggregate([{$unwind: "$a"}, {$sort: {a: -1}}]).toArray();
+assert.eq([5, 4, 3, 2, 1], result.map(function(z) {
+ return z.a;
+}));
+result =
+ coll.aggregate([{$unwind: {path: "$a", includeArrayIndex: "i"}}, {$sort: {i: 1}}]).toArray();
+assert.eq([NumberLong(0), NumberLong(0), NumberLong(1), NumberLong(1), NumberLong(2)],
+ result.map(function(z) {
+ return z.i;
+ }));
+result =
+ coll.aggregate([{$unwind: {path: "$a", includeArrayIndex: "i"}}, {$sort: {i: -1}}]).toArray();
+assert.eq([NumberLong(2), NumberLong(1), NumberLong(1), NumberLong(0), NumberLong(0)],
+ result.map(function(z) {
+ return z.i;
+ }));