summaryrefslogtreecommitdiff
path: root/jstests/sharding
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-12-19 17:24:04 -0500
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2018-01-12 13:02:04 -0500
commit7298d273c0497f2720ec1471ad0f4910bff07af4 (patch)
tree09d37af52b38667db4f75b81d8203d8975e27848 /jstests/sharding
parent5fe3df3c49c1f7b0906cc7650f3b339f22ddd0b5 (diff)
downloadmongo-7298d273c0497f2720ec1471ad0f4910bff07af4.tar.gz
SERVER-32308: Add the ability for a $lookup stage to execute on mongos against a sharded foreign collection
Diffstat (limited to 'jstests/sharding')
-rw-r--r--jstests/sharding/collation_lookup.js454
-rw-r--r--jstests/sharding/lookup.js609
-rw-r--r--jstests/sharding/lookup_mongod_unaware.js168
-rw-r--r--jstests/sharding/lookup_stale_mongos.js130
4 files changed, 1361 insertions, 0 deletions
diff --git a/jstests/sharding/collation_lookup.js b/jstests/sharding/collation_lookup.js
new file mode 100644
index 00000000000..f06e92ab3fc
--- /dev/null
+++ b/jstests/sharding/collation_lookup.js
@@ -0,0 +1,454 @@
+/**
+ * Tests that the $lookup stage respects the collation when the local and/or foreign collections
+ * are sharded.
+ *
+ * The comparison of string values between the 'localField' and 'foreignField' should use the
+ * collation either explicitly set on the aggregation operation, or the collation inherited from the
+ * collection the "aggregate" command was performed on.
+ */
+(function() {
+ "use strict";
+
+ load("jstests/aggregation/extras/utils.js"); // for arrayEq
+
+ function runTests(withDefaultCollationColl, withoutDefaultCollationColl, collation) {
+ // Test that the $lookup stage respects the inherited collation.
+ let res = withDefaultCollationColl
+ .aggregate([{
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ localField: "str",
+ foreignField: "str",
+ as: "matched",
+ },
+ }])
+ .toArray();
+ assert.eq(1, res.length, tojson(res));
+
+ let expected = [{_id: "lowercase", str: "abc"}, {_id: "uppercase", str: "ABC"}];
+ assert(arrayEq(expected, res[0].matched),
+ "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) +
+ " up to ordering");
+
+ res = withDefaultCollationColl
+ .aggregate([{
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str1: "$str"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str2: "$str"},
+ pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
+ as: "matched2"
+ }
+ }
+ ],
+ as: "matched1",
+ },
+ }])
+ .toArray();
+ assert.eq(1, res.length, tojson(res));
+
+ expected = [
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
+ },
+ {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2":
+ [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
+ }
+ ];
+ assert(arrayEq(expected, res[0].matched1),
+ "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
+ " up to ordering. " + tojson(res));
+
+ // Test that the $lookup stage respects the inherited collation when it optimizes with an
+ // $unwind stage.
+ res = withDefaultCollationColl
+ .aggregate([
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ localField: "str",
+ foreignField: "str",
+ as: "matched",
+ },
+ },
+ {$unwind: "$matched"},
+ ])
+ .toArray();
+ assert.eq(2, res.length, tojson(res));
+
+ expected = [
+ {_id: "lowercase", str: "abc", matched: {_id: "lowercase", str: "abc"}},
+ {_id: "lowercase", str: "abc", matched: {_id: "uppercase", str: "ABC"}}
+ ];
+ assert(arrayEq(expected, res),
+ "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");
+
+ res = withDefaultCollationColl
+ .aggregate([
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str1: "$str"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str2: "$str"},
+ pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
+ as: "matched2"
+ }
+ },
+ {$unwind: "$matched2"},
+ ],
+ as: "matched1",
+ },
+ },
+ {$unwind: "$matched1"},
+ ])
+ .toArray();
+ assert.eq(4, res.length, tojson(res));
+
+ expected = [
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": {"_id": "lowercase", "str": "abc"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": {"_id": "uppercase", "str": "ABC"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2": {"_id": "lowercase", "str": "abc"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2": {"_id": "uppercase", "str": "ABC"}
+ }
+ }
+ ];
+ assert(arrayEq(expected, res),
+ "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");
+
+ // Test that the $lookup stage respects an explicit collation on the aggregation operation.
+ res = withoutDefaultCollationColl
+ .aggregate(
+ [
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ localField: "str",
+ foreignField: "str",
+ as: "matched",
+ },
+ },
+ ],
+ collation)
+ .toArray();
+ assert.eq(1, res.length, tojson(res));
+
+ expected = [{_id: "lowercase", str: "abc"}, {_id: "uppercase", str: "ABC"}];
+ assert(arrayEq(expected, res[0].matched),
+ "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) +
+ " up to ordering");
+
+ res = withoutDefaultCollationColl
+ .aggregate(
+ [
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str1: "$str"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str2: "$str"},
+ pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
+ as: "matched2"
+ }
+ }
+ ],
+ as: "matched1",
+ },
+ }
+ ],
+ collation)
+ .toArray();
+ assert.eq(1, res.length, tojson(res));
+
+ expected = [
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
+ },
+ {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2":
+ [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
+ }
+ ];
+ assert(arrayEq(expected, res[0].matched1),
+ "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
+ " up to ordering");
+
+ // Test that the $lookup stage respects an explicit collation on the aggregation operation
+ // when
+ // it optimizes with an $unwind stage.
+ res = withoutDefaultCollationColl
+ .aggregate(
+ [
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ localField: "str",
+ foreignField: "str",
+ as: "matched",
+ },
+ },
+ {$unwind: "$matched"},
+ ],
+ collation)
+ .toArray();
+ assert.eq(2, res.length, tojson(res));
+
+ expected = [
+ {_id: "lowercase", str: "abc", matched: {_id: "lowercase", str: "abc"}},
+ {_id: "lowercase", str: "abc", matched: {_id: "uppercase", str: "ABC"}}
+ ];
+ assert(arrayEq(expected, res),
+ "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");
+
+ res = withoutDefaultCollationColl
+ .aggregate(
+ [
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str1: "$str"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str2: "$str"},
+ pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
+ as: "matched2"
+ }
+ },
+ {$unwind: "$matched2"},
+ ],
+ as: "matched1",
+ },
+ },
+ {$unwind: "$matched1"},
+ ],
+ collation)
+ .toArray();
+ assert.eq(4, res.length, tojson(res));
+
+ expected = [
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": {"_id": "lowercase", "str": "abc"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": {"_id": "uppercase", "str": "ABC"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2": {"_id": "lowercase", "str": "abc"}
+ }
+ },
+ {
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": {
+ "_id": "uppercase",
+ "str": "ABC",
+ "matched2": {"_id": "uppercase", "str": "ABC"}
+ }
+ }
+ ];
+ assert(arrayEq(expected, res),
+ "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");
+
+ // Test that the $lookup stage uses the "simple" collation if a collation isn't set on the
+ // collection or the aggregation operation.
+ res = withoutDefaultCollationColl
+ .aggregate([
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withDefaultCollationColl.getName(),
+ localField: "str",
+ foreignField: "str",
+ as: "matched",
+ },
+ },
+ ])
+ .toArray();
+ assert.eq([{_id: "lowercase", str: "abc", matched: [{_id: "lowercase", str: "abc"}]}], res);
+
+ res = withoutDefaultCollationColl
+ .aggregate([
+ {$match: {_id: "lowercase"}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str1: "$str"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
+ {
+ $lookup: {
+ from: withoutDefaultCollationColl.getName(),
+ let : {str2: "$str"},
+ pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
+ as: "matched2"
+ }
+ },
+ {$unwind: "$matched2"},
+ ],
+ as: "matched1",
+ },
+ },
+ ])
+ .toArray();
+ assert.eq([{
+ "_id": "lowercase",
+ "str": "abc",
+ "matched1": [{
+ "_id": "lowercase",
+ "str": "abc",
+ "matched2": {"_id": "lowercase", "str": "abc"}
+ }]
+ }],
+ res);
+ }
+
+ const st = new ShardingTest({shards: 2, config: 1});
+ const testName = "collation_lookup";
+ const caseInsensitive = {collation: {locale: "en_US", strength: 2}};
+
+ const mongosDB = st.s0.getDB(testName);
+ const withDefaultCollationColl = mongosDB[testName + "_with_default"];
+ const withoutDefaultCollationColl = mongosDB[testName + "_without_default"];
+
+ assert.commandWorked(
+ mongosDB.createCollection(withDefaultCollationColl.getName(), caseInsensitive));
+ assert.writeOK(withDefaultCollationColl.insert({_id: "lowercase", str: "abc"}));
+
+ assert.writeOK(withoutDefaultCollationColl.insert({_id: "lowercase", str: "abc"}));
+ assert.writeOK(withoutDefaultCollationColl.insert({_id: "uppercase", str: "ABC"}));
+ assert.writeOK(withoutDefaultCollationColl.insert({_id: "unmatched", str: "def"}));
+
+ //
+ // Sharded collection with default collation and unsharded collection without a default
+ // collation.
+ //
+ assert.commandWorked(
+ withDefaultCollationColl.createIndex({str: 1}, {collation: {locale: "simple"}}));
+
+ // Enable sharding on the test DB and ensure its primary is shard0000.
+ assert.commandWorked(mongosDB.adminCommand({enableSharding: mongosDB.getName()}));
+ st.ensurePrimaryShard(mongosDB.getName(), st.shard0.shardName);
+
+ // Shard the collection with a default collation.
+ assert.commandWorked(mongosDB.adminCommand({
+ shardCollection: withDefaultCollationColl.getFullName(),
+ key: {str: 1},
+ collation: {locale: "simple"}
+ }));
+
+ // Split the collection into 2 chunks.
+ assert.commandWorked(mongosDB.adminCommand(
+ {split: withDefaultCollationColl.getFullName(), middle: {str: "abc"}}));
+
+ // Move the chunk containing {str: "abc"} to shard0001.
+ assert.commandWorked(mongosDB.adminCommand({
+ moveChunk: withDefaultCollationColl.getFullName(),
+ find: {str: "abc"},
+ to: st.shard1.shardName
+ }));
+
+ runTests(withDefaultCollationColl, withoutDefaultCollationColl, caseInsensitive);
+
+ // TODO: Enable the following tests once SERVER-32536 is fixed.
+ //
+ // Sharded collection with default collation and sharded collection without a default
+ // collation.
+ //
+
+ // Shard the collection without a default collation.
+ // assert.commandWorked(mongosDB.adminCommand({
+ // shardCollection: withoutDefaultCollationColl.getFullName(),
+ // key: {_id: 1},
+ // }));
+
+ // // Split the collection into 2 chunks.
+ // assert.commandWorked(mongosDB.adminCommand(
+ // {split: withoutDefaultCollationColl.getFullName(), middle: {_id: "unmatched"}}));
+
+ // // Move the chunk containing {_id: "lowercase"} to shard0001.
+ // assert.commandWorked(mongosDB.adminCommand({
+ // moveChunk: withoutDefaultCollationColl.getFullName(),
+ // find: {_id: "lowercase"},
+ // to: st.shard1.shardName
+ // }));
+
+ // runTests(withDefaultCollationColl, withoutDefaultCollationColl, caseInsensitive);
+
+ st.stop();
+})();
diff --git a/jstests/sharding/lookup.js b/jstests/sharding/lookup.js
new file mode 100644
index 00000000000..cc41cbff319
--- /dev/null
+++ b/jstests/sharding/lookup.js
@@ -0,0 +1,609 @@
+// Basic $lookup regression tests.
+(function() {
+ "use strict";
+
+ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode.
+
+ const st = new ShardingTest({shards: 2, config: 1, mongos: 1});
+ const testName = "lookup_sharded";
+
+ const mongosDB = st.s0.getDB(testName);
+ assert.commandWorked(mongosDB.dropDatabase());
+
+ // Used by testPipeline to sort result documents. All _ids must be primitives.
+ function compareId(a, b) {
+ if (a._id < b._id) {
+ return -1;
+ }
+ if (a._id > b._id) {
+ return 1;
+ }
+ return 0;
+ }
+
+ // Helper for testing that pipeline returns correct set of results.
+ function testPipeline(pipeline, expectedResult, collection) {
+ assert.eq(collection.aggregate(pipeline).toArray().sort(compareId),
+ expectedResult.sort(compareId));
+ }
+
+ // Shards and splits the collection 'coll' on _id.
+ function shardAndSplit(db, coll) {
+ // Shard the collection on _id.
+ assert.commandWorked(db.adminCommand({shardCollection: coll.getFullName(), key: {_id: 1}}));
+
+ // Split the collection into 2 chunks: [MinKey, 0), [0, MaxKey).
+ assert.commandWorked(db.adminCommand({split: coll.getFullName(), middle: {_id: 0}}));
+
+ // Move the [0, MaxKey) chunk to shard0001.
+ assert.commandWorked(db.adminCommand({
+ moveChunk: coll.getFullName(),
+ find: {_id: 1},
+ to: st.shard1.shardName,
+ }));
+ }
+
+ function runTest(coll, from, thirdColl, fourthColl) {
+ let db = null; // Using the db variable is banned in this function.
+
+ coll.remove({});
+ from.remove({});
+ thirdColl.remove({});
+ fourthColl.remove({});
+
+ assert.writeOK(coll.insert({_id: 0, a: 1}));
+ assert.writeOK(coll.insert({_id: 1, a: null}));
+ assert.writeOK(coll.insert({_id: 2}));
+
+ assert.writeOK(from.insert({_id: 0, b: 1}));
+ assert.writeOK(from.insert({_id: 1, b: null}));
+ assert.writeOK(from.insert({_id: 2}));
+ //
+ // Basic functionality.
+ //
+
+ // "from" document added to "as" field if a == b, where nonexistent fields are treated as
+ // null.
+ let expectedResults = [
+ {_id: 0, a: 1, "same": [{_id: 0, b: 1}]},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline([{$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}}],
+ expectedResults,
+ coll);
+
+ // If localField is nonexistent, it is treated as if it is null.
+ expectedResults = [
+ {_id: 0, a: 1, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline(
+ [{$lookup: {localField: "nonexistent", foreignField: "b", from: "from", as: "same"}}],
+ expectedResults,
+ coll);
+
+ // If foreignField is nonexistent, it is treated as if it is null.
+ expectedResults = [
+ {_id: 0, a: 1, "same": []},
+ {_id: 1, a: null, "same": [{_id: 0, b: 1}, {_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 0, b: 1}, {_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline(
+ [{$lookup: {localField: "a", foreignField: "nonexistent", from: "from", as: "same"}}],
+ expectedResults,
+ coll);
+
+ // If there are no matches or the from coll doesn't exist, the result is an empty array.
+ expectedResults =
+ [{_id: 0, a: 1, "same": []}, {_id: 1, a: null, "same": []}, {_id: 2, "same": []}];
+ testPipeline(
+ [{$lookup: {localField: "_id", foreignField: "nonexistent", from: "from", as: "same"}}],
+ expectedResults,
+ coll);
+ testPipeline(
+ [{$lookup: {localField: "a", foreignField: "b", from: "nonexistent", as: "same"}}],
+ expectedResults,
+ coll);
+
+ // If field name specified by "as" already exists, it is overwritten.
+ expectedResults = [
+ {_id: 0, "a": [{_id: 0, b: 1}]},
+ {_id: 1, "a": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "a": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline([{$lookup: {localField: "a", foreignField: "b", from: "from", as: "a"}}],
+ expectedResults,
+ coll);
+
+ // Running multiple $lookups in the same pipeline is allowed.
+ expectedResults = [
+ {_id: 0, a: 1, "c": [{_id: 0, b: 1}], "d": [{_id: 0, b: 1}]},
+ {
+ _id: 1,
+ a: null, "c": [{_id: 1, b: null}, {_id: 2}], "d": [{_id: 1, b: null}, {_id: 2}]
+ },
+ {_id: 2, "c": [{_id: 1, b: null}, {_id: 2}], "d": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "a", foreignField: "b", from: "from", as: "c"}},
+ {$project: {"a": 1, "c": 1}},
+ {$lookup: {localField: "a", foreignField: "b", from: "from", as: "d"}}
+ ],
+ expectedResults,
+ coll);
+
+ //
+ // Coalescing with $unwind.
+ //
+
+ // A normal $unwind with on the "as" field.
+ expectedResults = [
+ {_id: 0, a: 1, same: {_id: 0, b: 1}},
+ {_id: 1, a: null, same: {_id: 1, b: null}},
+ {_id: 1, a: null, same: {_id: 2}},
+ {_id: 2, same: {_id: 1, b: null}},
+ {_id: 2, same: {_id: 2}}
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}},
+ {$unwind: {path: "$same"}}
+ ],
+ expectedResults,
+ coll);
+
+ // An $unwind on the "as" field, with includeArrayIndex.
+ expectedResults = [
+ {_id: 0, a: 1, same: {_id: 0, b: 1}, index: NumberLong(0)},
+ {_id: 1, a: null, same: {_id: 1, b: null}, index: NumberLong(0)},
+ {_id: 1, a: null, same: {_id: 2}, index: NumberLong(1)},
+ {_id: 2, same: {_id: 1, b: null}, index: NumberLong(0)},
+ {_id: 2, same: {_id: 2}, index: NumberLong(1)},
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}},
+ {$unwind: {path: "$same", includeArrayIndex: "index"}}
+ ],
+ expectedResults,
+ coll);
+
+ // Normal $unwind with no matching documents.
+ expectedResults = [];
+ testPipeline(
+ [
+ {$lookup: {localField: "_id", foreignField: "nonexistent", from: "from", as: "same"}},
+ {$unwind: {path: "$same"}}
+ ],
+ expectedResults,
+ coll);
+
+ // $unwind with preserveNullAndEmptyArray with no matching documents.
+ expectedResults = [
+ {_id: 0, a: 1},
+ {_id: 1, a: null},
+ {_id: 2},
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "_id", foreignField: "nonexistent", from: "from", as: "same"}},
+ {$unwind: {path: "$same", preserveNullAndEmptyArrays: true}}
+ ],
+ expectedResults,
+ coll);
+
+ // $unwind with preserveNullAndEmptyArray, some with matching documents, some without.
+ expectedResults = [
+ {_id: 0, a: 1},
+ {_id: 1, a: null, same: {_id: 0, b: 1}},
+ {_id: 2},
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "_id", foreignField: "b", from: "from", as: "same"}},
+ {$unwind: {path: "$same", preserveNullAndEmptyArrays: true}}
+ ],
+ expectedResults,
+ coll);
+
+ // $unwind with preserveNullAndEmptyArray and includeArrayIndex, some with matching
+ // documents, some without.
+ expectedResults = [
+ {_id: 0, a: 1, index: null},
+ {_id: 1, a: null, same: {_id: 0, b: 1}, index: NumberLong(0)},
+ {_id: 2, index: null},
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "_id", foreignField: "b", from: "from", as: "same"}},
+ {
+ $unwind:
+ {path: "$same", preserveNullAndEmptyArrays: true, includeArrayIndex: "index"}
+ }
+ ],
+ expectedResults,
+ coll);
+
+ //
+ // Dependencies.
+ //
+
+ // If $lookup didn't add "localField" to its dependencies, this test would fail as the
+ // value of the "a" field would be lost and treated as null.
+ expectedResults = [
+ {_id: 0, "same": [{_id: 0, b: 1}]},
+ {_id: 1, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+ testPipeline(
+ [
+ {$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}},
+ {$project: {"same": 1}}
+ ],
+ expectedResults,
+ coll);
+
+ // If $lookup didn't add fields referenced by "let" variables to its dependencies, this test
+ // would fail as the value of the "a" field would be lost and treated as null.
+ expectedResults = [
+ {"_id": 0, "same": [{"_id": 0, "x": 1}, {"_id": 1, "x": 1}, {"_id": 2, "x": 1}]},
+ {
+ "_id": 1,
+ "same": [{"_id": 0, "x": null}, {"_id": 1, "x": null}, {"_id": 2, "x": null}]
+ },
+ {"_id": 2, "same": [{"_id": 0}, {"_id": 1}, {"_id": 2}]}
+ ];
+ testPipeline(
+ [
+ {
+ $lookup: {
+ let : {var1: "$a"},
+ pipeline: [{$project: {x: "$$var1"}}],
+ from: "from",
+ as: "same"
+ }
+ },
+ {$project: {"same": 1}}
+ ],
+ expectedResults,
+ coll);
+
+ //
+ // Dotted field paths.
+ //
+
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: 1}));
+ assert.writeOK(coll.insert({_id: 1, a: null}));
+ assert.writeOK(coll.insert({_id: 2}));
+ assert.writeOK(coll.insert({_id: 3, a: {c: 1}}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0, b: 1}));
+ assert.writeOK(from.insert({_id: 1, b: null}));
+ assert.writeOK(from.insert({_id: 2}));
+ assert.writeOK(from.insert({_id: 3, b: {c: 1}}));
+ assert.writeOK(from.insert({_id: 4, b: {c: 2}}));
+
+ // Once without a dotted field.
+ let pipeline = [{$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}}];
+ expectedResults = [
+ {_id: 0, a: 1, "same": [{_id: 0, b: 1}]},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 3, a: {c: 1}, "same": [{_id: 3, b: {c: 1}}]}
+ ];
+ testPipeline(pipeline, expectedResults, coll);
+
+ // Look up a dotted field.
+ pipeline = [{$lookup: {localField: "a.c", foreignField: "b.c", from: "from", as: "same"}}];
+ // All but the last document in 'coll' have a nullish value for 'a.c'.
+ expectedResults = [
+ {_id: 0, a: 1, same: [{_id: 0, b: 1}, {_id: 1, b: null}, {_id: 2}]},
+ {_id: 1, a: null, same: [{_id: 0, b: 1}, {_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, same: [{_id: 0, b: 1}, {_id: 1, b: null}, {_id: 2}]},
+ {_id: 3, a: {c: 1}, same: [{_id: 3, b: {c: 1}}]}
+ ];
+ testPipeline(pipeline, expectedResults, coll);
+
+ // With an $unwind stage.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: {b: 1}}));
+ assert.writeOK(coll.insert({_id: 1}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0, target: 1}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a.b",
+ foreignField: "target",
+ from: "from",
+ as: "same.documents",
+ }
+ },
+ {
+ // Expected input to $unwind:
+ // {_id: 0, a: {b: 1}, same: {documents: [{_id: 0, target: 1}]}}
+ // {_id: 1, same: {documents: []}}
+ $unwind: {
+ path: "$same.documents",
+ preserveNullAndEmptyArrays: true,
+ includeArrayIndex: "c.d.e",
+ }
+ }
+ ];
+ expectedResults = [
+ {_id: 0, a: {b: 1}, same: {documents: {_id: 0, target: 1}}, c: {d: {e: NumberLong(0)}}},
+ {_id: 1, same: {}, c: {d: {e: null}}},
+ ];
+ testPipeline(pipeline, expectedResults, coll);
+
+ //
+ // Query-like local fields (SERVER-21287)
+ //
+
+ // This must only do an equality match rather than treating the value as a regex.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: /a regex/}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0, b: /a regex/}));
+ assert.writeOK(from.insert({_id: 1, b: "string that matches /a regex/"}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a",
+ foreignField: "b",
+ from: "from",
+ as: "b",
+ }
+ },
+ ];
+ expectedResults = [{_id: 0, a: /a regex/, b: [{_id: 0, b: /a regex/}]}];
+ testPipeline(pipeline, expectedResults, coll);
+
+ //
+ // A local value of an array.
+ //
+
+ // Basic array corresponding to multiple documents.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: [0, 1, 2]}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0}));
+ assert.writeOK(from.insert({_id: 1}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a",
+ foreignField: "_id",
+ from: "from",
+ as: "b",
+ }
+ },
+ ];
+ expectedResults = [{_id: 0, a: [0, 1, 2], b: [{_id: 0}, {_id: 1}]}];
+ testPipeline(pipeline, expectedResults, coll);
+
+ // Basic array corresponding to a single document.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: [1]}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0}));
+ assert.writeOK(from.insert({_id: 1}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a",
+ foreignField: "_id",
+ from: "from",
+ as: "b",
+ }
+ },
+ ];
+ expectedResults = [{_id: 0, a: [1], b: [{_id: 1}]}];
+ testPipeline(pipeline, expectedResults, coll);
+
+ // Array containing regular expressions.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: [/a regex/, /^x/]}));
+ assert.writeOK(coll.insert({_id: 1, a: [/^x/]}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0, b: "should not match a regex"}));
+ assert.writeOK(from.insert({_id: 1, b: "xxxx"}));
+ assert.writeOK(from.insert({_id: 2, b: /a regex/}));
+ assert.writeOK(from.insert({_id: 3, b: /^x/}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a",
+ foreignField: "b",
+ from: "from",
+ as: "b",
+ }
+ },
+ ];
+ expectedResults = [
+ {_id: 0, a: [/a regex/, /^x/], b: [{_id: 2, b: /a regex/}, {_id: 3, b: /^x/}]},
+ {_id: 1, a: [/^x/], b: [{_id: 3, b: /^x/}]}
+ ];
+ testPipeline(pipeline, expectedResults, coll);
+
+ // 'localField' references a field within an array of sub-objects.
+ coll.remove({});
+ assert.writeOK(coll.insert({_id: 0, a: [{b: 1}, {b: 2}]}));
+
+ from.remove({});
+ assert.writeOK(from.insert({_id: 0}));
+ assert.writeOK(from.insert({_id: 1}));
+ assert.writeOK(from.insert({_id: 2}));
+ assert.writeOK(from.insert({_id: 3}));
+
+ pipeline = [
+ {
+ $lookup: {
+ localField: "a.b",
+ foreignField: "_id",
+ from: "from",
+ as: "c",
+ }
+ },
+ ];
+
+ expectedResults = [{"_id": 0, "a": [{"b": 1}, {"b": 2}], "c": [{"_id": 1}, {"_id": 2}]}];
+ testPipeline(pipeline, expectedResults, coll);
+
+ //
+ // Test $lookup when the foreign collection is a view.
+ //
+ // TODO: Enable this test as part of SERVER-32548, fails whenever the foreign collection is
+ // sharded.
+ // coll.getDB().fromView.drop();
+ // assert.commandWorked(
+ // coll.getDB().runCommand({create: "fromView", viewOn: "from", pipeline: []}));
+
+ // pipeline = [
+ // {
+ // $lookup: {
+ // localField: "a.b",
+ // foreignField: "_id",
+ // from: "fromView",
+ // as: "c",
+ // }
+ // },
+ // ];
+
+ // expectedResults = [{"_id": 0, "a": [{"b": 1}, {"b": 2}], "c": [{"_id": 1}, {"_id": 2}]}];
+ // testPipeline(pipeline, expectedResults, coll);
+
+ //
+ // Error cases.
+ //
+
+ // 'from', 'as', 'localField' and 'foreignField' must all be specified when run with
+ // localField/foreignField syntax.
+ assertErrorCode(coll,
+ [{$lookup: {foreignField: "b", from: "from", as: "same"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", from: "from", as: "same"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: "b", as: "same"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: "b", from: "from"}}],
+ ErrorCodes.FailedToParse);
+
+ // localField/foreignField and pipeline/let syntax must not be mixed.
+ assertErrorCode(coll,
+ [{$lookup: {pipeline: [], foreignField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {pipeline: [], localField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(
+ coll,
+ [{$lookup: {pipeline: [], localField: "b", foreignField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {let : {a: "$b"}, foreignField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {let : {a: "$b"}, localField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(
+ coll,
+ [{
+ $lookup:
+ {let : {a: "$b"}, localField: "b", foreignField: "b", from: "from", as: "as"}
+ }],
+ ErrorCodes.FailedToParse);
+
+ // 'from', 'as', 'localField' and 'foreignField' must all be of type string.
+ assertErrorCode(coll,
+ [{$lookup: {localField: 1, foreignField: "b", from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: 1, from: "from", as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: "b", from: 1, as: "as"}}],
+ ErrorCodes.FailedToParse);
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: "b", from: "from", as: 1}}],
+ ErrorCodes.FailedToParse);
+
+ // The foreign collection must be a valid namespace.
+ assertErrorCode(coll,
+ [{$lookup: {localField: "a", foreignField: "b", from: "", as: "as"}}],
+ ErrorCodes.InvalidNamespace);
+ // $lookup's field must be an object.
+ assertErrorCode(coll, [{$lookup: "string"}], ErrorCodes.FailedToParse);
+ }
+
+ //
+ // Test unsharded local collection and unsharded foreign collection.
+ //
+ mongosDB.lookUp.drop();
+ mongosDB.from.drop();
+ mongosDB.thirdColl.drop();
+ mongosDB.fourthColl.drop();
+
+ runTest(mongosDB.lookUp, mongosDB.from, mongosDB.thirdColl, mongosDB.fourthColl);
+
+ // Verify that the command is sent only to the primary shard when both the local and foreign
+ // collections are unsharded.
+ assert(!assert
+ .commandWorked(mongosDB.lookup.explain().aggregate([{
+ $lookup: {
+ from: mongosDB.from.getName(),
+ localField: "a",
+ foreignField: "b",
+ as: "results"
+ }
+ }]))
+ .hasOwnProperty("shards"));
+ // Enable sharding on the test DB and ensure its primary is shard0000.
+ assert.commandWorked(mongosDB.adminCommand({enableSharding: mongosDB.getName()}));
+ st.ensurePrimaryShard(mongosDB.getName(), st.shard0.shardName);
+
+ //
+ // Test unsharded local collection and sharded foreign collection.
+ //
+
+ // Shard the foreign collection on _id.
+ shardAndSplit(mongosDB, mongosDB.from);
+ runTest(mongosDB.lookUp, mongosDB.from, mongosDB.thirdColl, mongosDB.fourthColl);
+
+ //
+ // Test sharded local collection and unsharded foreign collection.
+ //
+ mongosDB.from.drop();
+
+ // Shard the local collection on _id.
+ shardAndSplit(mongosDB, mongosDB.lookup);
+ runTest(mongosDB.lookUp, mongosDB.from, mongosDB.thirdColl, mongosDB.fourthColl);
+
+ //
+ // Test sharded local and foreign collections.
+ //
+
+ // Shard the foreign collection on _id.
+ shardAndSplit(mongosDB, mongosDB.from);
+ runTest(mongosDB.lookUp, mongosDB.from, mongosDB.thirdColl, mongosDB.fourthColl);
+
+ st.stop();
+}());
diff --git a/jstests/sharding/lookup_mongod_unaware.js b/jstests/sharding/lookup_mongod_unaware.js
new file mode 100644
index 00000000000..6333eec15de
--- /dev/null
+++ b/jstests/sharding/lookup_mongod_unaware.js
@@ -0,0 +1,168 @@
+// Tests the behavior of a $lookup when a shard contains incorrect routing information for the
+// local and/or foreign collections. This includes when the shard thinks the collection is sharded
+// when it's not, and likewise when it thinks the collection is unsharded but is actually sharded.
+//
+// We restart a mongod to cause it to forget that a collection was sharded. When restarted, we
+// expect it to still have all the previous data.
+// @tags: [requires_persistence]
+(function() {
+ "use strict";
+
+ // Restarts the primary shard and ensures that it believes both collections are unsharded.
+ function restartPrimaryShard(rs, localColl, foreignColl) {
+ // Returns true if the shard is aware that the collection is sharded.
+ function hasRoutingInfoForNs(shardConn, coll) {
+ const res = shardConn.adminCommand({getShardVersion: coll, fullMetadata: true});
+ assert.commandWorked(res);
+ return res.metadata.collVersion != undefined;
+ }
+
+ rs.restart(0);
+ rs.awaitSecondaryNodes();
+ assert(!hasRoutingInfoForNs(rs.getPrimary(), localColl.getFullName()));
+ assert(!hasRoutingInfoForNs(rs.getPrimary(), foreignColl.getFullName()));
+ }
+
+ const testName = "lookup_stale_mongod";
+ const st = new ShardingTest({
+ shards: 2,
+ mongos: 2,
+ rs: {nodes: 1},
+ });
+
+ const mongos0DB = st.s0.getDB(testName);
+ const mongos0LocalColl = mongos0DB[testName + "_local"];
+ const mongos0ForeignColl = mongos0DB[testName + "_foreign"];
+
+ const mongos1DB = st.s1.getDB(testName);
+ const mongos1LocalColl = mongos1DB[testName + "_local"];
+ const mongos1ForeignColl = mongos1DB[testName + "_foreign"];
+
+ // Ensure that shard0 is the primary shard.
+ assert.commandWorked(mongos0DB.adminCommand({enableSharding: mongos0DB.getName()}));
+ st.ensurePrimaryShard(mongos0DB.getName(), st.shard0.shardName);
+
+ assert.writeOK(mongos0LocalColl.insert({_id: 0, a: 1}));
+ assert.writeOK(mongos0LocalColl.insert({_id: 1, a: null}));
+
+ assert.writeOK(mongos0ForeignColl.insert({_id: 0, b: 1}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 1, b: null}));
+
+ // Send writes through mongos1 such that it's aware of the collections and believes they are
+ // unsharded.
+ assert.writeOK(mongos1LocalColl.insert({_id: 2}));
+ assert.writeOK(mongos1ForeignColl.insert({_id: 2}));
+
+ const pipeline = [
+ {
+ $lookup:
+ {localField: "a", foreignField: "b", from: mongos0ForeignColl.getName(), as: "same"}
+ },
+ {$sort: {_id: 1}}
+ ];
+
+ // The results are expected to be correct if the $lookup stage is executed on the mongos which
+ // is aware that the collection is sharded.
+ const expectedResults = [
+ {_id: 0, a: 1, "same": [{_id: 0, b: 1}]},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+
+ //
+ // Test unsharded local and sharded foreign collections, with the primary shard unaware that
+ // the foreign collection is sharded.
+ //
+
+ // Shard the foreign collection.
+ assert.commandWorked(
+ mongos0DB.adminCommand({shardCollection: mongos0ForeignColl.getFullName(), key: {_id: 1}}));
+
+ // Split the collection into 2 chunks: [MinKey, 1), [1, MaxKey).
+ assert.commandWorked(
+ mongos0DB.adminCommand({split: mongos0ForeignColl.getFullName(), middle: {_id: 1}}));
+
+ // Move the [minKey, 1) chunk to shard1.
+ assert.commandWorked(mongos0DB.adminCommand({
+ moveChunk: mongos0ForeignColl.getFullName(),
+ find: {_id: 0},
+ to: st.shard1.shardName,
+ _waitForDelete: true
+ }));
+
+ // Verify $lookup results through the fresh mongos.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos0LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ // Verify $lookup results through mongos1, which is not aware that the local
+ // collection is sharded. The results are expected to be incorrect when both the mongos and
+ // primary shard incorrectly believe that a collection is unsharded.
+ // TODO: This should be fixed by SERVER-32629, likewise for the other aggregates in this file
+ // sent to the stale mongos.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), [
+ {_id: 0, a: 1, "same": []},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ]);
+
+ //
+ // Test sharded local and sharded foreign collections, with the primary shard unaware that
+ // either collection is sharded.
+ //
+
+ // Shard the local collection.
+ assert.commandWorked(
+ mongos0DB.adminCommand({shardCollection: mongos0LocalColl.getFullName(), key: {_id: 1}}));
+
+ // Split the collection into 2 chunks: [MinKey, 1), [1, MaxKey).
+ assert.commandWorked(
+ mongos0DB.adminCommand({split: mongos0LocalColl.getFullName(), middle: {_id: 1}}));
+
+ // Move the [minKey, 1) chunk to shard1.
+ assert.commandWorked(mongos0DB.adminCommand({
+ moveChunk: mongos0LocalColl.getFullName(),
+ find: {_id: 0},
+ to: st.shard1.shardName,
+ _waitForDelete: true
+ }));
+
+ // Verify $lookup results through the fresh mongos.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos0LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ // Verify $lookup results through mongos1, which is not aware that the local
+ // collection is sharded. The results are expected to be incorrect when both the mongos and
+ // primary shard incorrectly believe that a collection is unsharded.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), [
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ]);
+
+ //
+ // Test sharded local and unsharded foreign collections, with the primary shard unaware that
+ // the local collection is sharded.
+ //
+
+ // Recreate the foreign collection as unsharded.
+ mongos0ForeignColl.drop();
+ assert.writeOK(mongos0ForeignColl.insert({_id: 0, b: 1}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 1, b: null}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 2}));
+
+ // Verify $lookup results through the fresh mongos.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos0LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ // Verify $lookup results through mongos1, which is not aware that the local
+ // collection is sharded. The results are expected to be incorrect when both the mongos and
+ // primary shard incorrectly believe that a collection is unsharded.
+ restartPrimaryShard(st.rs0, mongos0LocalColl, mongos0ForeignColl);
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), [
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ]);
+
+ st.stop();
+})();
diff --git a/jstests/sharding/lookup_stale_mongos.js b/jstests/sharding/lookup_stale_mongos.js
new file mode 100644
index 00000000000..3c713733b49
--- /dev/null
+++ b/jstests/sharding/lookup_stale_mongos.js
@@ -0,0 +1,130 @@
+// Tests the behavior of a $lookup when the mongos contains stale routing information for the
+// local and/or foreign collections. This includes when mongos thinks the collection is sharded
+// when it's not, and likewise when mongos thinks the collection is unsharded but is actually
+// sharded.
+(function() {
+ "use strict";
+
+ const testName = "lookup_stale_mongos";
+ const st = new ShardingTest({
+ shards: 2,
+ mongos: 2,
+ });
+
+ const mongos0DB = st.s0.getDB(testName);
+ assert.commandWorked(mongos0DB.dropDatabase());
+ const mongos0LocalColl = mongos0DB[testName + "_local"];
+ const mongos0ForeignColl = mongos0DB[testName + "_foreign"];
+
+ const mongos1DB = st.s1.getDB(testName);
+ const mongos1LocalColl = mongos1DB[testName + "_local"];
+ const mongos1ForeignColl = mongos1DB[testName + "_foreign"];
+
+ const pipeline = [
+ {
+ $lookup:
+ {localField: "a", foreignField: "b", from: mongos1ForeignColl.getName(), as: "same"}
+ },
+ {$sort: {_id: 1}}
+ ];
+ const expectedResults = [
+ {_id: 0, a: 1, "same": [{_id: 0, b: 1}]},
+ {_id: 1, a: null, "same": [{_id: 1, b: null}, {_id: 2}]},
+ {_id: 2, "same": [{_id: 1, b: null}, {_id: 2}]}
+ ];
+
+ // Ensure that shard0 is the primary shard.
+ assert.commandWorked(mongos0DB.adminCommand({enableSharding: mongos0DB.getName()}));
+ st.ensurePrimaryShard(mongos0DB.getName(), st.shard0.shardName);
+
+ assert.writeOK(mongos0LocalColl.insert({_id: 0, a: 1}));
+ assert.writeOK(mongos0LocalColl.insert({_id: 1, a: null}));
+
+ assert.writeOK(mongos0ForeignColl.insert({_id: 0, b: 1}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 1, b: null}));
+
+ // Send writes through mongos1 such that it's aware of the collections and believes they are
+ // unsharded.
+ assert.writeOK(mongos1LocalColl.insert({_id: 2}));
+ assert.writeOK(mongos1ForeignColl.insert({_id: 2}));
+
+ //
+ // Test unsharded local and sharded foreign collections, with mongos unaware that the foreign
+ // collection is sharded.
+ //
+
+ // Shard the foreign collection through mongos0.
+ assert.commandWorked(
+ mongos0DB.adminCommand({shardCollection: mongos0ForeignColl.getFullName(), key: {_id: 1}}));
+
+ // Split the collection into 2 chunks: [MinKey, 1), [1, MaxKey).
+ assert.commandWorked(
+ mongos0DB.adminCommand({split: mongos0ForeignColl.getFullName(), middle: {_id: 1}}));
+
+ // Move the [minKey, 1) chunk to shard1.
+ assert.commandWorked(mongos0DB.adminCommand({
+ moveChunk: mongos0ForeignColl.getFullName(),
+ find: {_id: 0},
+ to: st.shard1.shardName,
+ _waitForDelete: true
+ }));
+
+ // Issue a $lookup through mongos1, which is unaware that the foreign collection is sharded.
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ //
+ // Test sharded local and sharded foreign collections, with mongos unaware that the local
+ // collection is sharded.
+ //
+
+ // Shard the local collection through mongos0.
+ assert.commandWorked(
+ mongos0DB.adminCommand({shardCollection: mongos0LocalColl.getFullName(), key: {_id: 1}}));
+
+ // Split the collection into 2 chunks: [MinKey, 1), [1, MaxKey).
+ assert.commandWorked(
+ mongos0DB.adminCommand({split: mongos0LocalColl.getFullName(), middle: {_id: 1}}));
+
+ // Move the [minKey, 1) chunk to shard1.
+ assert.commandWorked(mongos0DB.adminCommand({
+ moveChunk: mongos0LocalColl.getFullName(),
+ find: {_id: 0},
+ to: st.shard1.shardName,
+ _waitForDelete: true
+ }));
+
+ // Issue a $lookup through mongos1, which is unaware that the local collection is sharded.
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ //
+ // Test sharded local and unsharded foreign collections, with mongos unaware that the foreign
+ // collection is unsharded.
+ //
+
+ // Recreate the foreign collection as unsharded through mongos0.
+ mongos0ForeignColl.drop();
+ assert.writeOK(mongos0ForeignColl.insert({_id: 0, b: 1}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 1, b: null}));
+ assert.writeOK(mongos0ForeignColl.insert({_id: 2}));
+
+ // Issue a $lookup through mongos1, which is unaware that the foreign collection is now
+ // unsharded.
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ //
+ // Test unsharded local and foreign collections, with mongos unaware that the local
+ // collection is unsharded.
+ //
+
+ // Recreate the foreign collection as unsharded through mongos0.
+ mongos0LocalColl.drop();
+ assert.writeOK(mongos0LocalColl.insert({_id: 0, a: 1}));
+ assert.writeOK(mongos0LocalColl.insert({_id: 1, a: null}));
+ assert.writeOK(mongos0LocalColl.insert({_id: 2}));
+
+ // Issue a $lookup through mongos1, which is unaware that the local collection is now
+ // unsharded.
+ assert.eq(mongos1LocalColl.aggregate(pipeline).toArray(), expectedResults);
+
+ st.stop();
+})();