diff options
| author | Chi-I Huang <8468853+chiihuang@users.noreply.github.com> | 2024-07-30 14:13:52 -0700 |
|---|---|---|
| committer | MongoDB Bot <mongo-bot@mongodb.com> | 2024-07-30 22:19:16 +0000 |
| commit | 8ee59a3af161ded4ff28c3215ec60d82f07aa6dd (patch) | |
| tree | 13e5889a50c06a5ac7b58c43b941ea3f2140813c | |
| parent | 606aba44e6c413e5abfea213ef38c2d8043cd9c5 (diff) | |
SERVER-85892 Fix incorrect output namespace in $merge spec (#19846) (#24833)
GitOrigin-RevId: 9f1eb0f38887e04f692e191be116c8035480df58
| -rw-r--r-- | jstests/aggregation/documents_merge.js | 113 | ||||
| -rw-r--r-- | src/mongo/db/pipeline/document_source_merge.cpp | 2 |
2 files changed, 114 insertions, 1 deletions
diff --git a/jstests/aggregation/documents_merge.js b/jstests/aggregation/documents_merge.js new file mode 100644 index 00000000000..99e9a8f1a41 --- /dev/null +++ b/jstests/aggregation/documents_merge.js @@ -0,0 +1,113 @@ +/** + * 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: [ + * requires_fcv_80, + * 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/src/mongo/db/pipeline/document_source_merge.cpp b/src/mongo/db/pipeline/document_source_merge.cpp index 33be917de08..c46cfa8c641 100644 --- a/src/mongo/db/pipeline/document_source_merge.cpp +++ b/src/mongo/db/pipeline/document_source_merge.cpp @@ -624,7 +624,7 @@ Value DocumentSourceMerge::serialize(const SerializationOptions& opts) const { if (!_pipeline.has_value()) { return boost::none; } - auto expCtxWithLetVariables = pExpCtx->copyWith(pExpCtx->ns); + auto expCtxWithLetVariables = pExpCtx->copyWith(getOutputNs()); if (spec.getLet()) { BSONObjBuilder cleanLetSpecBuilder; for (auto&& [name, expr] : *_letVariables) { |
