import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout"; import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand"; import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges"; import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint"; import type { ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database"; type Translate = (key: string, params?: Record) => string; export type OutputView = "result" | "summary" | "explain" | "chart"; export function connectionDisplayName(connectionId: string): string { const connectionStore = useConnectionStore(); return connectionStore.getConfig(connectionId)?.name || connectionId; } export function connectionGroupDisplayName(connectionId: string, t: Translate): string | undefined { const connectionStore = useConnectionStore(); const path = findConnectionGroupPath(connectionStore.sidebarLayout, connectionId); if (path === null) return undefined; return path.join(" / ") || t("connectionGroup.ungroupedLabel"); } export function connectionColor(connectionId: string): string { const connectionStore = useConnectionStore(); return connectionStore.getConfig(connectionId)?.color || ""; } export function isConnectionReadonly(connectionId: string): boolean { const connectionStore = useConnectionStore(); return connectionStore.getConfig(connectionId)?.read_only ?? false; } function jdbcTargetLabel(connection: ConnectionConfig): string { const url = connection.connection_string?.trim() || ""; const serviceMatch = url.match(/@\/\/[^/?;]+\/([^?;]+)/); if (serviceMatch?.[1]) return serviceMatch[1]; const sidMatch = url.match(/@[^:]+:\d+:([^?;]+)/); if (sidMatch?.[1]) return sidMatch[1]; const pathMatch = url.match(/^jdbc:[^:]+:\/\/[^/?;]+\/([^?;]+)/); if (pathMatch?.[1]) return pathMatch[1]; return connection.driver_label || "JDBC"; } export function databaseDisplayNameForTab(connectionId: string, database: string, t: Translate): string { const connectionStore = useConnectionStore(); const connection = connectionStore.getConfig(connectionId); if (connection?.db_type === "redis" && database !== "") return `db${database}`; if (connection?.db_type === "jdbc" && !database) return jdbcTargetLabel(connection); return database || t("editor.noDatabase"); } export function isPreviewTab(tab: QueryTab): boolean { const connectionStore = useConnectionStore(); const config = connectionStore.getConfig(tab.connectionId); return !!config?.name.startsWith("[Preview]"); } function queryTitle(tab: QueryTab): string | undefined { if (tab.customTitle || tab.savedSqlId || tab.objectSource) return tab.title.trim() || undefined; return undefined; } export function tabDisplayTitle(tab: QueryTab, t: Translate): string { const database = databaseDisplayNameForTab(tab.connectionId, tab.database, t); const settingsStore = useSettingsStore(); const compact = settingsStore.editorSettings.compactTabTitle; if (isPreviewTab(tab)) return tab.title; if (tab.mode === "data" && tab.tableMeta?.tableName) { if (compact) return tab.tableMeta.tableName; const suffix = tab.tableMeta.schema && tab.tableMeta.schema !== tab.database ? `@${database}.${tab.tableMeta.schema}` : `@${database}`; return `${tab.tableMeta.tableName}${suffix}`; } if (tab.mode === "query") { const title = queryTitle(tab); if (title) return title; if (compact) return connectionDisplayName(tab.connectionId); return `${connectionDisplayName(tab.connectionId)}@${database}`; } if (tab.mode === "mongo" && tab.sql) { if (compact) return tab.sql; return `${tab.sql}@${database}`; } if (tab.mode === "mongo-gridfs") { if (compact) return t("tabs.gridfs"); return `${t("tabs.gridfs")}@${database}`; } if (tab.mode === "mongo-bucket") { const bucketName = tab.mongoBucket?.bucketName || tab.sql || tab.title.split(".").pop() || tab.title; if (compact) return bucketName; return `${bucketName}@${database}`; } if (tab.mode === "vector" && tab.sql) { if (compact) return tab.sql; return `${tab.sql}@${database}`; } if (tab.mode === "redis") { if (compact) return connectionDisplayName(tab.connectionId); return `${connectionDisplayName(tab.connectionId)}@${database}`; } if (tab.mode === "etcd") { if (compact) return connectionDisplayName(tab.connectionId); return `${connectionDisplayName(tab.connectionId)}@keys`; } if (tab.mode === "zookeeper") { if (compact) return connectionDisplayName(tab.connectionId); return `${connectionDisplayName(tab.connectionId)}@keys`; } if (tab.mode === "objects") { const schema = tab.objectBrowser?.schema; if (compact) return schema || tab.title; return schema ? `${schema}@${database}` : `${tab.title}@${database}`; } if (tab.mode === "users") { if (compact) return t("tabs.users"); return `${t("tabs.users")}@${connectionDisplayName(tab.connectionId)}`; } return tab.title; } export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; value: string }[] { const connName = connectionDisplayName(tab.connectionId); const groupName = connectionGroupDisplayName(tab.connectionId, t); const database = databaseDisplayNameForTab(tab.connectionId, tab.database, t); const lines: { label: string; value: string }[] = [{ label: t("tabs.tooltipConnection"), value: connName }, ...(groupName ? [{ label: t("tabs.tooltipGroup"), value: groupName }] : []), { label: t("tabs.tooltipDatabase"), value: database }]; if (tab.mode === "query" && queryTitle(tab)) { lines.unshift({ label: t("tabs.tooltipTitle"), value: tab.title }); } if (tab.mode === "query" && tab.externalSqlPath) { lines.push({ label: t("tabs.tooltipFilePath"), value: tab.externalSqlPath }); } if (tab.mode === "data" && tab.tableMeta?.tableName) { lines.push({ label: t("tabs.tooltipTable"), value: tab.tableMeta.tableName }); } if (tab.mode === "mongo" && tab.sql) { lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql }); } if (tab.mode === "mongo-gridfs") { lines.push({ label: t("tabs.gridfs"), value: t("tabs.gridfs") }); } if (tab.mode === "mongo-bucket") { lines.push({ label: t("tabs.gridfs"), value: tab.mongoBucket?.bucketName || tab.sql || tab.title }); } if (tab.mode === "vector" && tab.sql) { lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql }); } if (tab.mode === "objects" && tab.objectBrowser?.schema) { lines.push({ label: t("tabs.tooltipSchema"), value: tab.objectBrowser.schema }); } return lines; } export function queryResultStatementLabel(result: Pick): string | undefined { return result.sourceLabel; } export function middleEllipsis(value: string, maxLength = 24): string { if (value.length <= maxLength) return value; if (maxLength <= 3) return ".".repeat(Math.max(0, maxLength)); const visibleLength = maxLength - 3; const startLength = Math.ceil(visibleLength / 2); const endLength = Math.floor(visibleLength / 2); const end = endLength > 0 ? value.slice(-endLength) : ""; return `${value.slice(0, startLength)}...${end}`; } export function resultSqlForGrid(tab: Pick): string { return tab.result?.sourceStatement || tab.resultBaseSql || tab.lastExecutedSql || tab.sql; } /** * Resolves a result's executed statement back to its current editor range. * A stale or ambiguous source is ignored instead of highlighting a different * statement that happens to have the same text. */ export function resultSourceRange(editorSql: string, result: Pick | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined { const sourceStatement = result?.sourceStatement; if (!sourceStatement) return undefined; if (typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === sourceStatement) { return { from: result.sourceFrom, to: result.sourceTo, sql: sourceStatement }; } const statements = statementRanges(editorSql, databaseType); const indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined; if (indexed?.sql === sourceStatement) { return { from: indexed.from, to: indexed.to, sql: indexed.sql }; } const matches = statements.filter((statement) => statement.sql === sourceStatement); if (matches.length !== 1) return undefined; const [match] = matches; return { from: match.from, to: match.to, sql: match.sql }; } export type StatementExecutionMarkerStatus = "success" | "error"; export interface StatementExecutionMarker { from: number; status: StatementExecutionMarkerStatus; successCount: number; errorCount: number; } function lineStartOffset(sql: string, from: number): number { return sql.lastIndexOf("\n", Math.max(0, from - 1)) + 1; } function statementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] { if (databaseType === "redis") return executableStatementRanges(sql, databaseType); if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text })); return splitSqlStatementRanges(sql, databaseType); } export function statementExecutionMarkers(editorSql: string, results: QueryResult[] | undefined, databaseType?: DatabaseType, submittedSql = editorSql, executionEditorFingerprint = sqlTextFingerprint(editorSql)): StatementExecutionMarker[] { if (!results?.length || sqlTextFingerprint(editorSql) !== executionEditorFingerprint) return []; const submittedStatements = statementRanges(submittedSql, databaseType); if (submittedStatements.length <= 1) return []; const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType); const byLine = new Map(); for (const result of results) { if (!Number.isInteger(result.statement_index) || result.statement_index! < 0) continue; const statementIndex = result.statement_index!; const submittedStatement = submittedStatements[statementIndex]; if (!submittedStatement || submittedStatement.sql !== result.sourceStatement) continue; const range = typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === result.sourceStatement ? { from: result.sourceFrom, to: result.sourceTo, sql: result.sourceStatement } : editorStatements[statementIndex]?.sql === result.sourceStatement ? editorStatements[statementIndex] : undefined; if (!range) continue; const from = lineStartOffset(editorSql, range.from); const current = byLine.get(from) ?? { success: 0, error: 0 }; if (result.execution_error === true) current.error += 1; else current.success += 1; byLine.set(from, current); } return [...byLine.entries()] .sort(([a], [b]) => a - b) .map(([from, counts]) => { return { from, status: counts.error > 0 ? "error" : "success", successCount: counts.success, errorCount: counts.error, }; }); } export function queryResultBaseSql(tab: Pick): string { return resultSqlForGrid(tab); } export function queryResultExecutionSql(tab: Pick): string { return tab.resultSortedSql || resultSqlForGrid(tab); } export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string; displayLabel?: string; labelTruncated: boolean; title?: string }[] { if (!results) return []; return results .map((result, index) => ({ result, index })) .filter((item) => item.result.columns.length > 0) .map((item, ordinal) => { const label = queryResultStatementLabel(item.result); const displayLabel = label ? middleEllipsis(label) : undefined; return { ...item, n: ordinal + 1, label, displayLabel, labelTruncated: !!label && displayLabel !== label, title: item.result.sourceLabel || item.result.sourceStatement, }; }); } export function activeResultRun(tab: Pick) { return tab.resultRuns?.find((run) => run.id === tab.activeResultRunId); } export function resultRunItems(tab: Pick): { id: string; title: string; sequence: number; active: boolean }[] { return (tab.resultRuns ?? []).map((run) => ({ id: run.id, title: run.title, sequence: run.sequence, active: run.id === tab.activeResultRunId, })); } export function resultGridCacheKey(tab: Pick): string { return `${tab.id}-${tab.activeResultRunId ?? "current"}-${tab.activeResultIndex ?? 0}`; } export function nextExecutionSummaryView(currentView: OutputView, canShowResult: boolean): OutputView { if (currentView === "summary" && canShowResult) return "result"; return "summary"; } export interface ExecutionSummaryItem { result: QueryResult; index: number; returnedColumns: number; returnedRows: number; affectedRows: number; executionTimeMs: number; hasTabularResult: boolean; isError: boolean; } export function executionSummaryItems(tab: Pick): ExecutionSummaryItem[] { const results = tab.results?.length ? tab.results : tab.result ? [tab.result] : []; return results.map((result, index) => ({ result, index, returnedColumns: result.columns.length, returnedRows: result.rows.length, affectedRows: result.affected_rows, executionTimeMs: result.execution_time_ms, hasTabularResult: result.columns.length > 0, isError: result.columns.includes("Error"), })); } export function tabModeLabel(tab: QueryTab, t: Translate): string { if (tab.mode === "data") return t("tabs.table"); if (tab.mode === "query") return t("tabs.sql"); if (tab.mode === "mongo") return t("tabs.mongo"); if (tab.mode === "mongo-gridfs" || tab.mode === "mongo-bucket") return t("tabs.gridfs"); if (tab.mode === "vector") return t("tabs.vector"); if (tab.mode === "redis") return t("tabs.redis"); if (tab.mode === "etcd") return t("tabs.etcd"); if (tab.mode === "zookeeper") return t("tabs.zookeeper"); if (tab.mode === "nacos") return "Nacos"; if (tab.mode === "objects") return t("tabs.objects"); if (tab.mode === "users") return t("tabs.users"); return tab.mode; }