Skip to content

Commit 3bc4725

Browse files
committed
chore: add explain tests
1 parent ed14781 commit 3bc4725

File tree

2 files changed

+202
-7
lines changed

2 files changed

+202
-7
lines changed

src/tools/mongodb/metadata/explain.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,31 @@ export class ExplainTool extends MongoDBToolBase {
4747
const method = methods[0];
4848

4949
if (!method) {
50-
throw new Error("No method provided");
50+
return {
51+
content: [
52+
{
53+
text: "No method provided. Expected one of the following: `aggregate`, `find`, or `count`",
54+
type: "text",
55+
},
56+
],
57+
};
5158
}
5259

5360
let result: Document;
5461
switch (method.name) {
5562
case "aggregate": {
5663
const { pipeline } = method.arguments;
57-
result = await provider.aggregate(database, collection, pipeline).explain(ExplainTool.defaultVerbosity);
64+
result = await provider
65+
.aggregate(
66+
database,
67+
collection,
68+
pipeline,
69+
{},
70+
{
71+
writeConcern: undefined,
72+
}
73+
)
74+
.explain(ExplainTool.defaultVerbosity);
5875
break;
5976
}
6077
case "find": {
@@ -66,18 +83,21 @@ export class ExplainTool extends MongoDBToolBase {
6683
}
6784
case "count": {
6885
const { query } = method.arguments;
69-
// This helper doesn't have explain() command but does have the argument explain
70-
result = (await provider.count(database, collection, query, {
71-
explain: ExplainTool.defaultVerbosity,
72-
})) as unknown as Document;
86+
result = await provider.mongoClient.db(database).command({
87+
explain: {
88+
count: collection,
89+
query,
90+
},
91+
verbosity: ExplainTool.defaultVerbosity,
92+
});
7393
break;
7494
}
7595
}
7696

7797
return {
7898
content: [
7999
{
80-
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in \`${database}.${collection}\`. This information can be used to understand how the query was executed and to optimize the query performance.`,
100+
text: `Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in "${database}.${collection}". This information can be used to understand how the query was executed and to optimize the query performance.`,
81101
type: "text",
82102
},
83103
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {
2+
databaseCollectionParameters,
3+
setupIntegrationTest,
4+
validateToolMetadata,
5+
validateAutoConnectBehavior,
6+
validateThrowsForInvalidArguments,
7+
getResponseContent,
8+
getResponseElements,
9+
} from "../../../helpers.js";
10+
11+
describe("explain tool", () => {
12+
const integration = setupIntegrationTest();
13+
14+
validateToolMetadata(
15+
integration,
16+
"explain",
17+
"Returns statistics describing the execution of the winning plan chosen by the query optimizer for the evaluated method",
18+
[
19+
...databaseCollectionParameters,
20+
21+
{
22+
name: "method",
23+
description: "The method and its arguments to run",
24+
type: "array",
25+
required: true,
26+
},
27+
]
28+
);
29+
30+
validateThrowsForInvalidArguments(integration, "explain", [
31+
{},
32+
{ database: 123, collection: "bar", method: [{ name: "find", arguments: {} }] },
33+
{ database: "test", collection: true, method: [{ name: "find", arguments: {} }] },
34+
{ database: "test", collection: "bar", method: [{ name: "dnif", arguments: {} }] },
35+
{ database: "test", collection: "bar", method: "find" },
36+
{ database: "test", collection: "bar", method: { name: "find", arguments: {} } },
37+
]);
38+
39+
const testCases = [
40+
{
41+
method: "aggregate",
42+
arguments: { pipeline: [{ $match: { name: "Peter" } }] },
43+
},
44+
{
45+
method: "find",
46+
arguments: { filter: { name: "Peter" } },
47+
},
48+
{
49+
method: "count",
50+
arguments: {
51+
query: { name: "Peter" },
52+
},
53+
},
54+
];
55+
56+
for (const testType of ["database", "collection"] as const) {
57+
describe(`with non-existing ${testType}`, () => {
58+
for (const testCase of testCases) {
59+
it(`should return the explain plan for ${testCase.method}`, async () => {
60+
if (testType === "database") {
61+
const { databases } = await integration.mongoClient().db("").admin().listDatabases();
62+
expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
63+
} else if (testType === "collection") {
64+
await integration
65+
.mongoClient()
66+
.db(integration.randomDbName())
67+
.createCollection("some-collection");
68+
69+
const collections = await integration
70+
.mongoClient()
71+
.db(integration.randomDbName())
72+
.listCollections()
73+
.toArray();
74+
75+
expect(collections.find((collection) => collection.name === "coll1")).toBeUndefined();
76+
}
77+
78+
await integration.connectMcpClient();
79+
80+
const response = await integration.mcpClient().callTool({
81+
name: "explain",
82+
arguments: {
83+
database: integration.randomDbName(),
84+
collection: "coll1",
85+
method: [
86+
{
87+
name: testCase.method,
88+
arguments: testCase.arguments,
89+
},
90+
],
91+
},
92+
});
93+
94+
const content = getResponseElements(response.content);
95+
expect(content).toHaveLength(2);
96+
expect(content[0].text).toEqual(
97+
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.coll1". This information can be used to understand how the query was executed and to optimize the query performance.`
98+
);
99+
100+
expect(content[1].text).toContain("queryPlanner");
101+
expect(content[1].text).toContain("winningPlan");
102+
});
103+
}
104+
});
105+
}
106+
107+
describe("with existing database and collection", () => {
108+
for (const indexed of [true, false] as const) {
109+
describe(`with ${indexed ? "an index" : "no index"}`, () => {
110+
beforeEach(async () => {
111+
await integration
112+
.mongoClient()
113+
.db(integration.randomDbName())
114+
.collection("people")
115+
.insertMany([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }]);
116+
117+
if (indexed) {
118+
await integration
119+
.mongoClient()
120+
.db(integration.randomDbName())
121+
.collection("people")
122+
.createIndex({ name: 1 });
123+
}
124+
});
125+
126+
for (const testCase of testCases) {
127+
it(`should return the explain plan for ${testCase.method}`, async () => {
128+
await integration.connectMcpClient();
129+
130+
const response = await integration.mcpClient().callTool({
131+
name: "explain",
132+
arguments: {
133+
database: integration.randomDbName(),
134+
collection: "people",
135+
method: [
136+
{
137+
name: testCase.method,
138+
arguments: testCase.arguments,
139+
},
140+
],
141+
},
142+
});
143+
144+
const content = getResponseElements(response.content);
145+
expect(content).toHaveLength(2);
146+
expect(content[0].text).toEqual(
147+
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${testCase.method}\` operation in "${integration.randomDbName()}.people". This information can be used to understand how the query was executed and to optimize the query performance.`
148+
);
149+
150+
expect(content[1].text).toContain("queryPlanner");
151+
expect(content[1].text).toContain("winningPlan");
152+
153+
if (indexed) {
154+
if (testCase.method === "count") {
155+
expect(content[1].text).toContain("COUNT_SCAN");
156+
} else {
157+
expect(content[1].text).toContain("IXSCAN");
158+
}
159+
expect(content[1].text).toContain("name_1");
160+
} else {
161+
expect(content[1].text).toContain("COLLSCAN");
162+
}
163+
});
164+
}
165+
});
166+
}
167+
});
168+
169+
validateAutoConnectBehavior(integration, "explain", () => {
170+
return {
171+
args: { database: integration.randomDbName(), collection: "coll1", method: [] },
172+
expectedResponse: "No method provided. Expected one of the following: `aggregate`, `find`, or `count`",
173+
};
174+
});
175+
});

0 commit comments

Comments
 (0)