summaryrefslogtreecommitdiff
path: root/jstests/aggregation/sources/merge/mode_pipeline_insert.js
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/aggregation/sources/merge/mode_pipeline_insert.js')
-rw-r--r--jstests/aggregation/sources/merge/mode_pipeline_insert.js1250
1 files changed, 615 insertions, 635 deletions
diff --git a/jstests/aggregation/sources/merge/mode_pipeline_insert.js b/jstests/aggregation/sources/merge/mode_pipeline_insert.js
index b8f8374cfc9..df3414e0950 100644
--- a/jstests/aggregation/sources/merge/mode_pipeline_insert.js
+++ b/jstests/aggregation/sources/merge/mode_pipeline_insert.js
@@ -4,644 +4,624 @@
// exists when none is expected.
// @tags: [assumes_no_implicit_collection_creation_after_drop]
(function() {
- "use strict";
-
- load("jstests/aggregation/extras/utils.js"); // For assertArrayEq.
- load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers.isMongos.
-
- // A helper function to create a pipeline with a $merge stage using a custom 'updatePipeline'
- // for the whenMatched mode. If 'initialStages' array is specified, the $merge stage will be
- // appended to this array and the result returned to the caller, otherwise an array with a
- // single $merge stage is returned. An output collection for the $merge stage is specified
- // in the 'target', and the $merge stage 'on' fields in the 'on' parameter. The 'letVars'
- // parameter describes the 'let' argument of the $merge stage and holds variables that can be
- // referenced in the pipeline.
- function makeMergePipeline({target = "",
- initialStages = [],
- updatePipeline = [],
- on = "_id",
- letVars = undefined} = {}) {
- const baseObj = letVars !== undefined ? {let : letVars} : {};
- return initialStages.concat([{
- $merge: Object.assign(
- baseObj,
- {into: target, on: on, whenMatched: updatePipeline, whenNotMatched: "insert"})
- }]);
+"use strict";
+
+load("jstests/aggregation/extras/utils.js"); // For assertArrayEq.
+load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers.isMongos.
+
+// A helper function to create a pipeline with a $merge stage using a custom 'updatePipeline'
+// for the whenMatched mode. If 'initialStages' array is specified, the $merge stage will be
+// appended to this array and the result returned to the caller, otherwise an array with a
+// single $merge stage is returned. An output collection for the $merge stage is specified
+// in the 'target', and the $merge stage 'on' fields in the 'on' parameter. The 'letVars'
+// parameter describes the 'let' argument of the $merge stage and holds variables that can be
+// referenced in the pipeline.
+function makeMergePipeline(
+ {target = "", initialStages = [], updatePipeline = [], on = "_id", letVars = undefined} = {}) {
+ const baseObj = letVars !== undefined ? {let : letVars} : {};
+ return initialStages.concat([{
+ $merge: Object.assign(
+ baseObj, {into: target, on: on, whenMatched: updatePipeline, whenNotMatched: "insert"})
+ }]);
+}
+
+const source = db[`${jsTest.name()}_source`];
+source.drop();
+const target = db[`${jsTest.name()}_target`];
+target.drop();
+
+(function testMergeIntoNonExistentCollection() {
+ assert.commandWorked(source.insert({_id: 1, a: 1, b: "a"}));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$addFields: {x: 1}}]})));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [
+ {_id: 1, x: 1},
+ ]
+ });
+})();
+
+// Test $merge inserts a document into an existing target collection if no matching document
+// is found.
+(function testMergeInsertsDocumentIfMatchNotFound() {
+ assert.commandWorked(target.deleteMany({}));
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$addFields: {x: 1, y: 2}}]})));
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 1, y: 2}]});
+})();
+
+// Test $merge updates an existing document in the target collection by applying a
+// pipeline-style update.
+(function testMergeUpdatesDocumentIfMatchFound() {
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ target: target.getName(),
+ updatePipeline: [{$project: {x: {$add: ["$x", 1]}, y: {$add: ["$y", 2]}}}]
+ })));
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2, y: 4}]});
+})();
+
+// Test $merge with various pipeline stages which are currently supported by the pipeline-style
+// update.
+(function testMergeWithSupportedUpdatePipelineStages() {
+ assert(source.drop());
+ assert(target.drop());
+
+ assert.commandWorked(source.insert([{_id: 1, a: 1}, {_id: 2, a: 2}]));
+ assert.commandWorked(target.insert({_id: 1, b: 1}));
+
+ // Test $addFields stage.
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$addFields: {x: {$add: ["$b", 1]}}}]})));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, b: 1, x: 2}, {_id: 2, x: null}]});
+
+ // Test $project stage.
+ assert(target.drop());
+ assert.commandWorked(target.insert({_id: 1, b: 1}));
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$project: {x: {$add: ["$b", 1]}}}]})));
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2}, {_id: 2, x: null}]});
+
+ // Test $replaceWith stage.
+ assert(target.drop());
+ assert.commandWorked(
+ target.insert([{_id: 1, b: 1, c: {x: {y: 1}}}, {_id: 2, b: 2, c: {x: {y: 2}}}]));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$replaceWith: "$c"}]})));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, x: {y: 1}}, {_id: 2, x: {y: 2}}]});
+
+ // Test $replaceRoot stage.
+ assert(target.drop());
+ assert.commandWorked(
+ target.insert([{_id: 1, b: 1, c: {x: {y: 1}}}, {_id: 2, b: 2, c: {x: {y: 2}}}]));
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$replaceRoot: {newRoot: "$c"}}]})));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, x: {y: 1}}, {_id: 2, x: {y: 2}}]});
+})();
+
+// Test $merge inserts a new document into the target collection if not matching document is
+// found by applying a pipeline-style update with upsert=true semantics.
+(function testMergeInsertDocumentIfMatchNotFound() {
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert({_id: 1, a: 1}));
+ assert.commandWorked(target.insert({_id: 2, a: 2}));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$addFields: {x: 1}}]})));
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 1}, {_id: 2, a: 2}]});
+})();
+
+// 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(makeMergePipeline({
+ target: target.getName(),
+ updatePipeline: [{$project: {x: {$add: ["$x", 1]}, a: 1}}]
+ })));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [
+ {_id: 1, x: 1},
+ {_id: 2, a: 2},
+ ]
+ });
+})();
+
+// 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;
}
- const source = db[`${jsTest.name()}_source`];
- source.drop();
- const target = db[`${jsTest.name()}_target`];
- target.drop();
-
- (function testMergeIntoNonExistentCollection() {
- assert.commandWorked(source.insert({_id: 1, a: 1, b: "a"}));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$addFields: {x: 1}}]})));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [
- {_id: 1, x: 1},
- ]
- });
- })();
-
- // Test $merge inserts a document into an existing target collection if no matching document
- // is found.
- (function testMergeInsertsDocumentIfMatchNotFound() {
- assert.commandWorked(target.deleteMany({}));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$addFields: {x: 1, y: 2}}]})));
- assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 1, y: 2}]});
- })();
-
- // Test $merge updates an existing document in the target collection by applying a
- // pipeline-style update.
- (function testMergeUpdatesDocumentIfMatchFound() {
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- target: target.getName(),
- updatePipeline: [{$project: {x: {$add: ["$x", 1]}, y: {$add: ["$y", 2]}}}]
- })));
- assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2, y: 4}]});
- })();
-
- // Test $merge with various pipeline stages which are currently supported by the pipeline-style
- // update.
- (function testMergeWithSupportedUpdatePipelineStages() {
- assert(source.drop());
- assert(target.drop());
-
- assert.commandWorked(source.insert([{_id: 1, a: 1}, {_id: 2, a: 2}]));
- assert.commandWorked(target.insert({_id: 1, b: 1}));
-
- // Test $addFields stage.
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- target: target.getName(),
- updatePipeline: [{$addFields: {x: {$add: ["$b", 1]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, b: 1, x: 2}, {_id: 2, x: null}]});
-
- // Test $project stage.
- assert(target.drop());
- assert.commandWorked(target.insert({_id: 1, b: 1}));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$project: {x: {$add: ["$b", 1]}}}]})));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, x: 2}, {_id: 2, x: null}]});
-
- // Test $replaceWith stage.
- assert(target.drop());
- assert.commandWorked(
- target.insert([{_id: 1, b: 1, c: {x: {y: 1}}}, {_id: 2, b: 2, c: {x: {y: 2}}}]));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$replaceWith: "$c"}]})));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, x: {y: 1}}, {_id: 2, x: {y: 2}}]
- });
-
- // Test $replaceRoot stage.
- assert(target.drop());
- assert.commandWorked(
- target.insert([{_id: 1, b: 1, c: {x: {y: 1}}}, {_id: 2, b: 2, c: {x: {y: 2}}}]));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$replaceRoot: {newRoot: "$c"}}]})));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, x: {y: 1}}, {_id: 2, x: {y: 2}}]
- });
- })();
-
- // Test $merge inserts a new document into the target collection if not matching document is
- // found by applying a pipeline-style update with upsert=true semantics.
- (function testMergeInsertDocumentIfMatchNotFound() {
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert({_id: 1, a: 1}));
- assert.commandWorked(target.insert({_id: 2, a: 2}));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$addFields: {x: 1}}]})));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, x: 1}, {_id: 2, a: 2}]});
- })();
-
- // 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(makeMergePipeline({
- target: target.getName(),
- updatePipeline: [{$project: {x: {$add: ["$x", 1]}, a: 1}}]
- })));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [
- {_id: 1, x: 1},
- {_id: 2, a: 2},
- ]
- });
- })();
-
- // 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;
- }
-
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert({_id: 4, a: 2}));
- assert.commandWorked(target.insert([{_id: 1, x: 1}, {_id: 2, a: 2}]));
- assert.commandWorked(target.createIndex({a: 1}, {unique: true}));
- const error = assert.throws(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$project: {x: 1, a: 1}}]})));
- assert.commandFailedWithCode(error, ErrorCodes.DuplicateKey);
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [
- {_id: 1, x: 1},
- {_id: 2, a: 2},
- ]
- });
- 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(makeMergePipeline({
- target: target.getName(),
- on: "nonexistent",
- updatePipeline: [{$project: {x: 1, a: 1}}]
- })));
- assert.commandFailedWithCode(error, [51190, 51183]);
-
- // The 'on' fields contains multiple document fields.
- error = assert.throws(() => source.aggregate(makeMergePipeline({
- target: target.getName(),
- on: ["nonexistent1", "nonexistent2"],
- updatePipeline: [{$project: {x: 1, a: 1}}]
- })));
- 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.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.createIndex({a: 1}, {unique: true}));
- assert.commandWorked(target.createIndex({a: 1}, {unique: true}));
- assert.commandWorked(source.insert([{_id: 1, a: 1}, {_id: 2, a: 2}, {_id: 3, a: 30}]));
- assert.commandWorked(
- target.insert([{_id: 1, a: 1, b: 1}, {_id: 4, a: 30, b: 2}, {_id: 5, a: 40, b: 3}]));
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- target: target.getName(),
- on: "a",
- updatePipeline: [{$addFields: {z: 1}}]
- })));
- assertArrayEq({
- actual: target.find({}, {_id: 0}).toArray(),
- expected: [{a: 1, b: 1, z: 1}, {a: 2, z: 1}, {a: 30, b: 2, z: 1}, {a: 40, b: 3}]
- });
-
- // The 'on' fields contains multiple document fields.
- assert(source.drop());
- assert(target.drop());
- 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: 1}, {_id: 2, a: 2, b: 4}, {_id: 3, a: 30, b: 2}]));
- assert.commandWorked(
- target.insert([{_id: 1, a: 1, b: 1}, {_id: 4, a: 30, b: 2}, {_id: 5, a: 40, b: 3}]));
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- target: target.getName(),
- on: ["a", "b"],
- updatePipeline: [{$addFields: {z: 1}}]
- })));
- assertArrayEq({
- actual: target.find({}, {_id: 0}).toArray(),
- expected:
- [{a: 1, b: 1, z: 1}, {a: 2, b: 4, z: 1}, {a: 30, b: 2, z: 1}, {a: 40, b: 3}]
- });
- 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;
- }
-
- assert(source.drop());
- assert(target.drop());
- 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"}, c: "y"}));
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- target: target.getName(),
- on: "a.b",
- updatePipeline: [{$addFields: {z: 1}}]
- })));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [
- {_id: 1, a: {b: "b"}, z: 1},
- {_id: 2, a: {b: "c"}, c: "y", z: 1},
- {_id: 3, a: {b: 30}, z: 1}
- ]
- });
- })();
-
- // 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;
- }
-
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.createIndex({"z": 1}, {unique: true}));
- assert.commandWorked(target.createIndex({"z": 1}, {unique: true}));
-
- const pipeline = makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- target: target.getName(),
- on: "z",
- updatePipeline: [{$addFields: {z: 1}}]
- });
-
- // The 'on' field is missing.
- assert.commandWorked(source.insert({_id: 1}));
- let error = assert.throws(() => source.aggregate(pipeline));
- assert.commandFailedWithCode(error, 51132);
-
- // The 'on' field is null.
- assert.commandWorked(source.update({_id: 1}, {z: null}));
- error = assert.throws(() => source.aggregate(pipeline));
- assert.commandFailedWithCode(error, 51132);
-
- // The 'on' field is an array.
- assert.commandWorked(source.update({_id: 1}, {z: [1, 2]}));
- error = assert.throws(() => source.aggregate(pipeline));
- 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. When the _id is missing, the $merge stage will create a new ObjectId in
- // its place before performing the insert or update.
- (function testMergeWhenDocIdIsRemovedFromProjection() {
- let pipeline = makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- target: target.getName(),
- updatePipeline: [{$addFields: {z: 1}}]
- });
-
- // The _id is a single 'on' field (a default one).
- assert(source.drop());
- 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(pipeline));
- assertArrayEq({
- actual: target.find({}, {_id: 0}).toArray(),
- // There is a matching document in the target with {_id: 1}, but since we cannot match
- // it (no _id in projection), we just insert two new documents from the source
- // collection by applying a pipeline-style update.
- expected: [{b: "c"}, {z: 1}, {z: 1}]
- });
-
- pipeline = makeMergePipeline({
- initialStages: [{$project: {_id: 0}}],
- on: ["_id", "a"],
- target: target.getName(),
- updatePipeline: [{$addFields: {z: 1}}]
- });
-
- // The _id is part of the compound 'on' field.
- assert(target.drop());
- 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(pipeline));
- assertArrayEq({
- actual: target.find({}, {_id: 0}).toArray(),
- expected: [{b: "c"}, {a: 1, z: 1}, {a: 2, z: 1}]
- });
- 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 = {z: {$gt: 0}};
- assert(target.drop());
- assert.commandWorked(db.createCollection(target.getName(), {validator: validator}));
- assert.commandWorked(target.createIndex({a: 1}));
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {target: target.getName(), updatePipeline: [{$addFields: {z: 1}}]})));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, z: 1}, {_id: 2, z: 1}]});
- 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, b: "a"}));
-
- const foreignDb = db.getSiblingDB(`${jsTest.name()}_foreign_db`);
- assert.commandWorked(foreignDb.dropDatabase());
- const foreignTarget = foreignDb[`${jsTest.name()}_target`];
- const foreignPipeline = makeMergePipeline({
- target: {db: foreignDb.getName(), coll: foreignTarget.getName()},
- updatePipeline: [{$addFields: {z: 1}}]
- });
-
- if (!FixtureHelpers.isMongos(db)) {
- assert.doesNotThrow(() => source.aggregate(foreignPipeline));
- assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, z: 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(foreignTarget.insert({_id: 1}));
- }
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert({_id: 4, a: 2}));
+ assert.commandWorked(target.insert([{_id: 1, x: 1}, {_id: 2, a: 2}]));
+ assert.commandWorked(target.createIndex({a: 1}, {unique: true}));
+ const error =
+ assert.throws(() => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$project: {x: 1, a: 1}}]})));
+ assert.commandFailedWithCode(error, ErrorCodes.DuplicateKey);
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [
+ {_id: 1, x: 1},
+ {_id: 2, a: 2},
+ ]
+ });
+ 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(makeMergePipeline({
+ target: target.getName(),
+ on: "nonexistent",
+ updatePipeline: [{$project: {x: 1, a: 1}}]
+ })));
+ assert.commandFailedWithCode(error, [51190, 51183]);
+
+ // The 'on' fields contains multiple document fields.
+ error = assert.throws(() => source.aggregate(makeMergePipeline({
+ target: target.getName(),
+ on: ["nonexistent1", "nonexistent2"],
+ updatePipeline: [{$project: {x: 1, a: 1}}]
+ })));
+ 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.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.createIndex({a: 1}, {unique: true}));
+ assert.commandWorked(target.createIndex({a: 1}, {unique: true}));
+ assert.commandWorked(source.insert([{_id: 1, a: 1}, {_id: 2, a: 2}, {_id: 3, a: 30}]));
+ assert.commandWorked(
+ target.insert([{_id: 1, a: 1, b: 1}, {_id: 4, a: 30, b: 2}, {_id: 5, a: 40, b: 3}]));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ target: target.getName(),
+ on: "a",
+ updatePipeline: [{$addFields: {z: 1}}]
+ })));
+ assertArrayEq({
+ actual: target.find({}, {_id: 0}).toArray(),
+ expected: [{a: 1, b: 1, z: 1}, {a: 2, z: 1}, {a: 30, b: 2, z: 1}, {a: 40, b: 3}]
+ });
+
+ // The 'on' fields contains multiple document fields.
+ assert(source.drop());
+ assert(target.drop());
+ 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: 1}, {_id: 2, a: 2, b: 4}, {_id: 3, a: 30, b: 2}]));
+ assert.commandWorked(
+ target.insert([{_id: 1, a: 1, b: 1}, {_id: 4, a: 30, b: 2}, {_id: 5, a: 40, b: 3}]));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ target: target.getName(),
+ on: ["a", "b"],
+ updatePipeline: [{$addFields: {z: 1}}]
+ })));
+ assertArrayEq({
+ actual: target.find({}, {_id: 0}).toArray(),
+ expected: [{a: 1, b: 1, z: 1}, {a: 2, b: 4, z: 1}, {a: 30, b: 2, z: 1}, {a: 40, b: 3}]
+ });
+ 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;
+ }
+ assert(source.drop());
+ assert(target.drop());
+ 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"}, c: "y"}));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ target: target.getName(),
+ on: "a.b",
+ updatePipeline: [{$addFields: {z: 1}}]
+ })));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [
+ {_id: 1, a: {b: "b"}, z: 1},
+ {_id: 2, a: {b: "c"}, c: "y", z: 1},
+ {_id: 3, a: {b: 30}, z: 1}
+ ]
+ });
+})();
+
+// 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;
+ }
+
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.createIndex({"z": 1}, {unique: true}));
+ assert.commandWorked(target.createIndex({"z": 1}, {unique: true}));
+
+ const pipeline = makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ target: target.getName(),
+ on: "z",
+ updatePipeline: [{$addFields: {z: 1}}]
+ });
+
+ // The 'on' field is missing.
+ assert.commandWorked(source.insert({_id: 1}));
+ let error = assert.throws(() => source.aggregate(pipeline));
+ assert.commandFailedWithCode(error, 51132);
+
+ // The 'on' field is null.
+ assert.commandWorked(source.update({_id: 1}, {z: null}));
+ error = assert.throws(() => source.aggregate(pipeline));
+ assert.commandFailedWithCode(error, 51132);
+
+ // The 'on' field is an array.
+ assert.commandWorked(source.update({_id: 1}, {z: [1, 2]}));
+ error = assert.throws(() => source.aggregate(pipeline));
+ 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. When the _id is missing, the $merge stage will create a new ObjectId in
+// its place before performing the insert or update.
+(function testMergeWhenDocIdIsRemovedFromProjection() {
+ let pipeline = makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ target: target.getName(),
+ updatePipeline: [{$addFields: {z: 1}}]
+ });
+
+ // The _id is a single 'on' field (a default one).
+ assert(source.drop());
+ 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(pipeline));
+ assertArrayEq({
+ actual: target.find({}, {_id: 0}).toArray(),
+ // There is a matching document in the target with {_id: 1}, but since we cannot match
+ // it (no _id in projection), we just insert two new documents from the source
+ // collection by applying a pipeline-style update.
+ expected: [{b: "c"}, {z: 1}, {z: 1}]
+ });
+
+ pipeline = makeMergePipeline({
+ initialStages: [{$project: {_id: 0}}],
+ on: ["_id", "a"],
+ target: target.getName(),
+ updatePipeline: [{$addFields: {z: 1}}]
+ });
+
+ // The _id is part of the compound 'on' field.
+ assert(target.drop());
+ 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(pipeline));
+ assertArrayEq({
+ actual: target.find({}, {_id: 0}).toArray(),
+ expected: [{b: "c"}, {a: 1, z: 1}, {a: 2, z: 1}]
+ });
+ 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 = {z: {$gt: 0}};
+ assert(target.drop());
+ assert.commandWorked(db.createCollection(target.getName(), {validator: validator}));
+ assert.commandWorked(target.createIndex({a: 1}));
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline(
+ {target: target.getName(), updatePipeline: [{$addFields: {z: 1}}]})));
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, z: 1}, {_id: 2, z: 1}]});
+ 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, b: "a"}));
+
+ const foreignDb = db.getSiblingDB(`${jsTest.name()}_foreign_db`);
+ assert.commandWorked(foreignDb.dropDatabase());
+ const foreignTarget = foreignDb[`${jsTest.name()}_target`];
+ const foreignPipeline = makeMergePipeline({
+ target: {db: foreignDb.getName(), coll: foreignTarget.getName()},
+ updatePipeline: [{$addFields: {z: 1}}]
+ });
+
+ if (!FixtureHelpers.isMongos(db)) {
assert.doesNotThrow(() => source.aggregate(foreignPipeline));
assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, z: 1}]});
- assert.commandWorked(foreignDb.dropDatabase());
- })();
-
- // Test that $merge can reference the default 'let' variable 'new' which holds the entire
- // document from the source collection.
- (function testMergeWithDefaultLetVariable() {
- assert(source.drop());
- assert(target.drop());
-
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- target: target.getName(),
- updatePipeline: [{$set: {x: {$add: ["$$new.a", "$$new.b"]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 4}]});
- })();
-
- // Test that the default 'let' variable 'new' is not available once the 'let' argument to the
- // $merge stage is specified explicitly.
- (function testMergeCannotUseDefaultLetVariableIfLetIsSpecified() {
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- const error = assert.throws(() => source.aggregate(makeMergePipeline({
- letVars: {foo: "bar"},
- target: target.getName(),
- updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
- })));
- assert.commandFailedWithCode(error, 17276);
- })();
-
- // Test that $merge can accept an empty object holding no variables and the default 'new'
- // variable is not available.
- (function testMergeWithEmptyLetVariables() {
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- // Can use an empty object.
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {letVars: {}, target: target.getName(), updatePipeline: [{$set: {x: "foo"}}]})));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, x: "foo"}]
- });
-
- // No default variable 'new' is available.
- const error = assert.throws(() => source.aggregate(makeMergePipeline({
- letVars: {},
- target: target.getName(),
- updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
- })));
- assert.commandFailedWithCode(error, 17276);
- })();
-
- // Test that $merge can accept a null value as the 'let' argument and the default variable 'new'
- // can be used.
- // Note that this is not a desirable behaviour but rather a limitation in the IDL parser which
- // cannot differentiate between an optional field specified explicitly as 'null', or not
- // specified at all. In both cases it will treat the field like it wasn't specified. So, this
- // test ensures that we're aware of this limitation. Once the limitation is addressed in
- // SERVER-41272, this test should be updated to accordingly.
- (function testMergeWithNullLetVariables() {
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- // Can use a null 'let' argument.
- assert.doesNotThrow(
- () => source.aggregate(makeMergePipeline(
- {letVars: null, target: target.getName(), updatePipeline: [{$set: {x: "foo"}}]})));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, x: "foo"}]
- });
-
- // Can use the default 'new' variable.
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: null,
- target: target.getName(),
- updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
- })));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, x: 1, y: 1}, {_id: 2, x: 2, y: 2}]
- });
- })();
-
- // Test that constant values can be specified in the 'let' argument and referenced in the update
- // pipeline.
- (function testMergeWithConstantLetVariable() {
- // Non-array constants.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {a: 1, b: "foo", c: true},
- target: target.getName(),
- updatePipeline: [{$set: {x: "$$a", y: "$$b", z: "$$c"}}]
- })));
- assertArrayEq({
- actual: target.find().toArray(),
- expected:
- [{_id: 1, c: 1, x: 1, y: "foo", z: true}, {_id: 2, x: 1, y: "foo", z: true}]
- });
-
- // Constant array.
- assert(target.drop());
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {a: [1, 2, 3]},
- target: target.getName(),
- updatePipeline: [{$set: {x: {$arrayElemAt: ["$$a", 1]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 2}]});
- })();
-
- // Test that variables referencing the fields in the source document can be specified in the
- // 'let' argument and referenced in the update pipeline.
- (function testMergeWithNonConstantLetVariables() {
- // Non-array fields.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {x: "$a", y: "$b"},
- target: target.getName(),
- updatePipeline: [{$set: {z: {$add: ["$$x", "$$y"]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, z: 4}]});
-
- // Array field with expressions in the pipeline.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: [1, 2, 3]}, {_id: 2, a: [4, 5, 6]}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {x: "$a"},
- target: target.getName(),
- updatePipeline: [{$set: {z: {$arrayElemAt: ["$$x", 1]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, z: 5}]});
-
- // Array field with expressions in the 'let' argument.
- assert(target.drop());
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {x: {$arrayElemAt: ["$a", 2]}},
- target: target.getName(),
- updatePipeline: [{$set: {z: "$$x"}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 3}, {_id: 2, z: 6}]});
- })();
-
- // Test that variables using the dotted path can be specified in the 'let' argument and
- // referenced in the update pipeline.
- (function testMergeWithDottedPathLetVariables() {
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(source.insert([{_id: 1, a: {b: {c: 2}}}, {_id: 2, a: {b: {c: 3}}}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- letVars: {x: "$a.b.c"},
- target: target.getName(),
- updatePipeline: [{$set: {z: {$pow: ["$$x", 2]}}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 4}, {_id: 2, z: 9}]});
- })();
-
- // Test that 'let' variables are referred to the computed document in the aggregation pipeline,
- // not the original document in the source collection.
- (function testMergeLetVariablesHoldsComputedValues() {
- // Test the default 'new' variable.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(
- source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 1, b: 2}, {_id: 3, a: 2, b: 3}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- // In the $group stage the total field 'a' uses the same name as in the source collection
- // intentionally, to make sure that even when a referenced field is present in the source
- // collection under the same name, the actual value for the variable will be picked up from
- // the computed document.
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- initialStages: [{$group: {_id: "$a", a: {$sum: "$b"}}}],
- target: target.getName(),
- updatePipeline: [{$set: {z: "$$new"}}]
- })));
- assertArrayEq({
- actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, z: {_id: 1, a: 3}}, {_id: 2, z: {_id: 2, a: 3}}]
- });
-
- // Test custom 'let' variables.
- assert(source.drop());
- assert(target.drop());
- assert.commandWorked(
- source.insert([{_id: 1, a: 1, b: 5}, {_id: 2, a: 1, b: 2}, {_id: 3, a: 2, b: 3}]));
- assert.commandWorked(target.insert({_id: 1, c: 1}));
-
- assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
- initialStages: [{$group: {_id: "$a", a: {$sum: "$b"}}}],
- letVars: {x: {$pow: ["$a", 2]}},
- target: target.getName(),
- updatePipeline: [{$set: {z: "$$x"}}]
- })));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 49}, {_id: 2, z: 9}]});
- })();
+ } 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(foreignTarget.insert({_id: 1}));
+ }
+
+ assert.doesNotThrow(() => source.aggregate(foreignPipeline));
+ assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, z: 1}]});
+ assert.commandWorked(foreignDb.dropDatabase());
+})();
+
+// Test that $merge can reference the default 'let' variable 'new' which holds the entire
+// document from the source collection.
+(function testMergeWithDefaultLetVariable() {
+ assert(source.drop());
+ assert(target.drop());
+
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ target: target.getName(),
+ updatePipeline: [{$set: {x: {$add: ["$$new.a", "$$new.b"]}}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 4}]});
+})();
+
+// Test that the default 'let' variable 'new' is not available once the 'let' argument to the
+// $merge stage is specified explicitly.
+(function testMergeCannotUseDefaultLetVariableIfLetIsSpecified() {
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ const error = assert.throws(() => source.aggregate(makeMergePipeline({
+ letVars: {foo: "bar"},
+ target: target.getName(),
+ updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
+ })));
+ assert.commandFailedWithCode(error, 17276);
+})();
+
+// Test that $merge can accept an empty object holding no variables and the default 'new'
+// variable is not available.
+(function testMergeWithEmptyLetVariables() {
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ // Can use an empty object.
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {letVars: {}, target: target.getName(), updatePipeline: [{$set: {x: "foo"}}]})));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, x: "foo"}]
+ });
+
+ // No default variable 'new' is available.
+ const error = assert.throws(() => source.aggregate(makeMergePipeline({
+ letVars: {},
+ target: target.getName(),
+ updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
+ })));
+ assert.commandFailedWithCode(error, 17276);
+})();
+
+// Test that $merge can accept a null value as the 'let' argument and the default variable 'new'
+// can be used.
+// Note that this is not a desirable behaviour but rather a limitation in the IDL parser which
+// cannot differentiate between an optional field specified explicitly as 'null', or not
+// specified at all. In both cases it will treat the field like it wasn't specified. So, this
+// test ensures that we're aware of this limitation. Once the limitation is addressed in
+// SERVER-41272, this test should be updated to accordingly.
+(function testMergeWithNullLetVariables() {
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ // Can use a null 'let' argument.
+ assert.doesNotThrow(
+ () => source.aggregate(makeMergePipeline(
+ {letVars: null, target: target.getName(), updatePipeline: [{$set: {x: "foo"}}]})));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, x: "foo"}]
+ });
+
+ // Can use the default 'new' variable.
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: null,
+ target: target.getName(),
+ updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, x: 1, y: 1}, {_id: 2, x: 2, y: 2}]});
+})();
+
+// Test that constant values can be specified in the 'let' argument and referenced in the update
+// pipeline.
+(function testMergeWithConstantLetVariable() {
+ // Non-array constants.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {a: 1, b: "foo", c: true},
+ target: target.getName(),
+ updatePipeline: [{$set: {x: "$$a", y: "$$b", z: "$$c"}}]
+ })));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, x: 1, y: "foo", z: true}, {_id: 2, x: 1, y: "foo", z: true}]
+ });
+
+ // Constant array.
+ assert(target.drop());
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {a: [1, 2, 3]},
+ target: target.getName(),
+ updatePipeline: [{$set: {x: {$arrayElemAt: ["$$a", 1]}}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 2}]});
+})();
+
+// Test that variables referencing the fields in the source document can be specified in the
+// 'let' argument and referenced in the update pipeline.
+(function testMergeWithNonConstantLetVariables() {
+ // Non-array fields.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 2, b: 2}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {x: "$a", y: "$b"},
+ target: target.getName(),
+ updatePipeline: [{$set: {z: {$add: ["$$x", "$$y"]}}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, z: 4}]});
+
+ // Array field with expressions in the pipeline.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: [1, 2, 3]}, {_id: 2, a: [4, 5, 6]}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {x: "$a"},
+ target: target.getName(),
+ updatePipeline: [{$set: {z: {$arrayElemAt: ["$$x", 1]}}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, z: 5}]});
+
+ // Array field with expressions in the 'let' argument.
+ assert(target.drop());
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {x: {$arrayElemAt: ["$a", 2]}},
+ target: target.getName(),
+ updatePipeline: [{$set: {z: "$$x"}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 3}, {_id: 2, z: 6}]});
+})();
+
+// Test that variables using the dotted path can be specified in the 'let' argument and
+// referenced in the update pipeline.
+(function testMergeWithDottedPathLetVariables() {
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(source.insert([{_id: 1, a: {b: {c: 2}}}, {_id: 2, a: {b: {c: 3}}}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {x: "$a.b.c"},
+ target: target.getName(),
+ updatePipeline: [{$set: {z: {$pow: ["$$x", 2]}}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 4}, {_id: 2, z: 9}]});
+})();
+
+// Test that 'let' variables are referred to the computed document in the aggregation pipeline,
+// not the original document in the source collection.
+(function testMergeLetVariablesHoldsComputedValues() {
+ // Test the default 'new' variable.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(
+ source.insert([{_id: 1, a: 1, b: 1}, {_id: 2, a: 1, b: 2}, {_id: 3, a: 2, b: 3}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ // In the $group stage the total field 'a' uses the same name as in the source collection
+ // intentionally, to make sure that even when a referenced field is present in the source
+ // collection under the same name, the actual value for the variable will be picked up from
+ // the computed document.
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ initialStages: [{$group: {_id: "$a", a: {$sum: "$b"}}}],
+ target: target.getName(),
+ updatePipeline: [{$set: {z: "$$new"}}]
+ })));
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, z: {_id: 1, a: 3}}, {_id: 2, z: {_id: 2, a: 3}}]
+ });
+
+ // Test custom 'let' variables.
+ assert(source.drop());
+ assert(target.drop());
+ assert.commandWorked(
+ source.insert([{_id: 1, a: 1, b: 5}, {_id: 2, a: 1, b: 2}, {_id: 3, a: 2, b: 3}]));
+ assert.commandWorked(target.insert({_id: 1, c: 1}));
+
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ initialStages: [{$group: {_id: "$a", a: {$sum: "$b"}}}],
+ letVars: {x: {$pow: ["$a", 2]}},
+ target: target.getName(),
+ updatePipeline: [{$set: {z: "$$x"}}]
+ })));
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 49}, {_id: 2, z: 9}]});
+})();
}());