mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(mongodb): support find explain in the SQL editor
This commit is contained in:
@@ -24,6 +24,16 @@ describe("MongoDB shell statement risk", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["", '"queryPlanner"', '"executionStats"', '"allPlansExecution"'])("treats find explain(%s) as read-only without hiding batch writes", (verbosity) => {
|
||||
const explain = `db.demo.find({}).explain(${verbosity})`;
|
||||
expect(classifySqlRisk(explain, { dialect: "mongodb" }).risk).toBe("read");
|
||||
for (const sql of [`${explain}; db.demo.insertOne({active: true})`, `db.demo.insertOne({active: true}); ${explain}`]) {
|
||||
const assessment = classifySqlRisk(sql, { dialect: "mongodb" });
|
||||
expect(assessment.risk).toBe("write");
|
||||
expect(assessment.statements.map((statement) => statement.risk)).toEqual(sql.startsWith(explain) ? ["read", "write"] : ["write", "read"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains per-command assessments for shell batches", () => {
|
||||
const assessment = classifySqlRisk('db.getCollection("demo").find({}); db.getCollection("demo").insertOne({"active": true})', { dialect: "mongodb" });
|
||||
expect(assessment.risk).toBe("write");
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ invoke: vi.fn() }));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({ invoke: mocks.invoke }));
|
||||
vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn() }));
|
||||
|
||||
const options = { skip: 2, limit: 10, filter: '{"active":true}', projection: '{"name":1}', sort: '{"name":1}', collation: '{"locale":"en"}', verbosity: "executionStats" };
|
||||
const plan = { queryPlanner: { namespace: "app.users" } };
|
||||
|
||||
describe("MongoDB find explain adapters", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it.each(["explain-1", undefined])("forwards optional executionId %s through HTTP", async (executionId) => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(plan) });
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const { mongoExplainFind } = await import("@/lib/backend/http");
|
||||
|
||||
const result = executionId === undefined ? await mongoExplainFind("mongo-1", "app", "users", options) : await mongoExplainFind("mongo-1", "app", "users", options, executionId);
|
||||
|
||||
expect(result).toEqual(plan);
|
||||
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
|
||||
expect(url).toBe("/api/mongo/explain-find");
|
||||
expect(JSON.parse(String(init.body))).toEqual({ connectionId: "mongo-1", database: "app", collection: "users", ...options, ...(executionId === undefined ? {} : { executionId }) });
|
||||
});
|
||||
|
||||
it.each(["explain-1", undefined])("forwards optional executionId %s through Tauri", async (executionId) => {
|
||||
mocks.invoke.mockResolvedValue(plan);
|
||||
const { mongoExplainFind } = await import("@/lib/backend/tauri");
|
||||
|
||||
const result = executionId === undefined ? await mongoExplainFind("mongo-1", "app", "users", options) : await mongoExplainFind("mongo-1", "app", "users", options, executionId);
|
||||
|
||||
expect(result).toEqual(plan);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mongo_explain_find", { connectionId: "mongo-1", database: "app", collection: "users", ...options, executionId });
|
||||
});
|
||||
});
|
||||
@@ -825,6 +825,7 @@ export const mongoUpdateDocument = forward("mongoUpdateDocument");
|
||||
export const mongoUpdateDocuments = forward("mongoUpdateDocuments");
|
||||
export const mongoReplaceDocument = forward("mongoReplaceDocument");
|
||||
export const mongoBulkWrite = forward("mongoBulkWrite");
|
||||
export const mongoExplainFind = forward("mongoExplainFind");
|
||||
export const documentDeleteDocument = forward("documentDeleteDocument");
|
||||
export const documentSaveMeilisearchBatch = forward("documentSaveMeilisearchBatch");
|
||||
export const meilisearchSearchDocuments = forward("meilisearchSearchDocuments");
|
||||
|
||||
@@ -4676,6 +4676,22 @@ export async function documentUpdateDocument(connectionId: string, database: str
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoExplainFind(connectionId: string, database: string, collection: string, options: { skip: number; limit: number; filter?: string; projection?: string; sort?: string; collation?: string; verbosity?: string }, executionId?: string): Promise<unknown> {
|
||||
return post("/api/mongo/explain-find", {
|
||||
connectionId,
|
||||
database,
|
||||
collection,
|
||||
skip: options.skip,
|
||||
limit: options.limit,
|
||||
filter: options.filter,
|
||||
projection: options.projection,
|
||||
sort: options.sort,
|
||||
collation: options.collation,
|
||||
verbosity: options.verbosity,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoBulkWrite(connectionId: string, database: string, collection: string, operationsJson: string, optionsJson?: string): Promise<MongoBulkWriteResult> {
|
||||
return post("/api/mongo/bulk-write", {
|
||||
connectionId,
|
||||
|
||||
@@ -4446,6 +4446,22 @@ export async function documentUpdateDocument(connectionId: string, database: str
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoExplainFind(connectionId: string, database: string, collection: string, options: { skip: number; limit: number; filter?: string; projection?: string; sort?: string; collation?: string; verbosity?: string }, executionId?: string): Promise<unknown> {
|
||||
return invoke<unknown>("mongo_explain_find", {
|
||||
connectionId,
|
||||
database,
|
||||
collection,
|
||||
skip: options.skip,
|
||||
limit: options.limit,
|
||||
filter: options.filter,
|
||||
projection: options.projection,
|
||||
sort: options.sort,
|
||||
collation: options.collation,
|
||||
verbosity: options.verbosity,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoBulkWrite(connectionId: string, database: string, collection: string, operationsJson: string, optionsJson?: string): Promise<MongoBulkWriteResult> {
|
||||
return invoke<MongoBulkWriteResult>("mongo_bulk_write", {
|
||||
connectionId,
|
||||
|
||||
@@ -117,6 +117,27 @@ describe("read-only write unlock", () => {
|
||||
expect(connectionIsEffectivelyReadOnly(connection)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["", '"queryPlanner"', '"executionStats"', '"allPlansExecution"'])("does not request write unlock for MongoDB find explain(%s)", async (verbosity) => {
|
||||
const store = useReadOnlyUnlockStore();
|
||||
const connection = { id: "mongo", read_only: true, db_type: "mongodb" as const };
|
||||
const pending = ensureReadOnlyWriteAccess({ connection, sql: `db.demo.find({}).explain(${verbosity})` });
|
||||
|
||||
expect(connectionWriteUnlockState).not.toHaveBeenCalled();
|
||||
expect(store.pending).toBeUndefined();
|
||||
await expect(pending).resolves.toBe(true);
|
||||
expect(unlockConnectionWrites).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still requests write unlock for MongoDB explain mixed with writes", async () => {
|
||||
const connection = { id: "mongo", read_only: true, db_type: "mongodb" as const };
|
||||
const pending = ensureReadOnlyWriteAccess({ connection, sql: "db.demo.find({}).explain(); db.demo.insertOne({active: true})" });
|
||||
const store = await waitForPending("mongo");
|
||||
|
||||
store.cancel();
|
||||
await expect(pending).resolves.toBe(false);
|
||||
expect(unlockConnectionWrites).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns to effectively read-only when the local timer expires", async () => {
|
||||
vi.useFakeTimers();
|
||||
const store = useReadOnlyUnlockStore();
|
||||
|
||||
@@ -153,6 +153,7 @@ const CURSOR_METHODS = [
|
||||
{ label: "sort", detail: "Sort cursor results", apply: "sort({ ${field}: 1 })" },
|
||||
{ label: "limit", detail: "Limit cursor results", apply: "limit(100)" },
|
||||
{ label: "skip", detail: "Skip cursor results", apply: "skip(0)" },
|
||||
{ label: "explain", detail: "Show the query plan instead of the results", apply: 'explain("executionStats")' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -219,6 +219,13 @@ export interface MongoFindCommand {
|
||||
collation?: string;
|
||||
}
|
||||
|
||||
export interface MongoFindExplainCommand extends MongoFindCommand {
|
||||
verbosity: MongoExplainVerbosity;
|
||||
}
|
||||
|
||||
export type MongoExplainVerbosity = "queryPlanner" | "executionStats" | "allPlansExecution";
|
||||
const EXPLAIN_VERBOSITIES: readonly MongoExplainVerbosity[] = ["queryPlanner", "executionStats", "allPlansExecution"];
|
||||
|
||||
export interface MongoFindPaginationPlan {
|
||||
pageOffset: number;
|
||||
pageLimit: number;
|
||||
@@ -284,6 +291,7 @@ type MongoWriteKind = "runCommand" | "insert" | "update" | "replace" | "bulkWrit
|
||||
|
||||
export type MongoCommand =
|
||||
| ({ kind: "find" } & MongoFindCommand)
|
||||
| ({ kind: "findExplain" } & MongoFindExplainCommand)
|
||||
| ({ kind: "findOne" } & MongoFindOneCommand)
|
||||
| MongoVersionCommand
|
||||
| MongoShowDatabasesCommand
|
||||
@@ -594,6 +602,35 @@ function parseCollectionCountCommand(source: string, method: "countDocuments" |
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `find(...)…explain([verbosity])`: the query plan for a find, with explain as the
|
||||
* final chain call. Everything before it is parsed exactly as the find would be.
|
||||
*/
|
||||
export function parseMongoFindExplainCommand(input: string): MongoFindExplainCommand | null {
|
||||
const source = input.trim().replace(/;$/, "").trim();
|
||||
const target = parseFindTarget(source);
|
||||
if (!target) return null;
|
||||
const findOpenIndex = source.indexOf("(", target.findCallIndex);
|
||||
const findCloseIndex = findMatchingParen(source, findOpenIndex);
|
||||
if (findCloseIndex < 0) return null;
|
||||
|
||||
const chain = source.slice(findCloseIndex + 1).trim();
|
||||
const calls = listChainedCalls(chain);
|
||||
const last = calls?.[calls.length - 1];
|
||||
if (!calls || last?.name !== "explain") return null;
|
||||
|
||||
const verbosityArg = last.args.trim();
|
||||
let verbosity: MongoExplainVerbosity = "queryPlanner";
|
||||
if (verbosityArg) {
|
||||
const literal = /^(["'])([^"']*)\1$/.exec(verbosityArg)?.[2];
|
||||
if (!literal || !EXPLAIN_VERBOSITIES.includes(literal as MongoExplainVerbosity)) return null;
|
||||
verbosity = literal as MongoExplainVerbosity;
|
||||
}
|
||||
|
||||
const find = parseMongoFindCommand(source.slice(0, findCloseIndex + 1) + chain.slice(0, last.index));
|
||||
return find ? { ...find, verbosity } : null;
|
||||
}
|
||||
|
||||
function parseFindCountCommand(source: string): MongoCountDocumentsCommand | null {
|
||||
const target = parseFindTarget(source);
|
||||
if (!target) return null;
|
||||
@@ -888,6 +925,10 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null {
|
||||
const count = parseMongoCountDocumentsCommand(source);
|
||||
return count ? { kind: "countDocuments", ...count } : null;
|
||||
},
|
||||
(source) => {
|
||||
const explain = parseMongoFindExplainCommand(source);
|
||||
return explain ? { kind: "findExplain", ...explain } : null;
|
||||
},
|
||||
(source) => {
|
||||
const find = parseMongoFindCommand(source);
|
||||
return find ? { kind: "find", ...find } : null;
|
||||
@@ -1606,10 +1647,13 @@ function describeUnsupportedFindChain(chain: string): string | null {
|
||||
if (!calls) return null;
|
||||
const offending = calls.find((call) => !FIND_CHAIN_METHODS.has(call.name) || (isNoopCursorMethod(call.name) && call.args.trim()));
|
||||
if (!offending) return null;
|
||||
const supported = "Supported after find(): sort, skip, limit, collation, count, toArray, pretty.";
|
||||
const supported = "Supported after find(): sort, skip, limit, collation, count, explain, toArray, pretty.";
|
||||
switch (offending.name) {
|
||||
case "explain":
|
||||
return `find().explain() is not supported in the editor yet; aggregate([...], { explain: true }) is. ${supported}`;
|
||||
case "explain": {
|
||||
const verbosity = offending.args.trim();
|
||||
if (calls[calls.length - 1] !== offending) return "find().explain() must be the final chain method.";
|
||||
return `find().explain() verbosity must be "queryPlanner", "executionStats" or "allPlansExecution"${verbosity ? `, got ${verbosity}` : ""}.`;
|
||||
}
|
||||
case "itcount":
|
||||
case "size":
|
||||
return `find().${offending.name}() is not supported; use find().count() or countDocuments() to count results. ${supported}`;
|
||||
|
||||
@@ -92,7 +92,7 @@ function searchEngineAssessment(sql: string, dialect: DatabaseType | string | un
|
||||
return { risk: risk === "read" ? "read" : risk === "write" ? "write" : "ddl", firstKeyword: "rest" };
|
||||
}
|
||||
|
||||
const MONGO_READ_KINDS = new Set<MongoCommand["kind"]>(["find", "findOne", "countDocuments", "distinct", "getIndexes", "collectionStats", "version", "showDatabases", "use"]);
|
||||
const MONGO_READ_KINDS = new Set<MongoCommand["kind"]>(["find", "findExplain", "findOne", "countDocuments", "distinct", "getIndexes", "collectionStats", "version", "showDatabases", "use"]);
|
||||
const MONGO_DDL_KINDS = new Set<MongoCommand["kind"]>(["createIndex", "dropIndex", "dropIndexes", "dropCollection", "createUser"]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ const mocks = vi.hoisted(() => ({
|
||||
ensureConnected: vi.fn(),
|
||||
getConnectionConfig: vi.fn(),
|
||||
mongoFindDocuments: vi.fn(),
|
||||
mongoExplainFind: vi.fn(),
|
||||
cancelQuery: vi.fn(),
|
||||
mongoParseShellCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -14,6 +16,8 @@ vi.mock("@/lib/backend/api", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
mongoFindDocuments: mocks.mongoFindDocuments,
|
||||
mongoExplainFind: mocks.mongoExplainFind,
|
||||
cancelQuery: mocks.cancelQuery,
|
||||
mongoParseShellCommand: mocks.mongoParseShellCommand,
|
||||
};
|
||||
});
|
||||
@@ -118,4 +122,35 @@ describe("queryStore MongoDB execution summary", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards the active execution ID to a pending explain and its cancellation", async () => {
|
||||
let rejectExplain!: (reason: Error) => void;
|
||||
mocks.mongoExplainFind.mockImplementation(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectExplain = reject;
|
||||
}),
|
||||
);
|
||||
mocks.cancelQuery.mockImplementation(async () => {
|
||||
rejectExplain(new Error("Query canceled"));
|
||||
return true;
|
||||
});
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mongo-1", "app", "Query");
|
||||
const execution = store.executeTabSql(tabId, 'db.users.find({}).explain("executionStats")');
|
||||
|
||||
await vi.waitFor(() => expect(mocks.mongoExplainFind).toHaveBeenCalledOnce());
|
||||
const executionId = store.tabs.find((tab) => tab.id === tabId)!.executionId;
|
||||
try {
|
||||
expect(executionId).toEqual(expect.any(String));
|
||||
expect(mocks.mongoExplainFind).toHaveBeenCalledWith("mongo-1", "app", "users", expect.objectContaining({ verbosity: "executionStats" }), executionId);
|
||||
await expect(store.cancelTabExecution(tabId)).resolves.toBe(true);
|
||||
expect(mocks.cancelQuery).toHaveBeenCalledWith(executionId);
|
||||
} finally {
|
||||
rejectExplain(new Error("Query canceled"));
|
||||
await execution;
|
||||
}
|
||||
expect(store.tabs.find((tab) => tab.id === tabId)).toMatchObject({ isExecuting: false, executionId: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6595,6 +6595,28 @@ export const useQueryStore = defineStore("query", () => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "findExplain": {
|
||||
queryExecutionLog("info", "mongo-explain:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
|
||||
const plan = await api.mongoExplainFind(
|
||||
executionConnectionId,
|
||||
currentDatabase,
|
||||
mongoCommand.collection,
|
||||
{
|
||||
skip: mongoCommand.skip,
|
||||
limit: mongoCommand.limit,
|
||||
filter: mongoCommand.filter,
|
||||
projection: mongoCommand.projection,
|
||||
sort: mongoCommand.sort,
|
||||
collation: mongoCommand.collation,
|
||||
verbosity: mongoCommand.verbosity,
|
||||
},
|
||||
executionId,
|
||||
);
|
||||
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult([plan], performance.now() - commandStartedAt, 1))));
|
||||
mongoEditTarget = undefined;
|
||||
queryExecutionLog("info", "mongo-explain:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, elapsed: elapsed() });
|
||||
break;
|
||||
}
|
||||
case "aggregate": {
|
||||
if (options?.mongoSafety) {
|
||||
const safety = evaluateMongoAggregateSafety(mongoCommand, options.mongoSafety);
|
||||
|
||||
+3
-3
@@ -137,7 +137,7 @@ test("prioritizes common read helpers and keeps destructive helpers last", () =>
|
||||
assert.deepEqual(methodLabels.slice(0, 6), ["find", "findOne", "aggregate", "countDocuments", "estimatedDocumentCount", "distinct"]);
|
||||
assert.deepEqual(getCollectionMethodLabels.slice(0, 6), ["find", "findOne", "aggregate", "countDocuments", "estimatedDocumentCount", "distinct"]);
|
||||
assert.deepEqual(methodLabels.slice(-3), ["dropIndex", "dropIndexes", "drop"]);
|
||||
assert.deepEqual(labels("db.users.find({})."), ["limit", "sort", "skip", "count"]);
|
||||
assert.deepEqual(labels("db.users.find({})."), ["limit", "sort", "skip", "count", "explain"]);
|
||||
});
|
||||
|
||||
test("keeps dotted collection names ahead of methods until the collection is resolved", () => {
|
||||
@@ -162,7 +162,7 @@ test("suggests cursor methods after find result chains", () => {
|
||||
|
||||
assert.deepEqual(
|
||||
allItems.map((item) => item.label),
|
||||
["limit", "sort", "skip", "count"],
|
||||
["limit", "sort", "skip", "count", "explain"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
prefixedItems.map((item) => item.label),
|
||||
@@ -170,7 +170,7 @@ test("suggests cursor methods after find result chains", () => {
|
||||
);
|
||||
assert.deepEqual(
|
||||
formattedChainItems.map((item) => item.label),
|
||||
["limit", "sort", "skip", "count"],
|
||||
["limit", "sort", "skip", "count", "explain"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
formattedPrefixedItems.map((item) => item.label),
|
||||
|
||||
+33
-2
@@ -6,6 +6,7 @@ import {
|
||||
evaluateMongoWriteSafety,
|
||||
mongoAggregateWriteStage,
|
||||
listChainedCalls,
|
||||
parseMongoFindExplainCommand,
|
||||
mongoBulkWriteToQueryResult,
|
||||
mongoCollectionStatsToQueryResult,
|
||||
mongoCountToQueryResult,
|
||||
@@ -448,13 +449,43 @@ test("parseMongoFindCommand validates UUID strings at parse time", () => {
|
||||
assert.deepEqual(JSON.parse(command.filter), { u: { $uuid: "3B241101-E2BB-4255-8CAF-4136C566A962" } });
|
||||
});
|
||||
|
||||
test("parseMongoFindExplainCommand reads a final explain() with the find it wraps", () => {
|
||||
assert.deepEqual(parseMongoFindExplainCommand('db.c.find({a: 1}, {b: 1}).sort({b: -1}).skip(2).limit(5).explain("allPlansExecution")'), {
|
||||
collection: "c",
|
||||
filter: '{"a": 1}',
|
||||
projection: '{"b": 1}',
|
||||
skip: 2,
|
||||
limit: 5,
|
||||
sort: '{"b": -1}',
|
||||
verbosity: "allPlansExecution",
|
||||
});
|
||||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1}).explain()")?.verbosity, "queryPlanner");
|
||||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1})\n .sort({b: 1})\n .explain('executionStats');")?.verbosity, "executionStats");
|
||||
assert.equal(parseMongoFindExplainCommand('db["x-y"].find({}).explain()')?.collection, "x-y");
|
||||
assert.equal(parseMongoCommand("db.c.find({}).explain()")?.command.kind, "findExplain");
|
||||
// A plain find is not an explain, and vice versa.
|
||||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1}).limit(1)"), null);
|
||||
assert.equal(parseMongoFindCommand("db.c.find({a: 1}).explain()"), null);
|
||||
});
|
||||
|
||||
test("parseMongoFindExplainCommand rejects a misplaced explain or an unknown verbosity", () => {
|
||||
for (const [source, expected] of [
|
||||
["db.c.find({}).explain('verbose')", /verbosity must be "queryPlanner", "executionStats" or "allPlansExecution", got 'verbose'/],
|
||||
["db.c.find({}).explain(1)", /verbosity must be .*, got 1/],
|
||||
["db.c.find({}).explain().limit(5)", /explain\(\) must be the final chain method/],
|
||||
["db.c.find({}).explain", /Unexpected text after find\(\.\.\.\): "\.explain"/],
|
||||
] as const) {
|
||||
assert.equal(parseMongoCommand(source), null, source);
|
||||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseMongoFindCommand rejects chained methods it would otherwise silently drop", () => {
|
||||
// Each of these used to parse as a plain find and run a different query than written.
|
||||
for (const [source, expected] of [
|
||||
["db.c.find({}).hint({a: 1})", /find\(\)\.hint\(\) is not supported yet\. Supported after find\(\): sort, skip, limit, collation, count, toArray, pretty\./],
|
||||
["db.c.find({}).hint({a: 1})", /find\(\)\.hint\(\) is not supported yet\. Supported after find\(\): sort, skip, limit, collation, count, explain, toArray, pretty\./],
|
||||
["db.c.find({}).batchSize(10)", /batchSize\(\) is not supported yet/],
|
||||
["db.c.find({}).sort({a: 1}).maxTimeMS(100)", /maxTimeMS\(\) is not supported yet/],
|
||||
["db.c.find({}).explain('executionStats')", /find\(\)\.explain\(\) is not supported in the editor yet; aggregate\(\[\.\.\.\], \{ explain: true \}\) is/],
|
||||
["db.c.find({}).itcount()", /itcount\(\) is not supported; use find\(\)\.count\(\) or countDocuments\(\)/],
|
||||
["db.c.find({}).size()", /size\(\) is not supported; use find\(\)\.count\(\)/],
|
||||
["db.c.find({}).forEach(d => print(d))", /forEach\(\) runs JavaScript, which the editor does not execute/],
|
||||
|
||||
@@ -144,6 +144,47 @@ pub async fn mongo_find_documents(
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn mongo_explain_find(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
skip: u64,
|
||||
limit: i64,
|
||||
filter: Option<String>,
|
||||
projection: Option<String>,
|
||||
sort: Option<String>,
|
||||
collation: Option<String>,
|
||||
verbosity: Option<String>,
|
||||
execution_id: Option<String>,
|
||||
mcp_request: Option<bool>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let app = state.inner().clone();
|
||||
if mcp_request == Some(true) {
|
||||
crate::commands::mcp_bridge::ensure_mcp_read_allowed_by_id(&app, &connection_id, &database).await?;
|
||||
}
|
||||
run_cancellable(
|
||||
&app,
|
||||
execution_id,
|
||||
dbx_core::mongo_ops::mongo_explain_find_core(
|
||||
&app,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
skip,
|
||||
limit,
|
||||
filter.as_deref(),
|
||||
projection.as_deref(),
|
||||
sort.as_deref(),
|
||||
collation.as_deref(),
|
||||
verbosity.as_deref().unwrap_or("queryPlanner"),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn mongo_find_one(
|
||||
|
||||
@@ -2325,6 +2325,7 @@ pub fn run() {
|
||||
commands::mongo_cmd::mongo_find_documents,
|
||||
commands::mongo_cmd::mongo_parse_shell_command,
|
||||
commands::mongo_cmd::mongo_find_one,
|
||||
commands::mongo_cmd::mongo_explain_find,
|
||||
commands::mongo_cmd::mongo_count_documents,
|
||||
commands::mongo_cmd::mongo_server_version,
|
||||
commands::mongo_cmd::mongo_collection_stats,
|
||||
|
||||
Reference in New Issue
Block a user