diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-18 17:02:53 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-18 17:02:53 -0300 |
| commit | 959575a5ca598bf5f37fb5cebe7ed1d80d3d71f7 (patch) | |
| tree | acc8d60aedb12b70048e676e8a7349deb0010db8 /jstests/aggregation | |
| parent | 76588293975fc059cf076779e4283e6ffaf8afff (diff) | |
New upstream version 6.0.20upstream
Diffstat (limited to 'jstests/aggregation')
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; + })); |
