// Tests the behaviour of the $merge stage with whenMatched=keepExisting and whenNotMatched=insert. // // Cannot implicitly shard accessed collections because a collection can be implictly created and // exists when none is expected. (function() { "use strict"; load("jstests/aggregation/extras/merge_helpers.js"); // For dropWithoutImplicitRecreate. load("jstests/aggregation/extras/utils.js"); // For assertArrayEq. load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers.isMongos. const source = db[`${jsTest.name()}_source`]; source.drop(); const target = db[`${jsTest.name()}_target`]; target.drop(); const mergeStage = { $merge: {into: target.getName(), whenMatched: "keepExisting", whenNotMatched: "insert"} }; const pipeline = [mergeStage]; // Test $merge into a non-existent collection. (function testMergeIntoNonExistentCollection() { assert.commandWorked(source.insert({_id: 1, a: 1, b: "a"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, b: "a"}, ] }); })(); // Test $merge into an existing collection. (function testMergeIntoExistentCollection() { assert.commandWorked(source.insert({_id: 2, a: 2, b: "b"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}] }); })(); // Test $merge does not update documents in the target collection if they were not modified // in the source collection. (function testMergeDoesNotUpdateUnmodifiedDocuments() { assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}] }); })(); // Test $merge doesn't update documents in the target collection if they were modified in the // source collection. (function testMergeDoesNotUpdateModifiedDocuments() { // Update and merge a single document. assert.commandWorked(source.update({_id: 2}, {a: 22, c: "c"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}] }); // Update and merge multiple documents. assert.commandWorked(source.update({_id: 1}, {a: 11})); assert.commandWorked(source.update({_id: 2}, {a: 22, c: "c", d: "d"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}] }); })(); // Test $merge inserts a new document into the target collection if it was inserted into the // source collection. (function testMergeInsertsNewDocument() { // Insert and merge a single document. assert.commandWorked(source.insert({_id: 3, a: 3, b: "c"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, {_id: 3, a: 3, b: "c"}] }); assert.commandWorked(source.deleteOne({_id: 3})); assert.commandWorked(target.deleteOne({_id: 3})); // Insert and merge multiple documents. assert.commandWorked(source.insert({_id: 3, a: 3, b: "c"})); assert.commandWorked(source.insert({_id: 4, a: 4, c: "d"})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, {_id: 3, a: 3, b: "c"}, {_id: 4, a: 4, c: "d"} ] }); assert.commandWorked(source.deleteMany({_id: {$in: [3, 4]}})); assert.commandWorked(target.deleteMany({_id: {$in: [3, 4]}})); })(); // Test $merge doesn't modify the target collection if a document has been removed from the // source collection. (function testMergeDoesNotUpdateDeletedDocument() { assert.commandWorked(source.deleteOne({_id: 1})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, ] }); })(); // Test $merge fails if a unique index constraint in the target collection is violated. (function testMergeFailsIfTargetUniqueKeyIsViolated() { if (FixtureHelpers.isSharded(source)) { // Skip this test if the collection sharded, because an implicitly created sharded // key of {_id: 1} will not be covered by a unique index created in this test, which // is not allowed. return; } dropWithoutImplicitRecreate(source.getName()); assert.commandWorked(source.insert({_id: 4, a: 1})); assert.commandWorked(target.createIndex({a: 1}, {unique: true})); const error = assert.throws(() => source.aggregate(pipeline)); assert.commandFailedWithCode(error, ErrorCodes.DuplicateKey); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, ] }); assert.commandWorked(target.dropIndex({a: 1})); })(); // Test $merge fails if it cannot find an index to verify that the 'on' fields will be unique. (function testMergeFailsIfOnFieldCannotBeVerifiedForUniquness() { // The 'on' fields contains a single document field. let error = assert.throws( () => source.aggregate([{$merge: Object.assign({on: "nonexistent"}, mergeStage.$merge)}])); assert.commandFailedWithCode(error, [51190, 51183]); // The 'on' fields contains multiple document fields. error = assert.throws( () => source.aggregate( [{$merge: Object.assign({on: ["nonexistent1", "nonexistent2"]}, mergeStage.$merge)}])); assert.commandFailedWithCode(error, [51190, 51183]); })(); // Test $merge with an explicit 'on' field over a single or multiple document fields which // differ from the _id field. (function testMergeWithOnFields() { if (FixtureHelpers.isSharded(source)) { // Skip this test if the collection sharded, because an implicitly created sharded // key of {_id: 1} will not be covered by a unique index created in this test, which // is not allowed. return; } // The 'on' fields contains a single document field. dropWithoutImplicitRecreate(source.getName()); dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(source.createIndex({a: 1}, {unique: true})); assert.commandWorked(target.createIndex({a: 1}, {unique: true})); assert.commandWorked( source.insert([{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, {_id: 3, a: 30, b: "c"}])); assert.commandWorked( target.insert([{_id: 1, a: 1, c: "x"}, {_id: 4, a: 30, c: "y"}, {_id: 5, a: 40, c: "z"}])); assert.doesNotThrow( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: "a"}, mergeStage.$merge)}])); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, c: "x"}, {_id: 2, a: 2, b: "b"}, {_id: 4, a: 30, c: "y"}, {_id: 5, a: 40, c: "z"} ], fieldsToSkip: "_id" }); // The 'on' fields contains multiple document fields. dropWithoutImplicitRecreate(source.getName()); dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(source.createIndex({a: 1, b: 1}, {unique: true})); assert.commandWorked(target.createIndex({a: 1, b: 1}, {unique: true})); assert.commandWorked(source.insert( [{_id: 1, a: 1, b: "a", c: "x"}, {_id: 2, a: 2, b: "b"}, {_id: 3, a: 30, b: "c"}])); assert.commandWorked(target.insert( [{_id: 1, a: 1, b: "a"}, {_id: 4, a: 30, b: "c", c: "y"}, {_id: 5, a: 40, c: "z"}])); assert.doesNotThrow( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: ["a", "b"]}, mergeStage.$merge)}])); assertArrayEq({ actual: target.find().toArray(), expected: [ {_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}, {_id: 4, a: 30, b: "c", c: "y"}, {_id: 5, a: 40, c: "z"} ], fieldsToSkip: "_id" }); assert.commandWorked(source.dropIndex({a: 1, b: 1})); assert.commandWorked(target.dropIndex({a: 1, b: 1})); })(); // Test $merge with a dotted path in the 'on' field. (function testMergeWithDottedOnField() { if (FixtureHelpers.isSharded(source)) { // Skip this test if the collection sharded, because an implicitly created sharded // key of {_id: 1} will not be covered by a unique index created in this test, which // is not allowed. return; } dropWithoutImplicitRecreate(source.getName()); dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(source.createIndex({"a.b": 1}, {unique: true})); assert.commandWorked(target.createIndex({"a.b": 1}, {unique: true})); assert.commandWorked(source.insert([ {_id: 1, a: {b: "b"}, c: "x"}, {_id: 2, a: {b: "c"}, c: "y"}, {_id: 3, a: {b: 30}, b: "c"} ])); assert.commandWorked(target.insert({_id: 2, a: {b: "c"}})); assert.doesNotThrow( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: "a.b"}, mergeStage.$merge)}])); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: {b: "b"}, c: "x"}, {_id: 2, a: {b: "c"}}, {_id: 3, a: {b: 30}, b: "c"}], fieldsToSkip: "_id" }); })(); // Test $merge fails if the value of the 'on' field in a document is invalid, e.g. missing, // null or an array. (function testMergeFailsIfOnFieldIsInvalid() { if (FixtureHelpers.isSharded(source)) { // Skip this test if the collection sharded, because an implicitly created sharded // key of {_id: 1} will not be covered by a unique index created in this test, which // is not allowed. return; } dropWithoutImplicitRecreate(source.getName()); dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(source.createIndex({"z": 1}, {unique: true})); assert.commandWorked(target.createIndex({"z": 1}, {unique: true})); // The 'on' field is missing. assert.commandWorked(source.insert({_id: 1})); let error = assert.throws( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: "z"}, mergeStage.$merge)}])); assert.commandFailedWithCode(error, 51132); // The 'on' field is null. assert.commandWorked(source.update({_id: 1}, {z: null})); error = assert.throws( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: "z"}, mergeStage.$merge)}])); assert.commandFailedWithCode(error, 51132); // The 'on' field is an array. assert.commandWorked(source.update({_id: 1}, {z: [1, 2]})); error = assert.throws( () => source.aggregate( [{$project: {_id: 0}}, {$merge: Object.assign({on: "z"}, mergeStage.$merge)}])); assert.commandFailedWithCode(error, 51185); })(); // Test $merge when the _id field is removed from the aggregate projection but is used in the // $merge's 'on' field. (function testMergeWhenDocIdIsRemovedFromProjection() { // The _id is a single 'on' field (a default one). dropWithoutImplicitRecreate(source.getName()); assert(target.drop()); assert.commandWorked(source.insert([{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}])); assert.commandWorked(target.insert({_id: 1, b: "c"})); assert.doesNotThrow(() => source.aggregate([{$project: {_id: 0}}, mergeStage])); assertArrayEq({ // Remove the _id field from the projection as the arrayEq function cannot ignore // mismatches in the ObjectId. The target collection should contain all elements from // the source and the target even though they had the same _id's and would have been // merged should we not remove the _id field from the aggregate projection. actual: target.find({}, {_id: 0}).toArray(), expected: [{b: "c"}, {a: 1, b: "a"}, {a: 2, b: "b"}] }); // The _id is part of the compound 'on' field. dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(target.insert({_id: 1, b: "c"})); assert.commandWorked(source.createIndex({_id: 1, a: -1}, {unique: true})); assert.commandWorked(target.createIndex({_id: 1, a: -1}, {unique: true})); assert.doesNotThrow(() => source.aggregate([ {$project: {_id: 0}}, {$merge: Object.assign({on: ["_id", "a"]}, mergeStage.$merge)} ])); assertArrayEq({ // Remove the _id field from the projection as the arrayEq function cannot ignore // mismatches in the ObjectId. The target collection should contain all elements from // the source and the target even though they had the same _id's and would have been // merged should we not remove the _id field from the aggregate projection. actual: target.find({}, {_id: 0}).toArray(), expected: [{b: "c"}, {a: 1, b: "a"}, {a: 2, b: "b"}] }); assert.commandWorked(source.dropIndex({_id: 1, a: -1})); assert.commandWorked(target.dropIndex({_id: 1, a: -1})); })(); // Test $merge preserves indexes and options of the existing target collection. (function testMergePresrvesIndexesAndOptions() { const validator = {a: {$gt: 0}}; dropWithoutImplicitRecreate(target.getName()); assert.commandWorked(db.createCollection(target.getName(), {validator: validator})); assert.commandWorked(target.createIndex({a: 1})); assert.doesNotThrow(() => source.aggregate(pipeline)); assertArrayEq({ actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}, {_id: 2, a: 2, b: "b"}] }); assert.eq(2, target.getIndexes().length); const listColl = db.runCommand({listCollections: 1, filter: {name: target.getName()}}); assert.commandWorked(listColl); assert.eq(validator, listColl.cursor.firstBatch[0].options["validator"]); })(); // Test $merge implicitly creates a new database when the target collection's database doesn't // exist. (function testMergeImplicitlyCreatesTargetDatabase() { assert(source.drop()); assert.commandWorked(source.insert({_id: 1, a: 1})); const foreignDb = db.getSiblingDB(`${jsTest.name()}_foreign_db`); assert.commandWorked(foreignDb.dropDatabase()); const foreignTargetCollName = jsTest.name() + "_target"; const foreignPipeline = [{ $merge: { into: {db: foreignDb.getName(), coll: foreignTargetCollName}, whenMatched: "keepExisting", whenNotMatched: "insert" } }]; if (!FixtureHelpers.isMongos(db)) { assert.doesNotThrow(() => source.aggregate(foreignPipeline)); assertArrayEq({ actual: foreignDb[foreignTargetCollName].find().toArray(), expected: [{_id: 1, a: 1}] }); } else { // Implicit database creation is prohibited in a cluster. const error = assert.throws(() => source.aggregate(foreignPipeline)); assert.commandFailedWithCode(error, ErrorCodes.NamespaceNotFound); // Force a creation of the database and collection, then fall through the test below. assert.commandWorked(foreignDb[foreignTargetCollName].insert({_id: 1, a: 1})); } assert.commandWorked(source.update({_id: 1}, {a: 1, b: "a"})); assert.doesNotThrow(() => source.aggregate(foreignPipeline)); assertArrayEq( {actual: foreignDb[foreignTargetCollName].find().toArray(), expected: [{_id: 1, a: 1}]}); assert.commandWorked(foreignDb.dropDatabase()); })(); }());