416 lines
15 KiB
JavaScript
416 lines
15 KiB
JavaScript
// SERVER-19095: Add $lookup aggregation stage.
|
|
|
|
// For assertErrorCode.
|
|
load("jstests/aggregation/extras/utils.js");
|
|
|
|
(function() {
|
|
"use strict";
|
|
|
|
// 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));
|
|
}
|
|
|
|
function runTest(coll, from) {
|
|
var db = null; // Using the db variable is banned in this function.
|
|
|
|
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.
|
|
var 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);
|
|
|
|
//
|
|
// Dotted field paths.
|
|
//
|
|
|
|
coll.drop();
|
|
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.drop();
|
|
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.
|
|
var 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.drop();
|
|
assert.writeOK(coll.insert({_id: 0, a: {b: 1}}));
|
|
assert.writeOK(coll.insert({_id: 1}));
|
|
|
|
from.drop();
|
|
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.drop();
|
|
assert.writeOK(coll.insert({_id: 0, a: /a regex/}));
|
|
|
|
from.drop();
|
|
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.drop();
|
|
assert.writeOK(coll.insert({_id: 0, a: [0, 1, 2]}));
|
|
|
|
from.drop();
|
|
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);
|
|
|
|
// Array containing regular expressions.
|
|
coll.drop();
|
|
assert.writeOK(coll.insert({_id: 0, a: [/a regex/, /^x/]}));
|
|
|
|
from.drop();
|
|
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/}]}];
|
|
testPipeline(pipeline, expectedResults, coll);
|
|
|
|
//
|
|
// Error cases.
|
|
//
|
|
|
|
// All four fields must be specified.
|
|
assertErrorCode(coll, [{$lookup: {foreignField: "b", from: "from", as: "same"}}], 4572);
|
|
assertErrorCode(coll, [{$lookup: {localField: "a", from: "from", as: "same"}}], 4572);
|
|
assertErrorCode(coll, [{$lookup: {localField: "a", foreignField: "b", as: "same"}}], 4572);
|
|
assertErrorCode(
|
|
coll, [{$lookup: {localField: "a", foreignField: "b", from: "from"}}], 4572);
|
|
|
|
// All four field's values must be strings.
|
|
assertErrorCode(
|
|
coll, [{$lookup: {localField: 1, foreignField: "b", from: "from", as: "as"}}], 4570);
|
|
assertErrorCode(
|
|
coll, [{$lookup: {localField: "a", foreignField: 1, from: "from", as: "as"}}], 4570);
|
|
assertErrorCode(
|
|
coll, [{$lookup: {localField: "a", foreignField: "b", from: 1, as: "as"}}], 4570);
|
|
assertErrorCode(
|
|
coll, [{$lookup: {localField: "a", foreignField: "b", from: "from", as: 1}}], 4570);
|
|
|
|
// $lookup's field must be an object.
|
|
assertErrorCode(coll, [{$lookup: "string"}], 4569);
|
|
}
|
|
|
|
// Run tests on single node.
|
|
db.lookUp.drop();
|
|
db.from.drop();
|
|
runTest(db.lookUp, db.from);
|
|
|
|
// Run tests in a sharded environment.
|
|
var sharded = new ShardingTest({shards: 2, mongos: 1});
|
|
assert(sharded.adminCommand({enableSharding: "test"}));
|
|
sharded.getDB('test').lookUp.drop();
|
|
sharded.getDB('test').from.drop();
|
|
assert(sharded.adminCommand({shardCollection: "test.lookUp", key: {_id: 'hashed'}}));
|
|
runTest(sharded.getDB('test').lookUp, sharded.getDB('test').from);
|
|
|
|
// An error is thrown if the from collection is sharded.
|
|
assert(sharded.adminCommand({shardCollection: "test.from", key: {_id: 1}}));
|
|
assertErrorCode(sharded.getDB('test').lookUp,
|
|
[{$lookup: {localField: "a", foreignField: "b", from: "from", as: "same"}}],
|
|
28769);
|
|
sharded.stop();
|
|
}());
|