// 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);