fix(mongodb): support multi-command execution
* fix mongodb multi-command execution * fix(connection): 复制连接时保持在同一组内 - 修改 `addConnection` 方法以支持指定目标组 ID - 更新 `duplicateConnection` 函数以传递目标组 ID - 添加测试用例以验证复制连接的功能 * fix: add MongoDB data transfer entry * feat(editor): SQL 编辑器选区大小写转换 Co-authored-by: staff <staff@qimaos-MacBook-Pro.local> * fix(editor): 修复SQL快捷执行导致执行全量SQL的问题 (#2384) - 将执行SQL语句的逻辑从直接请求改为通过事件发射 - 增强了代码的可扩展性和可维护性 * fix(sidebar): cache tooltip overflow measurement * fix(dialog): allow dragging window from modal overlay * fix(oracle): reuse shared pool for data tabs * fix(sidebar): reduce comment text repaint cost * fix(saved-sql): preserve folder on metadata save * fix(agent): execute transaction control SQL as statements * feat(vector): add Qdrant/Milvus collection dimension display (#2396) * fix(oracle): 修复时间字段编辑和表结构修改报错 Co-authored-by: staff <staff@qimaos-MacBook-Pro.local> * fix(agents): limit common version bumps --------- Co-authored-by: 二丫讲梵 <eryajf@163.com> Co-authored-by: zipg <geeky@foxmail.com> Co-authored-by: staff <staff@qimaos-MacBook-Pro.local> Co-authored-by: t8y2 <1156263951@qq.com> Co-authored-by: lexmin0412 <zhangle_dev@outlook.com>
This commit is contained in:
9 files changed
+861
-272
No files matched your search
@@ -598,6 +598,8 @@ function executeSqlStatementFromGutter(currentView: EditorViewType, line: { from
|
|||||||
if (!statementRange) return false;
|
if (!statementRange) return false;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
// Gutter play is always scoped to the statement/command for that line, even
|
||||||
|
// when the main editor execute action would run the full document.
|
||||||
emit("execute", statementRange.sql);
|
emit("execute", statementRange.sql);
|
||||||
currentView.focus();
|
currentView.focus();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -342,6 +342,14 @@ describe("executableStatementRanges", () => {
|
|||||||
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("DEL")]);
|
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("DEL")]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns MongoDB command ranges for newline-separated shell commands", () => {
|
||||||
|
const sql = 'use archive\ndb.users.find({ status: "open" })\n .limit(5)';
|
||||||
|
const ranges = executableStatementRanges(sql, "mongodb");
|
||||||
|
|
||||||
|
expect(rangeSqlTexts(ranges)).toEqual(["use archive", 'db.users.find({ status: "open" })\n .limit(5)']);
|
||||||
|
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("db.users.find")]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not split executable Oracle PL/SQL ranges at inner statement starts", () => {
|
it("does not split executable Oracle PL/SQL ranges at inner statement starts", () => {
|
||||||
expect(rangeSqlTexts(executableStatementRanges(oraclePlSqlFixture, "oracle"))).toEqual([oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")), "SELECT 1"]);
|
expect(rangeSqlTexts(executableStatementRanges(oraclePlSqlFixture, "oracle"))).toEqual([oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")), "SELECT 1"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,15 @@ export interface MongoVersionCommand {
|
|||||||
kind: "version";
|
kind: "version";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MongoWriteCommand =
|
type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes";
|
||||||
|
|
||||||
|
export type MongoCommand =
|
||||||
|
| ({ kind: "find" } & MongoFindCommand)
|
||||||
|
| MongoVersionCommand
|
||||||
|
| ({ kind: "countDocuments" } & MongoCountDocumentsCommand)
|
||||||
|
| ({ kind: "aggregate" } & MongoAggregateCommand)
|
||||||
|
| ({ kind: "getIndexes" } & MongoGetIndexesCommand)
|
||||||
|
| ({ kind: "use" } & MongoUseCommand)
|
||||||
| { kind: "insert"; collection: string; docsJson: string }
|
| { kind: "insert"; collection: string; docsJson: string }
|
||||||
| { kind: "update"; collection: string; filter: string; update: string; many: boolean }
|
| { kind: "update"; collection: string; filter: string; update: string; many: boolean }
|
||||||
| { kind: "delete"; collection: string; filter: string; many: boolean }
|
| { kind: "delete"; collection: string; filter: string; many: boolean }
|
||||||
@@ -39,6 +47,18 @@ export type MongoWriteCommand =
|
|||||||
| { kind: "dropIndex"; collection: string; index: string }
|
| { kind: "dropIndex"; collection: string; index: string }
|
||||||
| { kind: "dropIndexes"; collection: string; indexes?: string };
|
| { kind: "dropIndexes"; collection: string; indexes?: string };
|
||||||
|
|
||||||
|
export type MongoWriteCommand = Extract<MongoCommand, { kind: MongoWriteKind }>;
|
||||||
|
|
||||||
|
export interface ParsedMongoCommand {
|
||||||
|
text: string;
|
||||||
|
command: MongoCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedMongoCommandRange extends ParsedMongoCommand {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MongoAggregateSafetyOptions {
|
export interface MongoAggregateSafetyOptions {
|
||||||
allowWrites?: boolean;
|
allowWrites?: boolean;
|
||||||
allowDangerous?: boolean;
|
allowDangerous?: boolean;
|
||||||
@@ -241,6 +261,65 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseMongoCommand(input: string): ParsedMongoCommand | null {
|
||||||
|
const text = trimMongoOuterComments(input);
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
// Keep the more specific readers ahead of generic write parsing so the
|
||||||
|
// returned kind matches the result renderer we want to use downstream.
|
||||||
|
const parsers: Array<(source: string) => MongoCommand | null> = [
|
||||||
|
(source) => {
|
||||||
|
const find = parseMongoFindCommand(source);
|
||||||
|
return find ? { kind: "find", ...find } : null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const version = parseMongoVersionCommand(source);
|
||||||
|
return version ?? null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const count = parseMongoCountDocumentsCommand(source);
|
||||||
|
return count ? { kind: "countDocuments", ...count } : null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const aggregate = parseMongoAggregateCommand(source);
|
||||||
|
return aggregate ? { kind: "aggregate", ...aggregate } : null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const getIndexes = parseMongoGetIndexesCommand(source);
|
||||||
|
return getIndexes ? { kind: "getIndexes", ...getIndexes } : null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const write = parseMongoWriteCommand(source);
|
||||||
|
return write ?? null;
|
||||||
|
},
|
||||||
|
(source) => {
|
||||||
|
const use = parseMongoUseCommand(source);
|
||||||
|
return use ? { kind: "use", ...use } : null;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const parse of parsers) {
|
||||||
|
const command = parse(text);
|
||||||
|
if (command) return { text, command };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMongoCommands(input: string): ParsedMongoCommand[] {
|
||||||
|
return splitMongoCommandRanges(input).map(({ from: _from, to: _to, ...command }) => command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMongoCommandRanges(input: string): ParsedMongoCommandRange[] {
|
||||||
|
const commands: ParsedMongoCommandRange[] = [];
|
||||||
|
for (const segment of splitMongoCommandTextRanges(input)) {
|
||||||
|
const parsed = parseMongoCommand(segment.text);
|
||||||
|
if (!parsed) return [];
|
||||||
|
commands.push({ from: segment.from, to: segment.to, ...parsed });
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: MongoAggregateSafetyOptions): { allowed: boolean; reason?: string } {
|
export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: MongoAggregateSafetyOptions): { allowed: boolean; reason?: string } {
|
||||||
if (!options.allowWrites) {
|
if (!options.allowWrites) {
|
||||||
return {
|
return {
|
||||||
@@ -447,6 +526,263 @@ function parseMethodArgs(source: string, methodCallIndex: number): string[] | nu
|
|||||||
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MongoTextRange {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMongoCommandTextRanges(input: string): MongoTextRange[] {
|
||||||
|
const commands: MongoTextRange[] = [];
|
||||||
|
for (const segment of splitMongoSemicolonSeparatedSegments(input)) {
|
||||||
|
const parsed = parseMongoCommand(segment.text);
|
||||||
|
if (parsed) {
|
||||||
|
commands.push({ ...segment, text: parsed.text });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mongo shell users often omit semicolons and rely on one top-level
|
||||||
|
// command per line, so fall back to a conservative newline split.
|
||||||
|
const softSplit = splitMongoSegmentAtSoftStarts(segment);
|
||||||
|
if (softSplit.length > 1) {
|
||||||
|
commands.push(...softSplit);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.push(segment);
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMongoSemicolonSeparatedSegments(input: string): MongoTextRange[] {
|
||||||
|
const segments: MongoTextRange[] = [];
|
||||||
|
let start = 0;
|
||||||
|
let depth = 0;
|
||||||
|
let quote: string | null = null;
|
||||||
|
let escaped = false;
|
||||||
|
let lineComment = false;
|
||||||
|
let blockComment = false;
|
||||||
|
|
||||||
|
// Respect semicolons only when they appear at the top level; JSON literals,
|
||||||
|
// strings and comments are allowed to contain semicolons verbatim.
|
||||||
|
for (let i = 0; i < input.length; i += 1) {
|
||||||
|
const char = input[i] ?? "";
|
||||||
|
const next = input[i + 1] ?? "";
|
||||||
|
|
||||||
|
if (lineComment) {
|
||||||
|
if (char === "\n") lineComment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockComment) {
|
||||||
|
if (char === "*" && next === "/") {
|
||||||
|
blockComment = false;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
if (escaped) escaped = false;
|
||||||
|
else if (char === "\\") escaped = true;
|
||||||
|
else if (char === quote) quote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "/" && next === "/") {
|
||||||
|
lineComment = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "/" && next === "*") {
|
||||||
|
blockComment = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '"' || char === "'" || char === "`") {
|
||||||
|
quote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "{" || char === "[" || char === "(") depth += 1;
|
||||||
|
else if ((char === "}" || char === "]" || char === ")") && depth > 0) depth -= 1;
|
||||||
|
else if (char === ";" && depth === 0) {
|
||||||
|
pushMongoSegment(segments, input, start, i);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushMongoSegment(segments, input, start, input.length);
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMongoSegmentAtSoftStarts(segment: MongoTextRange): MongoTextRange[] {
|
||||||
|
const boundaries = mongoTopLevelCommandLineStarts(segment.text);
|
||||||
|
if (boundaries.length <= 1) return [segment];
|
||||||
|
|
||||||
|
const segments: MongoTextRange[] = [];
|
||||||
|
let start = boundaries[0] ?? 0;
|
||||||
|
for (let index = 1; index < boundaries.length; index += 1) {
|
||||||
|
const boundary = boundaries[index] ?? 0;
|
||||||
|
const candidate = trimMongoOuterCommentRange(segment.text, start, boundary);
|
||||||
|
// Only accept newline-based splitting when every slice is a valid command;
|
||||||
|
// otherwise keep the original text intact and let normal parsing reject it.
|
||||||
|
if (!candidate || !parseMongoCommand(candidate.text)) return [segment];
|
||||||
|
segments.push({
|
||||||
|
from: segment.from + candidate.from,
|
||||||
|
to: segment.from + candidate.to,
|
||||||
|
text: candidate.text,
|
||||||
|
});
|
||||||
|
start = boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = trimMongoOuterCommentRange(segment.text, start, segment.text.length);
|
||||||
|
if (!last || !parseMongoCommand(last.text)) return [segment];
|
||||||
|
segments.push({
|
||||||
|
from: segment.from + last.from,
|
||||||
|
to: segment.from + last.to,
|
||||||
|
text: last.text,
|
||||||
|
});
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mongoTopLevelCommandLineStarts(segment: string): number[] {
|
||||||
|
const starts: number[] = [];
|
||||||
|
let depth = 0;
|
||||||
|
let quote: string | null = null;
|
||||||
|
let escaped = false;
|
||||||
|
let lineComment = false;
|
||||||
|
let blockComment = false;
|
||||||
|
let lineStart = 0;
|
||||||
|
let firstNonWhitespaceOnLine = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < segment.length; i += 1) {
|
||||||
|
const char = segment[i] ?? "";
|
||||||
|
const next = segment[i + 1] ?? "";
|
||||||
|
|
||||||
|
if (char === "\n") {
|
||||||
|
if (lineComment) lineComment = false;
|
||||||
|
lineStart = i + 1;
|
||||||
|
firstNonWhitespaceOnLine = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineComment) continue;
|
||||||
|
|
||||||
|
if (blockComment) {
|
||||||
|
if (char === "*" && next === "/") {
|
||||||
|
blockComment = false;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
if (escaped) escaped = false;
|
||||||
|
else if (char === "\\") escaped = true;
|
||||||
|
else if (char === quote) quote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "/" && next === "/") {
|
||||||
|
lineComment = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "/" && next === "*") {
|
||||||
|
blockComment = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '"' || char === "'" || char === "`") {
|
||||||
|
if (firstNonWhitespaceOnLine === -1 && !/\s/.test(char)) firstNonWhitespaceOnLine = i;
|
||||||
|
quote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "{" || char === "[" || char === "(") depth += 1;
|
||||||
|
else if ((char === "}" || char === "]" || char === ")") && depth > 0) depth -= 1;
|
||||||
|
|
||||||
|
if (firstNonWhitespaceOnLine === -1 && !/\s/.test(char)) {
|
||||||
|
firstNonWhitespaceOnLine = i;
|
||||||
|
if (depth === 0 && char !== "." && isMongoCommandLineStart(segment, i)) starts.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return starts.length > 0 ? starts : [lineStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMongoCommandLineStart(segment: string, index: number): boolean {
|
||||||
|
const rest = segment.slice(index);
|
||||||
|
return /^use\b/i.test(rest) || /^db(?:\s*\.|\b)/i.test(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushMongoSegment(segments: MongoTextRange[], source: string, from: number, to: number) {
|
||||||
|
const trimmed = trimMongoOuterCommentRange(source, from, to);
|
||||||
|
if (trimmed) segments.push(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimMongoOuterComments(source: string): string {
|
||||||
|
let value = source.trim();
|
||||||
|
while (value) {
|
||||||
|
const next = value.replace(/^(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)\s*/u, "");
|
||||||
|
if (next === value) break;
|
||||||
|
value = next.trimStart();
|
||||||
|
}
|
||||||
|
while (value) {
|
||||||
|
const next = value.replace(/\s*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)\s*$/u, "");
|
||||||
|
if (next === value) break;
|
||||||
|
value = next.trimEnd();
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimMongoOuterCommentRange(source: string, from: number, to: number): MongoTextRange | null {
|
||||||
|
let start = from;
|
||||||
|
let end = to;
|
||||||
|
|
||||||
|
while (start < end) {
|
||||||
|
const value = source.slice(start, end);
|
||||||
|
const trimmed = value.trimStart();
|
||||||
|
if (trimmed !== value) {
|
||||||
|
start += value.length - trimmed.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const next = value.replace(/^(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)\s*/u, "");
|
||||||
|
if (next !== value) {
|
||||||
|
start += value.length - next.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (start < end) {
|
||||||
|
const value = source.slice(start, end);
|
||||||
|
const trimmed = value.trimEnd();
|
||||||
|
if (trimmed !== value) {
|
||||||
|
end -= value.length - trimmed.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const next = value.replace(/\s*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)\s*$/u, "");
|
||||||
|
if (next !== value) {
|
||||||
|
end -= value.length - next.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= end) return null;
|
||||||
|
return {
|
||||||
|
from: start,
|
||||||
|
to: end,
|
||||||
|
text: source.slice(start, end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function convertSingleQuotedStrings(source: string): string {
|
function convertSingleQuotedStrings(source: string): string {
|
||||||
let result = "";
|
let result = "";
|
||||||
let copiedUntil = 0;
|
let copiedUntil = 0;
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export async function resolveExecutableSqlWithBackend(fullSql: string, selectedS
|
|||||||
const trimmedSelection = selectedSql.trim();
|
const trimmedSelection = selectedSql.trim();
|
||||||
if (trimmedSelection) return trimmedSelection;
|
if (trimmedSelection) return trimmedSelection;
|
||||||
|
|
||||||
|
// MongoDB uses dedicated per-command gutter actions for "current command";
|
||||||
|
// the main editor execute action keeps its long-standing "run all text"
|
||||||
|
// behavior when nothing is selected.
|
||||||
|
if (options?.databaseType === "mongodb") return fullSql;
|
||||||
|
|
||||||
if (options?.mode === "current" && options.cursorPos !== undefined) {
|
if (options?.mode === "current" && options.cursorPos !== undefined) {
|
||||||
return await api.findStatementAtCursor(fullSql, options.cursorPos, options.databaseType);
|
return await api.findStatementAtCursor(fullSql, options.cursorPos, options.databaseType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { SqlExecutionCandidate } from "./sqlExecutionTarget";
|
import type { SqlExecutionCandidate } from "./sqlExecutionTarget";
|
||||||
|
import { splitMongoCommandRanges } from "./mongoShellCommand";
|
||||||
import type { DatabaseType } from "@/types/database";
|
import type { DatabaseType } from "@/types/database";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1103,6 +1104,7 @@ function normalizeSql(sql: string): string {
|
|||||||
*/
|
*/
|
||||||
export function executableStatementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
|
export function executableStatementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
|
||||||
if (databaseType === "redis") return redisExecutableCommandRanges(sql);
|
if (databaseType === "redis") return redisExecutableCommandRanges(sql);
|
||||||
|
if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text }));
|
||||||
return splitSqlStatementRanges(sql, databaseType).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType).map((range) => rangeFor(range, sql)));
|
return splitSqlStatementRanges(sql, databaseType).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType).map((range) => rangeFor(range, sql)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,7 @@ import {
|
|||||||
mongoUseToQueryResult,
|
mongoUseToQueryResult,
|
||||||
mongoVersionToQueryResult,
|
mongoVersionToQueryResult,
|
||||||
mongoWriteToQueryResult,
|
mongoWriteToQueryResult,
|
||||||
parseMongoAggregateCommand,
|
splitMongoCommands,
|
||||||
parseMongoCountDocumentsCommand,
|
|
||||||
parseMongoFindCommand,
|
|
||||||
parseMongoGetIndexesCommand,
|
|
||||||
parseMongoWriteCommand,
|
|
||||||
parseMongoUseCommand,
|
|
||||||
parseMongoVersionCommand,
|
|
||||||
type MongoAggregateSafetyOptions,
|
type MongoAggregateSafetyOptions,
|
||||||
} from "@/lib/mongoShellCommand";
|
} from "@/lib/mongoShellCommand";
|
||||||
import { redisCommandResultToQueryResult } from "@/lib/redisQueryResult";
|
import { redisCommandResultToQueryResult } from "@/lib/redisQueryResult";
|
||||||
@@ -1605,14 +1599,24 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
let useAgentResultSession = false;
|
let useAgentResultSession = false;
|
||||||
try {
|
try {
|
||||||
const connStore = useConnectionStore();
|
const connStore = useConnectionStore();
|
||||||
|
let conn = connStore.getConfig(tab.connectionId);
|
||||||
|
const parsedMongoCommands = conn?.db_type === "mongodb" ? splitMongoCommands(sql) : undefined;
|
||||||
|
let mongoCommands = parsedMongoCommands ?? [];
|
||||||
|
const mongoNeedsConnection = mongoCommands.some(({ command }) => command.kind !== "use");
|
||||||
|
|
||||||
if (options?.skipEnsureConnected) {
|
if (options?.skipEnsureConnected) {
|
||||||
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "caller" });
|
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "caller" });
|
||||||
|
} else if (conn?.db_type === "mongodb" && mongoCommands.length > 0 && !mongoNeedsConnection) {
|
||||||
|
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "mongo-use-only" });
|
||||||
} else {
|
} else {
|
||||||
console.info("[DBX][executeTabSql:ensure-connected:start]", { traceId, elapsed: elapsed() });
|
console.info("[DBX][executeTabSql:ensure-connected:start]", { traceId, elapsed: elapsed() });
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
await connStore.ensureConnected(tab.connectionId);
|
||||||
console.info("[DBX][executeTabSql:ensure-connected:done]", { traceId, elapsed: elapsed() });
|
console.info("[DBX][executeTabSql:ensure-connected:done]", { traceId, elapsed: elapsed() });
|
||||||
}
|
}
|
||||||
const conn = connStore.getConfig(tab.connectionId);
|
conn = connStore.getConfig(tab.connectionId);
|
||||||
|
if (parsedMongoCommands === undefined && conn?.db_type === "mongodb") {
|
||||||
|
mongoCommands = splitMongoCommands(sql);
|
||||||
|
}
|
||||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||||
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
|
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
|
||||||
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
|
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
|
||||||
@@ -1690,6 +1694,195 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mongoCommands.length > 0) {
|
||||||
|
console.info("[DBX][executeTabSql:mongo:start]", { traceId, database: tab.database, commandCount: mongoCommands.length, sql });
|
||||||
|
|
||||||
|
const allResults: QueryResult[] = [];
|
||||||
|
// Track the effective db as we walk the batch so later commands observe
|
||||||
|
// earlier `use ...` statements in the same editor selection.
|
||||||
|
let currentDatabase = tab.database;
|
||||||
|
let mongoEditTarget: QueryTab["mongoEditTarget"] | undefined;
|
||||||
|
|
||||||
|
for (const parsedCommand of mongoCommands) {
|
||||||
|
const mongoCommand = parsedCommand.command;
|
||||||
|
const commandStartedAt = performance.now();
|
||||||
|
try {
|
||||||
|
switch (mongoCommand.kind) {
|
||||||
|
case "find": {
|
||||||
|
console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
|
||||||
|
const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.skip, mongoCommand.limit, mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, executionId);
|
||||||
|
const queryResult = markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total));
|
||||||
|
allResults.push(queryResult);
|
||||||
|
mongoEditTarget = mongoCommands.length === 1 && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-find:done]", {
|
||||||
|
traceId,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
database: currentDatabase,
|
||||||
|
rowCount: result.documents.length,
|
||||||
|
total: result.total,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "version": {
|
||||||
|
console.info("[DBX][executeTabSql:mongo-version:start]", { traceId, database: currentDatabase });
|
||||||
|
const version = await api.mongoServerVersion(tab.connectionId, currentDatabase, executionId);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoVersionToQueryResult(version, performance.now() - commandStartedAt)));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-version:done]", {
|
||||||
|
traceId,
|
||||||
|
database: currentDatabase,
|
||||||
|
version,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "countDocuments": {
|
||||||
|
console.info("[DBX][executeTabSql:mongo-count:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
|
||||||
|
const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, 0, 1, mongoCommand.filter, undefined, undefined, executionId);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoCountToQueryResult(result.total, performance.now() - commandStartedAt)));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-count:done]", {
|
||||||
|
traceId,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
database: currentDatabase,
|
||||||
|
total: result.total,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "aggregate": {
|
||||||
|
if (options?.mongoSafety) {
|
||||||
|
const safety = evaluateMongoAggregateSafety(mongoCommand, options.mongoSafety);
|
||||||
|
if (!safety.allowed) throw new Error(safety.reason);
|
||||||
|
}
|
||||||
|
console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
|
||||||
|
const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize);
|
||||||
|
const result = await api.mongoAggregateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, executionId);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total)));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-aggregate:done]", {
|
||||||
|
traceId,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
database: currentDatabase,
|
||||||
|
rowCount: result.documents.length,
|
||||||
|
total: result.total,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "getIndexes": {
|
||||||
|
console.info("[DBX][executeTabSql:mongo-indexes:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
|
||||||
|
const indexes = await api.listIndexes(tab.connectionId, currentDatabase, "", mongoCommand.collection);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt)));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-indexes:done]", {
|
||||||
|
traceId,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
database: currentDatabase,
|
||||||
|
indexCount: indexes.length,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "insert":
|
||||||
|
case "update":
|
||||||
|
case "delete":
|
||||||
|
case "createIndex":
|
||||||
|
case "dropIndex":
|
||||||
|
case "dropIndexes": {
|
||||||
|
if (options?.mongoSafety) {
|
||||||
|
const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety);
|
||||||
|
if (!safety.allowed) throw new Error(safety.reason);
|
||||||
|
}
|
||||||
|
console.info("[DBX][executeTabSql:mongo-write:start]", {
|
||||||
|
traceId,
|
||||||
|
database: currentDatabase,
|
||||||
|
kind: mongoCommand.kind,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
});
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
if (mongoCommand.kind === "insert") {
|
||||||
|
const result = await api.mongoInsertDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.docsJson);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)));
|
||||||
|
} else if (mongoCommand.kind === "update") {
|
||||||
|
const result = await api.mongoUpdateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.many);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)));
|
||||||
|
} else if (mongoCommand.kind === "createIndex") {
|
||||||
|
const result = await api.mongoCreateIndex(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt)));
|
||||||
|
} else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") {
|
||||||
|
const result = await api.mongoDropIndexes(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex");
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt)));
|
||||||
|
} else {
|
||||||
|
const result = await api.mongoDeleteDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many);
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)));
|
||||||
|
}
|
||||||
|
console.info("[DBX][executeTabSql:mongo-write:done]", {
|
||||||
|
traceId,
|
||||||
|
database: currentDatabase,
|
||||||
|
kind: mongoCommand.kind,
|
||||||
|
collection: mongoCommand.collection,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "use": {
|
||||||
|
currentDatabase = mongoCommand.database;
|
||||||
|
allResults.push(markQueryResultRowsRaw(mongoUseToQueryResult(currentDatabase, performance.now() - commandStartedAt)));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
console.info("[DBX][executeTabSql:mongo-use:done]", {
|
||||||
|
traceId,
|
||||||
|
database: currentDatabase,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Surface per-command failures inline and continue collecting results
|
||||||
|
// for the rest of the batch, matching the grouped-result UX.
|
||||||
|
allResults.push(toErrorResult(error));
|
||||||
|
mongoEditTarget = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("[DBX][executeTabSql:mongo:done]", {
|
||||||
|
traceId,
|
||||||
|
database: currentDatabase,
|
||||||
|
commandCount: mongoCommands.length,
|
||||||
|
elapsed: elapsed(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const current = tabs.value.find((t) => t.id === id);
|
||||||
|
if (current?.executionId === executionId) {
|
||||||
|
if (allResults.length > 1) {
|
||||||
|
// Open grouped output on the first non-error result when possible so
|
||||||
|
// mixed success/error batches land on the most useful table first.
|
||||||
|
const activeResultIndex = allResults.findIndex((result) => !result.columns.includes("Error"));
|
||||||
|
const resultIndex = activeResultIndex >= 0 ? activeResultIndex : 0;
|
||||||
|
current.results = allResults;
|
||||||
|
current.activeResultIndex = resultIndex;
|
||||||
|
current.result = allResults[resultIndex];
|
||||||
|
} else {
|
||||||
|
current.results = undefined;
|
||||||
|
current.activeResultIndex = undefined;
|
||||||
|
current.result = allResults[0];
|
||||||
|
}
|
||||||
|
touchResult(current);
|
||||||
|
current.queryAnalysis = undefined;
|
||||||
|
current.querySourceColumns = undefined;
|
||||||
|
current.queryEditabilityReason = undefined;
|
||||||
|
current.mongoEditTarget = mongoCommands.length === 1 ? mongoEditTarget : undefined;
|
||||||
|
current.tableMeta = undefined;
|
||||||
|
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
||||||
|
current.resultSortedSql = options?.resultSortedSql;
|
||||||
|
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
||||||
|
if (current.database !== currentDatabase) current.database = currentDatabase;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tab.mode === "query") {
|
if (tab.mode === "query") {
|
||||||
const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 };
|
const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 };
|
||||||
const plan = await api.prepareQueryPaginationExecutionPlan({
|
const plan = await api.prepareQueryPaginationExecutionPlan({
|
||||||
@@ -1709,269 +1902,6 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
pageLimit = options?.pagination?.limit ?? tableOpenPageLimit();
|
pageLimit = options?.pagination?.limit ?? tableOpenPageLimit();
|
||||||
pageOffset = options?.pagination?.offset ?? 0;
|
pageOffset = options?.pagination?.offset ?? 0;
|
||||||
}
|
}
|
||||||
const mongoFind = conn?.db_type === "mongodb" ? parseMongoFindCommand(sql) : null;
|
|
||||||
if (mongoFind) {
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoFind.collection });
|
|
||||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoFind.collection, mongoFind.skip, mongoFind.limit, mongoFind.filter, mongoFind.projection, mongoFind.sort, executionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-find:done]", {
|
|
||||||
traceId,
|
|
||||||
rowCount: result.documents.length,
|
|
||||||
total: result.total,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - startedAt, result.total));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = current.result.columns.includes("_id") ? { collection: mongoFind.collection, idColumn: "_id" } : undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mongoVersion = conn?.db_type === "mongodb" ? parseMongoVersionCommand(sql) : null;
|
|
||||||
if (mongoVersion) {
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-version:start]", { traceId });
|
|
||||||
const version = await api.mongoServerVersion(tab.connectionId, tab.database, executionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-version:done]", {
|
|
||||||
traceId,
|
|
||||||
version,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoVersionToQueryResult(version, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mongoCount = conn?.db_type === "mongodb" ? parseMongoCountDocumentsCommand(sql) : null;
|
|
||||||
if (mongoCount) {
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-count:start]", { traceId, collection: mongoCount.collection });
|
|
||||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoCount.collection, 0, 1, mongoCount.filter, undefined, undefined, executionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-count:done]", {
|
|
||||||
traceId,
|
|
||||||
total: result.total,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoCountToQueryResult(result.total, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoAggregate = conn?.db_type === "mongodb" ? parseMongoAggregateCommand(sql) : null;
|
|
||||||
if (mongoAggregate) {
|
|
||||||
if (options?.mongoSafety) {
|
|
||||||
const safety = evaluateMongoAggregateSafety(mongoAggregate, options.mongoSafety);
|
|
||||||
if (!safety.allowed) throw new Error(safety.reason);
|
|
||||||
}
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoAggregate.collection });
|
|
||||||
const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize);
|
|
||||||
const result = await api.mongoAggregateDocuments(tab.connectionId, tab.database, mongoAggregate.collection, mongoAggregate.pipeline, aggregateMaxRows, executionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-aggregate:done]", {
|
|
||||||
traceId,
|
|
||||||
rowCount: result.documents.length,
|
|
||||||
total: result.total,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - startedAt, result.total));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoGetIndexes = conn?.db_type === "mongodb" ? parseMongoGetIndexesCommand(sql) : null;
|
|
||||||
if (mongoGetIndexes) {
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-indexes:start]", { traceId, collection: mongoGetIndexes.collection });
|
|
||||||
const indexes = await api.listIndexes(tab.connectionId, tab.database, "", mongoGetIndexes.collection);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-indexes:done]", {
|
|
||||||
traceId,
|
|
||||||
indexCount: indexes.length,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoIndexesToQueryResult(indexes, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoWrite = conn?.db_type === "mongodb" ? parseMongoWriteCommand(sql) : null;
|
|
||||||
if (mongoWrite) {
|
|
||||||
if (options?.mongoSafety) {
|
|
||||||
const safety = evaluateMongoWriteSafety(mongoWrite, options.mongoSafety);
|
|
||||||
if (!safety.allowed) throw new Error(safety.reason);
|
|
||||||
}
|
|
||||||
await connStore.ensureConnected(tab.connectionId);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-write:start]", {
|
|
||||||
traceId,
|
|
||||||
kind: mongoWrite.kind,
|
|
||||||
collection: mongoWrite.collection,
|
|
||||||
});
|
|
||||||
let affectedRows = 0;
|
|
||||||
if (mongoWrite.kind === "insert") {
|
|
||||||
const result = await api.mongoInsertDocuments(tab.connectionId, tab.database, mongoWrite.collection, mongoWrite.docsJson);
|
|
||||||
affectedRows = result.affected_rows;
|
|
||||||
} else if (mongoWrite.kind === "update") {
|
|
||||||
const result = await api.mongoUpdateDocuments(tab.connectionId, tab.database, mongoWrite.collection, mongoWrite.filter, mongoWrite.update, mongoWrite.many);
|
|
||||||
affectedRows = result.affected_rows;
|
|
||||||
} else if (mongoWrite.kind === "createIndex") {
|
|
||||||
const result = await api.mongoCreateIndex(tab.connectionId, tab.database, mongoWrite.collection, mongoWrite.keys, mongoWrite.options);
|
|
||||||
console.info("[DBX][executeTabSql:mongo-write:done]", {
|
|
||||||
traceId,
|
|
||||||
indexName: result.name,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoCreateIndexToQueryResult(result.name, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (mongoWrite.kind === "dropIndex" || mongoWrite.kind === "dropIndexes") {
|
|
||||||
const result = await api.mongoDropIndexes(tab.connectionId, tab.database, mongoWrite.collection, mongoWrite.kind === "dropIndex" ? mongoWrite.index : mongoWrite.indexes, mongoWrite.kind === "dropIndex");
|
|
||||||
console.info("[DBX][executeTabSql:mongo-write:done]", {
|
|
||||||
traceId,
|
|
||||||
droppedNames: result.dropped_names,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const result = await api.mongoDeleteDocuments(tab.connectionId, tab.database, mongoWrite.collection, mongoWrite.filter, mongoWrite.many);
|
|
||||||
affectedRows = result.affected_rows;
|
|
||||||
}
|
|
||||||
console.info("[DBX][executeTabSql:mongo-write:done]", {
|
|
||||||
traceId,
|
|
||||||
affectedRows,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoWriteToQueryResult(affectedRows, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoUse = conn?.db_type === "mongodb" ? parseMongoUseCommand(sql) : null;
|
|
||||||
if (mongoUse) {
|
|
||||||
console.info("[DBX][executeTabSql:mongo-use:start]", { traceId, database: mongoUse.database });
|
|
||||||
const current = tabs.value.find((t) => t.id === id);
|
|
||||||
if (current?.executionId === executionId) {
|
|
||||||
current.database = mongoUse.database;
|
|
||||||
current.results = undefined;
|
|
||||||
current.activeResultIndex = undefined;
|
|
||||||
current.result = markQueryResultRowsRaw(mongoUseToQueryResult(mongoUse.database, performance.now() - startedAt));
|
|
||||||
touchResult(current);
|
|
||||||
current.queryAnalysis = undefined;
|
|
||||||
current.querySourceColumns = undefined;
|
|
||||||
current.queryEditabilityReason = undefined;
|
|
||||||
current.mongoEditTarget = undefined;
|
|
||||||
current.tableMeta = undefined;
|
|
||||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
|
||||||
current.resultSortedSql = options?.resultSortedSql;
|
|
||||||
syncDisplayedResultRun(current, options?.resultBaseSql ?? sql);
|
|
||||||
}
|
|
||||||
console.info("[DBX][executeTabSql:mongo-use:done]", {
|
|
||||||
traceId,
|
|
||||||
database: mongoUse.database,
|
|
||||||
elapsed: elapsed(),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionSchema = connectionUsesSchemaExecutionContext(conn) ? tab.schema || tab.database : tab.mode === "data" || connectionUsesDatabaseObjectTreeMode(conn) ? undefined : tab.schema;
|
const executionSchema = connectionUsesSchemaExecutionContext(conn) ? tab.schema || tab.database : tab.mode === "data" || connectionUsesDatabaseObjectTreeMode(conn) ? undefined : tab.schema;
|
||||||
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
|
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import {
|
|||||||
mongoDocumentsToQueryResult,
|
mongoDocumentsToQueryResult,
|
||||||
mongoIndexesToQueryResult,
|
mongoIndexesToQueryResult,
|
||||||
parseMongoAggregateCommand,
|
parseMongoAggregateCommand,
|
||||||
|
parseMongoCommand,
|
||||||
parseMongoCountDocumentsCommand,
|
parseMongoCountDocumentsCommand,
|
||||||
parseMongoFindCommand,
|
parseMongoFindCommand,
|
||||||
parseMongoGetIndexesCommand,
|
parseMongoGetIndexesCommand,
|
||||||
parseMongoVersionCommand,
|
parseMongoVersionCommand,
|
||||||
parseMongoWriteCommand,
|
parseMongoWriteCommand,
|
||||||
|
splitMongoCommands,
|
||||||
|
splitMongoCommandRanges,
|
||||||
} from "../../apps/desktop/src/lib/mongoShellCommand.ts";
|
} from "../../apps/desktop/src/lib/mongoShellCommand.ts";
|
||||||
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral } from "../../apps/desktop/src/lib/mongoDocumentValues.ts";
|
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral } from "../../apps/desktop/src/lib/mongoDocumentValues.ts";
|
||||||
|
|
||||||
@@ -91,6 +94,21 @@ test("parseMongoVersionCommand parses db.version", () => {
|
|||||||
assert.equal(parseMongoVersionCommand("db.jobs.version()"), null);
|
assert.equal(parseMongoVersionCommand("db.jobs.version()"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseMongoCommand normalizes outer comments around a command", () => {
|
||||||
|
const parsed = parseMongoCommand(`
|
||||||
|
// current database
|
||||||
|
use accounting;
|
||||||
|
// keep working here
|
||||||
|
`);
|
||||||
|
assert.deepEqual(parsed, {
|
||||||
|
text: "use accounting;",
|
||||||
|
command: {
|
||||||
|
kind: "use",
|
||||||
|
database: "accounting",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("parseMongoWriteCommand accepts unquoted insert and update commands", () => {
|
test("parseMongoWriteCommand accepts unquoted insert and update commands", () => {
|
||||||
assert.deepEqual(parseMongoWriteCommand("db.products.insertOne({name: 'demo', price: 1})"), {
|
assert.deepEqual(parseMongoWriteCommand("db.products.insertOne({name: 'demo', price: 1})"), {
|
||||||
kind: "insert",
|
kind: "insert",
|
||||||
@@ -211,6 +229,69 @@ test("parseMongoGetIndexesCommand parses collection index commands", () => {
|
|||||||
assert.equal(parseMongoGetIndexesCommand("db.web_log.getIndexes({})"), null);
|
assert.equal(parseMongoGetIndexesCommand("db.web_log.getIndexes({})"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("splitMongoCommands keeps semicolon-separated mongo commands in order", () => {
|
||||||
|
const commands = splitMongoCommands(`
|
||||||
|
db.users.insertOne({ name: "A" });
|
||||||
|
db.users.insertOne({ name: "B" });
|
||||||
|
`);
|
||||||
|
assert.deepEqual(
|
||||||
|
commands.map(({ text, command }) => ({ kind: command.kind, text })),
|
||||||
|
[
|
||||||
|
{ kind: "insert", text: 'db.users.insertOne({ name: "A" })' },
|
||||||
|
{ kind: "insert", text: 'db.users.insertOne({ name: "B" })' },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("splitMongoCommands splits top-level line starts without semicolons", () => {
|
||||||
|
const commands = splitMongoCommands(`
|
||||||
|
use accounting
|
||||||
|
db.getCollection("entries")
|
||||||
|
.find({ status: "open" })
|
||||||
|
.limit(5)
|
||||||
|
`);
|
||||||
|
assert.deepEqual(
|
||||||
|
commands.map(({ text, command }) => ({ kind: command.kind, text })),
|
||||||
|
[
|
||||||
|
{ kind: "use", text: "use accounting" },
|
||||||
|
{ kind: "find", text: 'db.getCollection("entries")\n .find({ status: "open" })\n .limit(5)' },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("splitMongoCommandRanges preserve document offsets for newline-separated commands", () => {
|
||||||
|
const source = `
|
||||||
|
use accounting
|
||||||
|
db.getCollection("entries")
|
||||||
|
.find({ status: "open" })
|
||||||
|
.limit(5)
|
||||||
|
`;
|
||||||
|
const commands = splitMongoCommandRanges(source);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
commands.map(({ from, to, text, command }) => ({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
kind: command.kind,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
from: source.indexOf("use accounting"),
|
||||||
|
to: source.indexOf("use accounting") + "use accounting".length,
|
||||||
|
text: "use accounting",
|
||||||
|
kind: "use",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: source.indexOf('db.getCollection("entries")'),
|
||||||
|
to: source.indexOf(' .limit(5)') + " .limit(5)".length,
|
||||||
|
text: 'db.getCollection("entries")\n .find({ status: "open" })\n .limit(5)',
|
||||||
|
kind: "find",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("evaluateMongoAggregateSafety blocks write stages unless MCP write flags allow them", () => {
|
test("evaluateMongoAggregateSafety blocks write stages unless MCP write flags allow them", () => {
|
||||||
const out = parseMongoAggregateCommand('db.products.aggregate([{"$out":"products_copy"}])');
|
const out = parseMongoAggregateCommand('db.products.aggregate([{"$out":"products_copy"}])');
|
||||||
assert.ok(out);
|
assert.ok(out);
|
||||||
|
|||||||
@@ -2120,6 +2120,189 @@ test("mongo dropIndexes execution returns dropped index names", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mongo multi-command execution runs writes sequentially and keeps grouped results", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const store = useQueryStore();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const insertBodies: any[] = [];
|
||||||
|
|
||||||
|
connectionStore.addEphemeralConnection({
|
||||||
|
...conn("mongo-1"),
|
||||||
|
db_type: "mongodb",
|
||||||
|
port: 27017,
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url === "/api/mongo/insert-documents") {
|
||||||
|
insertBodies.push(JSON.parse(String(init?.body ?? "{}")));
|
||||||
|
return new Response(JSON.stringify({ affected_rows: 1 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Response("unexpected request", { status: 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
|
||||||
|
await store.executeTabSql(
|
||||||
|
tabId,
|
||||||
|
`
|
||||||
|
db.users.insertOne({ name: "Ada" });
|
||||||
|
db.users.insertOne({ name: "Grace" });
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tab = store.tabs.find((item) => item.id === tabId);
|
||||||
|
|
||||||
|
assert.equal(insertBodies.length, 2);
|
||||||
|
assert.deepEqual(
|
||||||
|
insertBodies.map((body) => ({ database: body.database, collection: body.collection, docsJson: body.docsJson })),
|
||||||
|
[
|
||||||
|
{ database: "accounting", collection: "users", docsJson: '{ "name": "Ada" }' },
|
||||||
|
{ database: "accounting", collection: "users", docsJson: '{ "name": "Grace" }' },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(tab?.results?.length, 2);
|
||||||
|
assert.equal(tab?.activeResultIndex, 0);
|
||||||
|
assert.equal(tab?.result?.affected_rows, 1);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mongo multi-command execution reconnects before running commands", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const store = useQueryStore();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const requests: string[] = [];
|
||||||
|
|
||||||
|
connectionStore.addEphemeralConnection({
|
||||||
|
...conn("mongo-1"),
|
||||||
|
db_type: "mongodb",
|
||||||
|
port: 27017,
|
||||||
|
});
|
||||||
|
connectionStore.connectedIds.delete("mongo-1");
|
||||||
|
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url === "/api/connection/connect") {
|
||||||
|
requests.push(url);
|
||||||
|
return new Response(JSON.stringify("connected"), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url === "/api/mongo/insert-documents") {
|
||||||
|
requests.push(url);
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||||
|
assert.equal(body.database, "accounting");
|
||||||
|
return new Response(JSON.stringify({ affected_rows: 1 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Response("unexpected request", { status: 500 });
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
|
||||||
|
await store.executeTabSql(tabId, 'db.users.insertOne({ name: "Ada" })');
|
||||||
|
|
||||||
|
assert.deepEqual(requests, ["/api/connection/connect", "/api/mongo/insert-documents"]);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mongo multi-command execution applies use database before later commands", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const store = useQueryStore();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const insertBodies: any[] = [];
|
||||||
|
|
||||||
|
connectionStore.addEphemeralConnection({
|
||||||
|
...conn("mongo-1"),
|
||||||
|
db_type: "mongodb",
|
||||||
|
port: 27017,
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
|
||||||
|
const url = String(input);
|
||||||
|
if (url === "/api/mongo/insert-documents") {
|
||||||
|
insertBodies.push(JSON.parse(String(init?.body ?? "{}")));
|
||||||
|
return new Response(JSON.stringify({ affected_rows: 1 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Response("unexpected request", { status: 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
|
||||||
|
await store.executeTabSql(
|
||||||
|
tabId,
|
||||||
|
`
|
||||||
|
use archive
|
||||||
|
db.users.insertOne({ name: "Ada" })
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tab = store.tabs.find((item) => item.id === tabId);
|
||||||
|
|
||||||
|
assert.equal(insertBodies.length, 1);
|
||||||
|
assert.equal(insertBodies[0]?.database, "archive");
|
||||||
|
assert.equal(tab?.database, "archive");
|
||||||
|
assert.equal(tab?.results?.length, 2);
|
||||||
|
assert.deepEqual(tab?.results?.[0]?.rows, [["switched to db archive"]]);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mongo use-only execution updates the tab without reconnecting", async () => {
|
||||||
|
const restoreStorage = installMemoryStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const store = useQueryStore();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const requests: string[] = [];
|
||||||
|
|
||||||
|
connectionStore.addEphemeralConnection({
|
||||||
|
...conn("mongo-1"),
|
||||||
|
db_type: "mongodb",
|
||||||
|
port: 27017,
|
||||||
|
});
|
||||||
|
connectionStore.connectedIds.delete("mongo-1");
|
||||||
|
|
||||||
|
globalThis.fetch = async (input) => {
|
||||||
|
requests.push(String(input));
|
||||||
|
return new Response("unexpected request", { status: 500 });
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
|
||||||
|
await store.executeTabSql(tabId, "use archive");
|
||||||
|
const tab = store.tabs.find((item) => item.id === tabId);
|
||||||
|
|
||||||
|
assert.deepEqual(requests, []);
|
||||||
|
assert.equal(tab?.database, "archive");
|
||||||
|
assert.deepEqual(tab?.result?.rows, [["switched to db archive"]]);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
restoreStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("table data export fetches every filtered page", async () => {
|
test("table data export fetches every filtered page", async () => {
|
||||||
const restoreStorage = installMemoryStorage();
|
const restoreStorage = installMemoryStorage();
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { strict as assert } from "node:assert";
|
||||||
|
import { beforeEach, test, vi } from "vitest";
|
||||||
|
|
||||||
|
const apiMock = vi.hoisted(() => ({
|
||||||
|
findStatementAtCursor: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/api", () => apiMock);
|
||||||
|
|
||||||
|
const { resolveExecutableSqlWithBackend } = await import("../../apps/desktop/src/lib/sqlExecutionTarget.ts");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apiMock.findStatementAtCursor.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mongodb backend resolution keeps the full text when nothing is selected", async () => {
|
||||||
|
apiMock.findStatementAtCursor.mockResolvedValue("db.users.insertOne({ name: 'ignored' })");
|
||||||
|
|
||||||
|
const fullSql = "db.users.insertOne({ name: 'Ada' });\ndb.users.insertOne({ name: 'Grace' });";
|
||||||
|
const resolved = await resolveExecutableSqlWithBackend(fullSql, "", {
|
||||||
|
mode: "current",
|
||||||
|
cursorPos: fullSql.indexOf("Grace"),
|
||||||
|
databaseType: "mongodb",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolved, fullSql);
|
||||||
|
assert.equal(apiMock.findStatementAtCursor.mock.calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-mongodb backend resolution still asks the backend for the current statement", async () => {
|
||||||
|
apiMock.findStatementAtCursor.mockResolvedValue("SELECT 2");
|
||||||
|
|
||||||
|
const fullSql = "SELECT 1;\nSELECT 2;";
|
||||||
|
const resolved = await resolveExecutableSqlWithBackend(fullSql, "", {
|
||||||
|
mode: "current",
|
||||||
|
cursorPos: fullSql.indexOf("2"),
|
||||||
|
databaseType: "postgres",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolved, "SELECT 2");
|
||||||
|
assert.deepEqual(apiMock.findStatementAtCursor.mock.calls[0], [fullSql, fullSql.indexOf("2"), "postgres"]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user