summaryrefslogtreecommitdiff
path: root/jstests/aggregation/sources/merge/mode_pipeline_insert.js
diff options
context:
space:
mode:
authorBernard Gorman <bernard.gorman@mongodb.com>2019-11-14 21:59:35 +0000
committerevergreen <evergreen@mongodb.com>2019-11-14 21:59:35 +0000
commit23e55cb3d041236f399f7095df31cd3e3da491cc (patch)
tree25bc309af51bc66dbd46922b0cf7560b3351478a /jstests/aggregation/sources/merge/mode_pipeline_insert.js
parentcdc44d95e169da75093f25c324aa9670e72743e8 (diff)
downloadmongo-23e55cb3d041236f399f7095df31cd3e3da491cc.tar.gz
SERVER-43860 Always upsert exact source document for pipeline-insert $merge
Diffstat (limited to 'jstests/aggregation/sources/merge/mode_pipeline_insert.js')
-rw-r--r--jstests/aggregation/sources/merge/mode_pipeline_insert.js137
1 files changed, 83 insertions, 54 deletions
diff --git a/jstests/aggregation/sources/merge/mode_pipeline_insert.js b/jstests/aggregation/sources/merge/mode_pipeline_insert.js
index df3414e0950..e37f5467b81 100644
--- a/jstests/aggregation/sources/merge/mode_pipeline_insert.js
+++ b/jstests/aggregation/sources/merge/mode_pipeline_insert.js
@@ -37,19 +37,19 @@ target.drop();
assertArrayEq({
actual: target.find().toArray(),
expected: [
- {_id: 1, x: 1},
+ {_id: 1, a: 1, b: "a"},
]
});
})();
-// Test $merge inserts a document into an existing target collection if no matching document
-// is found.
+// Test $merge inserts the original source 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}]});
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}]});
})();
// Test $merge updates an existing document in the target collection by applying a
@@ -57,9 +57,10 @@ target.drop();
(function testMergeUpdatesDocumentIfMatchFound() {
assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
target: target.getName(),
- updatePipeline: [{$project: {x: {$add: ["$x", 1]}, y: {$add: ["$y", 2]}}}]
+ updatePipeline:
+ [{$project: {x: {$add: ["$a", 1]}, y: {$sum: ["$y", 2]}, z: {$add: ["$y", 2]}}}]
})));
- assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2, y: 4}]});
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2, y: 2, z: null}]});
})();
// Test $merge with various pipeline stages which are currently supported by the pipeline-style
@@ -76,7 +77,7 @@ target.drop();
() => 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}]});
+ {actual: target.find().toArray(), expected: [{_id: 1, b: 1, x: 2}, {_id: 2, a: 2}]});
// Test $project stage.
assert(target.drop());
@@ -84,7 +85,7 @@ target.drop();
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}]});
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, x: 2}, {_id: 2, a: 2}]});
// Test $replaceWith stage.
assert(target.drop());
@@ -106,8 +107,8 @@ target.drop();
{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.
+// Test $merge inserts a new document into the target collection if no matching document is
+// found by applying a pipeline-style update with upsert=true and upsertSupplied=true.
(function testMergeInsertDocumentIfMatchNotFound() {
assert(source.drop());
assert(target.drop());
@@ -115,7 +116,7 @@ target.drop();
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}]});
+ assertArrayEq({actual: target.find().toArray(), expected: [{_id: 1, a: 1}, {_id: 2, a: 2}]});
})();
// Test $merge doesn't modify the target collection if a document has been removed from the
@@ -129,7 +130,7 @@ target.drop();
assertArrayEq({
actual: target.find().toArray(),
expected: [
- {_id: 1, x: 1},
+ {_id: 1, a: 1},
{_id: 2, a: 2},
]
});
@@ -186,7 +187,7 @@ target.drop();
// differ from the _id field.
(function testMergeWithOnFields() {
if (FixtureHelpers.isSharded(source)) {
- // Skip this test if the collection sharded, because an implicitly created sharded
+ // Skip this test if the collection is 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;
@@ -208,7 +209,7 @@ target.drop();
})));
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}]
+ expected: [{a: 1, b: 1, z: 1}, {a: 2}, {a: 30, b: 2, z: 1}, {a: 40, b: 3}]
});
// The 'on' fields contains multiple document fields.
@@ -228,7 +229,7 @@ target.drop();
})));
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}]
+ expected: [{a: 1, b: 1, z: 1}, {a: 2, b: 4}, {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}));
@@ -260,12 +261,8 @@ target.drop();
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}
- ]
+ actual: target.find({}, {_id: 0}).toArray(),
+ expected: [{a: {b: "b"}, c: "x"}, {a: {b: "c"}, c: "y", z: 1}, {a: {b: 30}, b: "c"}]
});
})();
@@ -326,9 +323,8 @@ target.drop();
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}]
+ // it (no _id in projection), we insert the two documents from the source collection.
+ expected: [{b: "c"}, {a: 1, b: "a"}, {a: 2, b: "b"}]
});
pipeline = makeMergePipeline({
@@ -346,7 +342,7 @@ target.drop();
assert.doesNotThrow(() => source.aggregate(pipeline));
assertArrayEq({
actual: target.find({}, {_id: 0}).toArray(),
- expected: [{b: "c"}, {a: 1, z: 1}, {a: 2, z: 1}]
+ 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}));
@@ -358,6 +354,7 @@ target.drop();
assert(target.drop());
assert.commandWorked(db.createCollection(target.getName(), {validator: validator}));
assert.commandWorked(target.createIndex({a: 1}));
+ assert.commandWorked(target.insert([{_id: 1, z: 5}, {_id: 2, z: 5}]));
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}]});
@@ -368,8 +365,7 @@ target.drop();
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.
+// Test that $merge implicitly creates a new database when the target collection's db doesn't exist.
(function testMergeImplicitlyCreatesTargetDatabase() {
assert(source.drop());
assert.commandWorked(source.insert({_id: 1, a: 1, b: "a"}));
@@ -384,19 +380,19 @@ target.drop();
if (!FixtureHelpers.isMongos(db)) {
assert.doesNotThrow(() => source.aggregate(foreignPipeline));
- assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, z: 1}]});
+ assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, a: 1, b: "a"}]});
} 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}));
+ // Force creation of the database and collection, then fall through the test below.
+ assert.commandWorked(foreignTarget.insert({_id: 1, a: 1, b: "a"}));
}
assert.doesNotThrow(() => source.aggregate(foreignPipeline));
- assertArrayEq({actual: foreignTarget.find().toArray(), expected: [{_id: 1, z: 1}]});
+ assertArrayEq(
+ {actual: foreignTarget.find().toArray(), expected: [{_id: 1, a: 1, b: "a", z: 1}]});
assert.commandWorked(foreignDb.dropDatabase());
})();
@@ -414,27 +410,28 @@ target.drop();
updatePipeline: [{$set: {x: {$add: ["$$new.a", "$$new.b"]}}}]
})));
assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 4}]});
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, a: 2, b: 2}]});
})();
-// Test that the default 'let' variable 'new' is not available once the 'let' argument to the
-// $merge stage is specified explicitly.
+// Test that the default 'let' variable 'new' is always available even when 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({
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
letVars: {foo: "bar"},
target: target.getName(),
updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
})));
- assert.commandFailedWithCode(error, 17276);
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, x: 1, y: 1}, {_id: 2, a: 2, b: 2}]});
})();
// Test that $merge can accept an empty object holding no variables and the default 'new'
-// variable is not available.
+// variable is still available.
(function testMergeWithEmptyLetVariables() {
assert(source.drop());
assert(target.drop());
@@ -447,16 +444,42 @@ target.drop();
{letVars: {}, target: target.getName(), updatePipeline: [{$set: {x: "foo"}}]})));
assertArrayEq({
actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, x: "foo"}]
+ expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, a: 2, b: 2}]
});
// No default variable 'new' is available.
- const error = assert.throws(() => source.aggregate(makeMergePipeline({
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
letVars: {},
target: target.getName(),
updatePipeline: [{$project: {x: "$$new.a", y: "$$new.b"}}]
})));
- assert.commandFailedWithCode(error, 17276);
+ assertArrayEq(
+ {actual: target.find().toArray(), expected: [{_id: 1, x: 1, y: 1}, {_id: 2, x: 2, y: 2}]});
+})();
+
+// Test that $merge will reject a 'let' specification which attempts to redefine 'new'.
+(function testMergeRejectsLetVariablesWhichRedefineNew() {
+ 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}));
+
+ // Cannot override 'new' with an arbitrary value.
+ const error = assert.throws(() => source.aggregate(makeMergePipeline({
+ letVars: {new: "$a"},
+ target: target.getName(),
+ updatePipeline: [{$set: {x: "foo"}}]
+ })));
+ assert.commandFailedWithCode(error, 51273);
+
+ // If the user's 'let' explicitly sets 'new' to "$$ROOT", we allow it.
+ assert.doesNotThrow(() => source.aggregate(makeMergePipeline({
+ letVars: {new: "$$ROOT"},
+ 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, a: 2, b: 2}]});
})();
// Test that $merge can accept a null value as the 'let' argument and the default variable 'new'
@@ -465,7 +488,7 @@ target.drop();
// 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.
+// SERVER-41272, this test should be updated accordingly.
(function testMergeWithNullLetVariables() {
assert(source.drop());
assert(target.drop());
@@ -478,7 +501,7 @@ target.drop();
{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"}]
+ expected: [{_id: 1, c: 1, x: "foo"}, {_id: 2, a: 2, b: 2}]
});
// Can use the default 'new' variable.
@@ -507,7 +530,7 @@ target.drop();
})));
assertArrayEq({
actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, x: 1, y: "foo", z: true}, {_id: 2, x: 1, y: "foo", z: true}]
+ expected: [{_id: 1, c: 1, x: 1, y: "foo", z: true}, {_id: 2, a: 2, b: 2}]
});
// Constant array.
@@ -520,7 +543,7 @@ target.drop();
updatePipeline: [{$set: {x: {$arrayElemAt: ["$$a", 1]}}}]
})));
assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, x: 2}]});
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, x: 2}, {_id: 2, a: 2, b: 2}]});
})();
// Test that variables referencing the fields in the source document can be specified in the
@@ -538,7 +561,7 @@ target.drop();
updatePipeline: [{$set: {z: {$add: ["$$x", "$$y"]}}}]
})));
assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, z: 4}]});
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 2}, {_id: 2, a: 2, b: 2}]});
// Array field with expressions in the pipeline.
assert(source.drop());
@@ -551,8 +574,10 @@ target.drop();
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}]});
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, z: 2}, {_id: 2, a: [4, 5, 6]}]
+ });
// Array field with expressions in the 'let' argument.
assert(target.drop());
@@ -563,8 +588,10 @@ target.drop();
target: target.getName(),
updatePipeline: [{$set: {z: "$$x"}}]
})));
- assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 3}, {_id: 2, z: 6}]});
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, z: 3}, {_id: 2, a: [4, 5, 6]}]
+ });
})();
// Test that variables using the dotted path can be specified in the 'let' argument and
@@ -580,8 +607,10 @@ target.drop();
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}]});
+ assertArrayEq({
+ actual: target.find().toArray(),
+ expected: [{_id: 1, c: 1, z: 4}, {_id: 2, a: {b: {c: 3}}}]
+ });
})();
// Test that 'let' variables are referred to the computed document in the aggregation pipeline,
@@ -605,7 +634,7 @@ target.drop();
})));
assertArrayEq({
actual: target.find().toArray(),
- expected: [{_id: 1, c: 1, z: {_id: 1, a: 3}}, {_id: 2, z: {_id: 2, a: 3}}]
+ expected: [{_id: 1, c: 1, z: {_id: 1, a: 3}}, {_id: 2, a: 3}]
});
// Test custom 'let' variables.
@@ -622,6 +651,6 @@ target.drop();
updatePipeline: [{$set: {z: "$$x"}}]
})));
assertArrayEq(
- {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 49}, {_id: 2, z: 9}]});
+ {actual: target.find().toArray(), expected: [{_id: 1, c: 1, z: 49}, {_id: 2, a: 3}]});
})();
}());