426 lines
11 KiB
JavaScript
426 lines
11 KiB
JavaScript
// In MongoDB 3.4, $graphLookup was introduced. In this file, we test the error cases.
|
|
//
|
|
// @tags: [
|
|
// not_allowed_with_signed_security_token,
|
|
// requires_fcv_82,
|
|
// # This test sets a server parameter via setParameterOnAllNonConfigNodes. To keep the host list
|
|
// # consistent, no add/remove shard operations should occur during the test.
|
|
// assumes_stable_shard_list,
|
|
// ]
|
|
import "jstests/libs/query/sbe_assert_error_override.js";
|
|
|
|
import {assertErrorCode} from "jstests/aggregation/extras/utils.js";
|
|
import {setParameterOnAllNonConfigNodes} from "jstests/noPassthrough/libs/server_parameter_helpers.js";
|
|
|
|
function getKnob(knob) {
|
|
return assert.commandWorked(db.adminCommand({getParameter: 1, [knob]: 1}))[knob];
|
|
}
|
|
|
|
function setKnob(knob, value) {
|
|
setParameterOnAllNonConfigNodes(db.getMongo(), knob, value);
|
|
}
|
|
|
|
let local = db.local;
|
|
let foreign = db.foreign;
|
|
|
|
local.drop();
|
|
assert.commandWorked(local.insert({b: 0}));
|
|
|
|
foreign.drop();
|
|
|
|
let pipeline = {$graphLookup: 4};
|
|
assertErrorCode(local, pipeline, ErrorCodes.FailedToParse, "$graphLookup spec must be an object");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
maxDepth: "string",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40100, "maxDepth must be numeric");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
maxDepth: -1,
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40101, "maxDepth must be nonnegative");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
maxDepth: 2.3,
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40102, "maxDepth must be representable as a long long");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: -1,
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, ErrorCodes.FailedToParse, "from must be a string");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, ErrorCodes.InvalidNamespace, "from must be a valid namespace");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: 0,
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40103, "as must be a string");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "$output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 16410, "as cannot be a fieldPath");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: 0,
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40103, "connectFromField must be a string");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "$b",
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 16410, "connectFromField cannot be a fieldPath");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: 0,
|
|
connectFromField: "b",
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40103, "connectToField must be a string");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "$a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 16410, "connectToField cannot be a fieldPath");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
depthField: 0,
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40103, "depthField must be a string");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
depthField: "$depth",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 16410, "depthField cannot be a fieldPath");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: "notamatch",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40185, "restrictSearchWithMatch must be an object");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
notAField: "foo",
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, 40104, "unknown argument");
|
|
|
|
pipeline = {
|
|
$graphLookup: {from: "foreign", startWith: {$literal: 0}, connectFromField: "b", as: "output"},
|
|
};
|
|
assertErrorCode(local, pipeline, 40105, "connectToField was not specified");
|
|
|
|
pipeline = {
|
|
$graphLookup: {from: "foreign", startWith: {$literal: 0}, connectToField: "a", as: "output"},
|
|
};
|
|
assertErrorCode(local, pipeline, 40105, "connectFromField was not specified");
|
|
|
|
pipeline = {
|
|
$graphLookup: {from: "foreign", connectToField: "a", connectFromField: "b", as: "output"},
|
|
};
|
|
assertErrorCode(local, pipeline, 40105, "startWith was not specified");
|
|
|
|
pipeline = {
|
|
$graphLookup: {from: "foreign", startWith: {$literal: 0}, connectToField: "a", connectFromField: "b"},
|
|
};
|
|
assertErrorCode(local, pipeline, 40105, "as was not specified");
|
|
|
|
pipeline = {
|
|
$graphLookup: {startWith: {$literal: 0}, connectToField: "a", connectFromField: "b", as: "output"},
|
|
};
|
|
assertErrorCode(local, pipeline, ErrorCodes.FailedToParse, "from was not specified");
|
|
|
|
// restrictSearchWithMatch must be a valid match expression.
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {$not: {a: 1}},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "unable to parse match expression");
|
|
|
|
// $where and $text cannot be used inside $graphLookup.
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {$where: "3 > 2"},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "cannot use $where inside $graphLookup");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {$text: {$search: "some text"}},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "cannot use $text inside $graphLookup");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {
|
|
x: {$near: {$geometry: {type: "Point", coordinates: [0, 0]}, $maxDistance: 100}},
|
|
},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "cannot use $near inside $graphLookup");
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {
|
|
$and: [
|
|
{
|
|
x: {
|
|
$near: {
|
|
$geometry: {type: "Point", coordinates: [0, 0]},
|
|
$maxDistance: 100,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "cannot use $near inside $graphLookup at any depth");
|
|
|
|
// let foreign = db.foreign;
|
|
foreign.drop();
|
|
assert.commandWorked(foreign.insert({a: 0, x: 0}));
|
|
|
|
// Test a restrictSearchWithMatch expression that fails to parse.
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {$expr: {$eq: ["$x", "$$unbound"]}},
|
|
},
|
|
};
|
|
assert.throws(() => local.aggregate(pipeline), [], "cannot use $expr with unbound variable");
|
|
|
|
// Test a restrictSearchWithMatchExpression that throws at runtime.
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "a",
|
|
connectFromField: "b",
|
|
as: "output",
|
|
restrictSearchWithMatch: {$expr: {$divide: [1, "$x"]}},
|
|
},
|
|
};
|
|
assertErrorCode(local, pipeline, [16608, ErrorCodes.BadValue], "division by zero in $expr");
|
|
|
|
// $graphLookup can only consume at most 100MB of memory without spilling.
|
|
foreign.drop();
|
|
|
|
const string7KB = " ".repeat(7 * 1024);
|
|
const string14KB = string7KB + string7KB;
|
|
|
|
// Set memory limit to 100 KB to avoid consuming too much memory for the test.
|
|
const memoryLimitKnob = "internalDocumentSourceGraphLookupMaxMemoryBytes";
|
|
const originalMemoryLimitKnobValues = getKnob(memoryLimitKnob);
|
|
setKnob(memoryLimitKnob, 100 * 1024);
|
|
|
|
// Here, the visited set exceeds 100 KB.
|
|
let initial = [];
|
|
for (let i = 0; i < 8; i++) {
|
|
let obj = {_id: i};
|
|
obj["longString"] = string14KB;
|
|
initial.push(i);
|
|
assert.commandWorked(foreign.insertOne(obj));
|
|
}
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: initial},
|
|
connectToField: "_id",
|
|
connectFromField: "notimportant",
|
|
as: "graph",
|
|
},
|
|
};
|
|
assertErrorCode(
|
|
local,
|
|
pipeline,
|
|
ErrorCodes.QueryExceededMemoryLimitNoDiskUseAllowed,
|
|
"Exceeded memory limit and can't spill to disk",
|
|
{allowDiskUse: false},
|
|
);
|
|
|
|
// Here, the visited set should grow to approximately 90 KB, and the queue should push memory
|
|
// usage over 100KB.
|
|
foreign.drop();
|
|
|
|
for (let i = 0; i < 14; i++) {
|
|
let obj = {from: 0, to: 1};
|
|
obj["s"] = string7KB;
|
|
assert.commandWorked(foreign.insertOne(obj));
|
|
}
|
|
|
|
pipeline = {
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "from",
|
|
connectFromField: "s",
|
|
as: "out",
|
|
},
|
|
};
|
|
assertErrorCode(
|
|
local,
|
|
pipeline,
|
|
ErrorCodes.QueryExceededMemoryLimitNoDiskUseAllowed,
|
|
"Exceeded memory limit and can't spill to disk",
|
|
{allowDiskUse: false},
|
|
);
|
|
|
|
// Here, we test that the cache keeps memory usage under 100KB, and does not cause an error.
|
|
foreign.drop();
|
|
for (let i = 0; i < 13; i++) {
|
|
let obj = {from: 0, to: 1};
|
|
obj["s"] = string7KB;
|
|
assert.commandWorked(foreign.insertOne(obj));
|
|
}
|
|
|
|
let res = local
|
|
.aggregate(
|
|
{
|
|
$graphLookup: {
|
|
from: "foreign",
|
|
startWith: {$literal: 0},
|
|
connectToField: "from",
|
|
connectFromField: "to",
|
|
as: "out",
|
|
},
|
|
},
|
|
{$unwind: {path: "$out"}},
|
|
)
|
|
.toArray();
|
|
|
|
assert.eq(res.length, 13);
|
|
|
|
setKnob(memoryLimitKnob, originalMemoryLimitKnobValues);
|