import { defineStore } from "pinia"; import { uuid } from "@/lib/utils"; import { ref, watch, computed } from "vue"; import { useI18n } from "vue-i18n"; import type { DatabaseType, QueryTab } from "@/types/database"; import { orderPinnedFirst } from "@/lib/pinnedItems"; import { canCancelQueryExecution } from "@/lib/queryExecutionState"; import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions"; import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan"; import { allEditableColumnsWriteable, allPrimaryKeysPresent, sourceColumnsForResult } from "@/lib/sqlAnalysis"; import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence"; import { evaluateMongoAggregateSafety, mongoCountToQueryResult, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, type MongoAggregateSafetyOptions, } from "@/lib/mongoShellCommand"; import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities"; import { editablePrimaryKeys } from "@/lib/tableEditing"; import * as api from "@/lib/api"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import type { SavedSqlFile } from "@/types/database"; const STORAGE_KEY = "dbx-open-tabs"; const ACTIVE_TAB_KEY = "dbx-active-tab"; function saveTabs(tabs: QueryTab[], activeTabId: string | null) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs))); localStorage.setItem(ACTIVE_TAB_KEY, activeTabId || ""); } catch {} } function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } { try { return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY), { queryOnly: isTauriRuntime(), }); } catch { return { tabs: [], activeTabId: null }; } } function getI18nT() { try { return useI18n().t; } catch { return ((key: string, ..._args: unknown[]) => key) as ReturnType["t"]; } } export const useQueryStore = defineStore("query", () => { const t = getI18nT(); const restored = loadSavedTabs(); const tabs = ref(restored.tabs); const activeTabId = ref(restored.activeTabId); const MAX_CACHED_RESULTS = 5; async function closeResultSession(tab: QueryTab | undefined, preserveSessionId?: string) { const sessionId = tab?.resultSessionId ?? tab?.result?.session_id; if (!tab || !sessionId || sessionId === preserveSessionId) return; try { await api.closeQuerySession(tab.connectionId, tab.database, sessionId, tab.id); } catch (error) { console.warn("[DBX][query-session:close:error]", { tabId: tab.id, sessionId, error }); } finally { if (tab.resultSessionId === sessionId) tab.resultSessionId = undefined; if (tab.result?.session_id === sessionId) tab.result.session_id = undefined; } } async function closeClientConnectionSession(tab: QueryTab | undefined) { if (!tab?.connectionId) return; try { await api.closeClientConnectionSession(tab.connectionId, tab.database, tab.id); } catch (error) { console.warn("[DBX][client-session:close:error]", { tabId: tab.id, error }); } } function clearResultPayload(tab: QueryTab, options: { evicted?: boolean } = {}) { tab.result = undefined; tab.results = undefined; tab.activeResultIndex = undefined; tab.resultSessionId = undefined; tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = undefined; tab.resultEvicted = options.evicted ? true : undefined; } async function evictCachedResult(tab: QueryTab) { await closeResultSession(tab); clearResultPayload(tab, { evicted: true }); } const _persistSnapshot = computed(() => tabs.value.map((t) => ({ id: t.id, title: t.title, connectionId: t.connectionId, database: t.database, schema: t.schema, sql: t.sql, savedSqlId: t.savedSqlId, pinned: t.pinned, mode: t.mode, objectBrowser: t.objectBrowser, objectSource: t.objectSource, tableMeta: t.tableMeta, })), ); let _persistTimer: ReturnType | null = null; watch( [_persistSnapshot, activeTabId], () => { if (_persistTimer) clearTimeout(_persistTimer); _persistTimer = setTimeout(() => { saveTabs(tabs.value, activeTabId.value); _persistTimer = null; }, 300); }, { flush: "post" }, ); function findTabByIdentity( connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, ) { return tabs.value.find( (tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || ""), ); } function createTab( connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, ) { if (title) { const existing = findTabByIdentity(connectionId, database, title, mode, schema); if (existing) { activeTabId.value = existing.id; return existing.id; } } const id = uuid(); const tab: QueryTab = { id, title: title || `Query ${tabs.value.length + 1}`, connectionId, database, schema, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openObjectBrowser(connectionId: string, database: string, schema?: string) { const title = schema ? `${schema} objects` : `${database} objects`; const existing = tabs.value.find( (tab) => tab.mode === "objects" && tab.connectionId === connectionId && tab.database === database && (tab.objectBrowser?.schema || "") === (schema || ""), ); if (existing) { activeTabId.value = existing.id; return existing.id; } const id = uuid(); const tab: QueryTab = { id, title, connectionId, database, schema, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "objects", objectBrowser: { schema, objectType: "tables", }, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string) { const resolvedTableName = tableName || ""; const existing = tabs.value.find( (tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName, ); if (existing) { activeTabId.value = existing.id; return existing.id; } const title = resolvedTableName ? t("structureEditor.editTabTitle", { tableName: resolvedTableName }) : t("structureEditor.createTitle"); const id = uuid(); const tab: QueryTab = { id, title, connectionId, database, schema, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "structure", structureTableName: resolvedTableName, }; tabs.value.push(tab); activeTabId.value = id; return id; } function closeTab(id: string) { const idx = tabs.value.findIndex((t) => t.id === id); if (idx < 0) return; if (tabs.value[idx].isExecuting) void cancelTabExecution(id); if (tabs.value[idx].isExplaining) void cancelTabExplain(id); void closeResultSession(tabs.value[idx]); void closeClientConnectionSession(tabs.value[idx]); clearResultPayload(tabs.value[idx]); tabs.value.splice(idx, 1); if (activeTabId.value === id) { activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null; } } function closeOtherTabs(id: string) { tabs.value.filter((tab) => tab.id !== id && tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id)); tabs.value.filter((tab) => tab.id !== id && tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id)); tabs.value.filter((tab) => tab.id !== id).forEach((tab) => void closeResultSession(tab)); tabs.value.filter((tab) => tab.id !== id).forEach((tab) => void closeClientConnectionSession(tab)); const next = closeOtherTabsState(tabs.value, activeTabId.value, id); tabs.value = next.tabs; activeTabId.value = next.activeTabId; } function closeAllTabs() { tabs.value.filter((tab) => tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id)); tabs.value.filter((tab) => tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id)); tabs.value.forEach((tab) => void closeResultSession(tab)); tabs.value.forEach((tab) => void closeClientConnectionSession(tab)); const next = closeAllTabsState(tabs.value, activeTabId.value); tabs.value = next.tabs; activeTabId.value = next.activeTabId; } function updateSql(id: string, sql: string) { const tab = tabs.value.find((t) => t.id === id); if (tab) { tab.sql = sql; tab.resultSortedSql = undefined; tab.resultBaseSql = undefined; tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = undefined; } } function linkSavedSql(id: string, savedSqlId: string, title?: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.savedSqlId = savedSqlId; if (title) tab.title = title; } function openSavedSql(file: SavedSqlFile) { const existing = tabs.value.find((tab) => tab.savedSqlId === file.id); if (existing) { activeTabId.value = existing.id; return existing.id; } const id = uuid(); const tab: QueryTab = { id, title: file.name, connectionId: file.connectionId, database: file.database, schema: file.schema, sql: file.sql, savedSqlId: file.id, isExecuting: false, isCancelling: false, isExplaining: false, mode: "query", }; tabs.value.push(tab); activeTabId.value = id; return id; } function togglePinnedTab(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.pinned = !tab.pinned; tabs.value = orderPinnedFirst(tabs.value, (item) => !!item.pinned); } function updateDatabase(id: string, database: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.database === database) return; void closeResultSession(tab); void closeClientConnectionSession(tab); tab.database = database; tab.schema = undefined; tab.objectBrowser = undefined; clearResultPayload(tab); tab.lastExecutedSql = undefined; tab.resultBaseSql = undefined; tab.resultSortedSql = undefined; clearExplain(tab); tab.tableMeta = undefined; } function updateSchema(id: string, schema: string | undefined) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.schema === schema) return; tab.schema = schema; if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema }; } function updateConnection(id: string, connectionId: string, database = "") { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.connectionId === connectionId) return; void closeResultSession(tab); void closeClientConnectionSession(tab); tab.connectionId = connectionId; tab.database = database; tab.schema = undefined; clearResultPayload(tab); tab.lastExecutedSql = undefined; tab.resultBaseSql = undefined; tab.resultSortedSql = undefined; clearExplain(tab); tab.tableMeta = undefined; } function setTableMeta(id: string, meta: NonNullable) { const tab = tabs.value.find((t) => t.id === id); if (tab) tab.tableMeta = meta; } function setObjectSource(id: string, objectSource: NonNullable) { const tab = tabs.value.find((t) => t.id === id); if (tab) tab.objectSource = objectSource; } function setExecuting(id: string, isExecuting: boolean) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.isExecuting = isExecuting; if (!isExecuting) { tab.isCancelling = false; tab.executionId = undefined; } } function clearExplain(tab: QueryTab) { tab.explainPlan = undefined; tab.explainError = undefined; tab.explainSql = undefined; tab.lastExplainedSql = undefined; tab.isExplaining = false; tab.explainExecutionId = undefined; } function toErrorResult(e: any): NonNullable { const message = e instanceof Error ? e.message : String(e); return { columns: ["Error"], rows: [[message]], affected_rows: 0, execution_time_ms: 0, }; } function setErrorResult(id: string, e: any) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.result = toErrorResult(e); tab.results = undefined; tab.activeResultIndex = undefined; tab.resultSessionId = undefined; tab.isExecuting = false; tab.isCancelling = false; tab.executionId = undefined; } async function executeCurrentTab() { const tab = tabs.value.find((t) => t.id === activeTabId.value); if (!tab || !tab.sql.trim()) return; await executeCurrentSql(tab.sql); } async function executeCurrentSql(sql: string) { if (!activeTabId.value) return; await executeTabSql(activeTabId.value, sql, { resultBaseSql: sql, resultSortedSql: undefined }); } /** * Analyze query metadata for result tooltips and editability. */ async function analyzeQueryMetadata(tab: QueryTab, sql: string) { if (tab.mode !== "query") return; if (!tab.result || !tab.result.columns.length) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.tableMeta = undefined; return; } const editability = await api.analyzeEditableQueryEditability(sql); if (!editability.editable) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = editability.reason; tab.tableMeta = undefined; return; } const analysis = editability.analysis; if (!tab.connectionId || !tab.database) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = "metadata-unavailable"; tab.tableMeta = undefined; return; } // Resolve schema per database type const connStore = useConnectionStore(); const conn = connStore.getConfig(tab.connectionId); const dbType = conn?.db_type || ""; let schema = analysis.schema || tab.schema; if (!schema) { if (dbType === "postgres") schema = "public"; else schema = ""; } try { const columns = await api.getColumns(tab.connectionId, tab.database, schema, analysis.tableName); const primaryKeys = editablePrimaryKeys(dbType as DatabaseType, columns); tab.tableMeta = { schema: schema || undefined, tableName: analysis.tableName, columns, primaryKeys, }; if (primaryKeys.length === 0) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = "no-primary-key"; return; } if (!allPrimaryKeysPresent(primaryKeys, tab.result.columns, analysis)) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = "primary-key-not-returned"; return; } if (!allEditableColumnsWriteable(analysis, tab.result.columns)) { tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = "aliased-columns"; return; } tab.queryAnalysis = analysis; tab.querySourceColumns = sourceColumnsForResult(analysis, tab.result.columns); tab.queryEditabilityReason = undefined; } catch (err) { console.error("[DBX] ERROR fetching columns for query metadata:", err); tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = "metadata-unavailable"; tab.tableMeta = undefined; } } async function executeTabSql( id: string, sql: string, options?: { resultBaseSql?: string; resultSortedSql?: string | undefined; pagination?: { limit: number; offset: number; sessionId?: string }; mongoSafety?: MongoAggregateSafetyOptions; }, ) { const tab = tabs.value.find((t) => t.id === id); if (!tab || !sql.trim()) return; const executionId = uuid(); const traceId = executionId.slice(0, 8); const startedAt = performance.now(); const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`; tab.isExecuting = true; tab.isCancelling = false; tab.executionId = executionId; tab.lastExecutedSql = sql; tab.resultTotalRowCount = undefined; console.info("[DBX][executeTabSql:start]", { traceId, tabId: id, mode: tab.mode, connectionId: tab.connectionId, database: tab.database, schema: tab.schema, sql, }); const queryBaseSql = options?.resultBaseSql ?? sql; let sqlToExecute = sql; let pageSql: string | undefined; let pageLimit: number | undefined; let pageOffset: number | undefined; let countSql: string | undefined; let useAgentResultSession = false; try { const connStore = useConnectionStore(); await connStore.ensureConnected(tab.connectionId); const conn = connStore.getConfig(tab.connectionId); const useAgentCursor = !!conn?.db_type && AGENT_DRIVER_TYPES.has(conn.db_type); const queryTimeoutSecs = typeof conn?.query_timeout_secs === "number" && Number.isFinite(conn.query_timeout_secs) && conn.query_timeout_secs >= 0 ? conn.query_timeout_secs : 30; const settingsStore = useSettingsStore(); await closeResultSession(tab, options?.pagination?.sessionId); if (tab.mode === "query") { const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 }; const plan = await api.prepareQueryPaginationExecutionPlan({ sql, queryBaseSql, databaseType: conn?.db_type, pagination, useAgentCursor, }); sqlToExecute = plan.sqlToExecute; pageSql = plan.pageSql; pageLimit = plan.pageLimit; pageOffset = plan.pageOffset; countSql = plan.countSql; useAgentResultSession = plan.useAgentResultSession; } else if (tab.mode === "data") { pageLimit = settingsStore.editorSettings.pageSize; } 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.sort, ); 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 = mongoDocumentsToQueryResult(result.documents, performance.now() - startedAt, result.total); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.queryEditabilityReason = undefined; current.tableMeta = undefined; current.resultBaseSql = options?.resultBaseSql ?? sql; current.resultSortedSql = options?.resultSortedSql; } 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, ); 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 = mongoCountToQueryResult(result.total, performance.now() - startedAt); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.queryEditabilityReason = undefined; current.tableMeta = undefined; current.resultBaseSql = options?.resultBaseSql ?? sql; current.resultSortedSql = options?.resultSortedSql; } 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 result = await api.mongoAggregateDocuments( tab.connectionId, tab.database, mongoAggregate.collection, mongoAggregate.pipeline, pageLimit, ); 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 = mongoDocumentsToQueryResult(result.documents, performance.now() - startedAt, result.total); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.queryEditabilityReason = undefined; current.tableMeta = undefined; current.resultBaseSql = options?.resultBaseSql ?? sql; current.resultSortedSql = options?.resultSortedSql; } return; } console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() }); const executionOptions = { ...(typeof pageLimit === "number" ? useAgentResultSession ? { maxRows: pageLimit, fetchSize: pageLimit, pageSize: pageLimit, resultSessionId: options?.pagination?.sessionId, } : { maxRows: pageLimit, fetchSize: pageLimit } : {}), clientSessionId: tab.id, timeoutSecs: queryTimeoutSecs, }; const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60); const timeoutError = new Error(`查询超时 (${frontendTimeoutSecs}s),请检查数据库连接是否正常`); const results = await Promise.race([ api.executeMulti(tab.connectionId, tab.database, sqlToExecute, tab.schema, executionId, executionOptions), new Promise((_, reject) => setTimeout(() => reject(timeoutError), frontendTimeoutSecs * 1000)), ]); console.info("[DBX][executeTabSql:execute-multi:done]", { traceId, resultCount: results.length, rowCounts: results.map((result) => result.rows.length), columnCounts: results.map((result) => result.columns.length), elapsed: elapsed(), }); const current = tabs.value.find((t) => t.id === id); if (current?.executionId === executionId) { if (results.length > 1) { current.results = results; current.activeResultIndex = 0; current.result = results[0]; } else { current.results = undefined; current.activeResultIndex = undefined; current.result = results[0]; } current.resultBaseSql = queryBaseSql; current.resultSortedSql = options?.resultSortedSql; current.resultPageSql = pageSql; current.resultPageLimit = pageLimit; current.resultPageOffset = pageOffset; current.resultCountSql = countSql; current.resultSessionId = current.result?.session_id ?? undefined; if (countSql && current.result?.rows.length) { const capturedExecutionId = executionId; const capturedTabId = id; const capturedCountSql = countSql; const capturedConnectionId = tab.connectionId; const capturedDatabase = tab.database; const capturedSchema = tab.schema; api .executeQuery(capturedConnectionId, capturedDatabase ?? "", capturedCountSql, capturedSchema) .then((countResult) => { const tabAfterCount = tabs.value.find((t) => t.id === capturedTabId); if (tabAfterCount?.executionId === capturedExecutionId) { const total = Number(countResult.rows?.[0]?.[0] ?? 0); if (total > 0) { tabAfterCount.resultTotalRowCount = total; } } }) .catch(() => { // COUNT query failed — silently ignore }); } console.info("[DBX][executeTabSql:metadata:start]", { traceId, elapsed: elapsed() }); await analyzeQueryMetadata(current, queryBaseSql); console.info("[DBX][executeTabSql:metadata:done]", { traceId, elapsed: elapsed() }); } else { console.warn("[DBX][executeTabSql:stale-result]", { traceId, currentExecutionId: current?.executionId, elapsed: elapsed(), }); } } catch (e: any) { console.error("[DBX][executeTabSql:error]", { traceId, elapsed: elapsed(), error: e }); const current = tabs.value.find((t) => t.id === id); if (current?.executionId === executionId) { current.result = toErrorResult(e); current.results = undefined; current.activeResultIndex = undefined; current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.queryEditabilityReason = undefined; if (current.mode !== "data") current.tableMeta = undefined; current.resultBaseSql = queryBaseSql; current.resultSortedSql = options?.resultSortedSql; current.resultPageSql = pageSql; current.resultPageLimit = pageLimit; current.resultPageOffset = pageOffset; current.resultCountSql = countSql; current.resultSessionId = undefined; } } finally { const current = tabs.value.find((t) => t.id === id); if (current?.executionId === executionId) { current.isExecuting = false; current.isCancelling = false; current.executionId = undefined; console.info("[DBX][executeTabSql:finish]", { traceId, elapsed: elapsed() }); } else { console.warn("[DBX][executeTabSql:finish-stale]", { traceId, currentExecutionId: current?.executionId, elapsed: elapsed(), }); } } await trimResultCache(); } async function explainTabSql(id: string, sql: string, databaseType?: DatabaseType) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return { ok: false as const, reason: "empty" as const }; const conn = useConnectionStore().getConfig(tab.connectionId); const queryTimeoutSecs = typeof conn?.query_timeout_secs === "number" && Number.isFinite(conn.query_timeout_secs) && conn.query_timeout_secs >= 0 ? conn.query_timeout_secs : 30; const built = await buildExplainSql(databaseType, sql); if (!built.ok) { tab.explainPlan = undefined; tab.explainError = built.reason; return built; } const executionId = uuid(); tab.isExplaining = true; tab.explainExecutionId = executionId; tab.explainError = undefined; tab.explainSql = built.sql; tab.lastExplainedSql = sql; try { const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, tab.schema, executionId, { clientSessionId: tab.id, timeoutSecs: queryTimeoutSecs, }); const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.explainPlan = parseExplainResult(databaseType as "mysql" | "postgres", result); current.explainError = undefined; } } catch (e: any) { const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.explainPlan = undefined; current.explainError = String(e?.message || e); } } finally { const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.isExplaining = false; current.explainExecutionId = undefined; } } return { ok: true as const, sql: built.sql }; } async function cancelTabExecution(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab || !canCancelQueryExecution(tab)) return false; const executionId = tab.executionId; if (!executionId) return false; tab.isCancelling = true; try { const canceled = await api.cancelQuery(executionId); if (!canceled) { const current = tabs.value.find((t) => t.id === id); if (current && current.executionId === executionId) current.isCancelling = false; } return canceled; } catch (e: any) { const current = tabs.value.find((t) => t.id === id); if (current && current.executionId === executionId) { current.isCancelling = false; current.result = toErrorResult(e); } return false; } } async function cancelTabExplain(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.isExplaining || !tab.explainExecutionId) return false; const executionId = tab.explainExecutionId; try { const canceled = await api.cancelQuery(executionId); if (!canceled) { const current = tabs.value.find((t) => t.id === id); if (current && current.explainExecutionId === executionId) current.isExplaining = false; } return canceled; } catch (e: any) { const current = tabs.value.find((t) => t.id === id); if (current && current.explainExecutionId === executionId) { current.isExplaining = false; current.explainError = String(e?.message || e); } return false; } } function setActiveResultIndex(id: string, index: number) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.results || index < 0 || index >= tab.results.length) return; tab.activeResultIndex = index; tab.result = tab.results[index]; tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.queryEditabilityReason = undefined; } function notifyConnectionMayBeLost() { const stuck = tabs.value.filter((t) => t.isExecuting); if (stuck.length > 0) { stuck.forEach((t) => { t.isExecuting = false; t.isCancelling = false; t.executionId = undefined; t.result = toErrorResult(new Error("连接可能已断开,请刷新数据重试")); }); } } async function trimResultCache() { const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results)); if (inactive.length > MAX_CACHED_RESULTS) { const toEvict = inactive.slice(0, inactive.length - MAX_CACHED_RESULTS); await Promise.all(toEvict.map((t) => evictCachedResult(t))); } } async function reloadEvictedTab(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab || !tab.resultEvicted) return; tab.resultEvicted = false; const sql = tab.lastExecutedSql ?? tab.sql; if (!sql?.trim()) return; await executeTabSql(tab.id, sql, { resultBaseSql: tab.resultBaseSql ?? sql, resultSortedSql: tab.resultSortedSql, }); } return { tabs, activeTabId, createTab, closeTab, closeOtherTabs, closeAllTabs, updateSql, openObjectBrowser, openTableStructure, linkSavedSql, openSavedSql, togglePinnedTab, updateDatabase, updateSchema, updateConnection, setTableMeta, setObjectSource, setExecuting, setErrorResult, setActiveResultIndex, executeCurrentTab, executeCurrentSql, executeTabSql, explainTabSql, cancelTabExecution, cancelTabExplain, reloadEvictedTab, notifyConnectionMayBeLost, }; });