import { defineStore } from "pinia"; import { uuid } from "@/lib/common/utils"; import { computed, markRaw, nextTick, onScopeDispose, reactive, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, ObjectBrowserViewport, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database"; import { orderPinnedFirst } from "@/lib/app/pinnedItems"; import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState"; import { buildExplainSql, parseExplainResult, parseDamengExplainText, parseOracleExplainText, sqlServerExplainResult, type BuildExplainSqlResult } from "@/lib/diagram/explainPlan"; import { mysqlExplainCompatibilityHint } from "@/lib/diagram/mysqlExplainCompatibility"; import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQueryEditability, analyzeSelectStructureForDisplay, resolveMetadataColumnName, resolveSourceColumnsByOrdinal, sourceColumnsForResult, type EditableQueryInfo, type EditableQuerySource } from "@/lib/sql/sqlAnalysis"; import { buildQueryWithHiddenPrimaryKeys, hiddenResultColumnIndexes, type HiddenPrimaryKeyProjection } from "@/lib/sql/editableQueryHiddenKeys"; import { ACTIVE_TAB_STORAGE_KEY, OPEN_TABS_STORAGE_KEY, restoreOpenTabsPayload, restoreOpenTabsState, serializeOpenTabs } from "@/lib/app/openTabsPersistence"; import { evaluateMongoAggregateSafety, evaluateMongoWriteSafety, mongoCollectionStatsToQueryResult, mongoCountToQueryResult, mongoDatabasesToQueryResult, mongoDistinctToQueryResult, mongoCreateIndexToQueryResult, mongoDocumentsToQueryResult, describeMongoCommandParseFailure, mongoDroppedIndexesToQueryResult, mongoFindLogicalTotal, mongoIndexesToQueryResult, planMongoFindPagination, mongoUseToQueryResult, mongoVersionToQueryResult, mongoWriteToQueryResult, splitMongoCommandRanges, type MongoAggregateSafetyOptions, } from "@/lib/mongo/mongoShellCommand"; import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata"; import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult"; import { nextRedisCommandDb } from "@/lib/redis/redisCommandSession"; import { isRedisMutatingCommand } from "@/lib/redis/redisCommandTable"; import { usesAgentCursorForQuery } from "@/lib/database/databaseDriverManifest"; import { defaultAutoCommitForDbType, supportsClearableQuerySchema, supportsTransaction } from "@/lib/database/databaseFeatureSupport"; import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/table/tableEditing"; import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport"; import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta"; import { isDataTabMetadataLifecycleStale } from "@/lib/sidebar/dataTabOpenPolicy"; import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase"; import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit"; import { getCachedTableMetadata, loadTableColumns, loadTableIndexes, loadTableMetadata, tableMetadataToDataTabMeta, updateCachedTableMetadataType, type TableMetadataRequest } from "@/lib/metadata/tableMetadataCache"; import { MetadataTaskLimiter } from "@/lib/metadata/metadataTaskLimiter"; import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql"; import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint, metadataSchemaForConnection } from "@/lib/database/jdbcDialect"; import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource"; import { beginDataGridNativeSelectionBlock, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection"; import { appendLargeValueCells, canUseTableDataLargeValuePreview, remapLargeValueCells, tableDataLargeValuePreviewOptions, TABLE_DATA_RESULT_MAX_BYTES } from "@/lib/dataGrid/dataGridLargeValues"; import { simpleDataGridOrderByReferencesMissingColumn, sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { agentProtocolQueryResultMaxRows, capQueryResultTotal, effectiveQueryResultMaxRows, limitQueryPagination, queryResultLimitReached } from "@/lib/dataGrid/queryResultRowLimit"; import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { replaceSqlServerLeadingUseQuery, sqlServerLeadingUseScript, sqlServerUseDatabaseFromStatement } from "@/lib/sql/sqlCompletionLookupTarget"; import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen"; import { clearDataGridPendingSnapshot, clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor"; import { clearDataGridStructuredFilterStatesForTab } from "@/lib/dataGrid/dataGridFilterBuilderPersistence"; import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache"; import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize"; import { queryResultBaseSql, queryResultExecutionSql, resultGridInstanceKey } from "@/lib/tabs/tabPresentation"; import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError"; import { batchSqlRecoverySql, batchSqlRecoveryState, mergeBatchQueryResults, offsetBatchQueryResultIndexes, prepareBatchSqlRecovery, type BatchSqlRecoveryAction } from "@/lib/query/batchSqlRecovery"; import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/query/queryResultArchive"; import * as api from "@/lib/backend/api"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; import { useExportTracker } from "@/composables/useExportTracker"; import { recordQueryCancellationLatency, resourceLifecycleDiagnostics } from "@/lib/diagnostics/resourceLifecycleDiagnostics"; import { appendDebugLog } from "@/lib/backend/debugLog"; import { BackendErrorException, formatError, isManualTransactionSessionExpired, normalizeBackendError, type BackendError } from "@/lib/backend/errorUtils"; import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition"; import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName"; import { resolveSavedSqlExecutionTarget, savedSqlExecutionTargetFromTab, type SavedSqlExecutionTarget, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage"; import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint"; import { disposeAllSqlServerActivityTraces, disposeSqlServerActivityTrace } from "@/lib/sqlserver/sqlServerActivityTraceRuntime"; import type { SavedSqlFile } from "@/types/database"; import i18n from "@/i18n"; import { translateBackendError } from "@/i18n/backend-errors"; import type { SqlExecutionTargetContext } from "@/lib/database/sqlExecutionTargetRegistry"; import type { DriverProfileWorkspaceScope } from "@/lib/database/driverProfileExtensions"; import type { MultiDbExecutionTarget, MultiDbResultRunExecution } from "@/types/sqlExecution"; const QUERY_SURFACE_ACTIVATION_EVENT = "dbx:activate-query-surface"; const ORACLE_LIKE_METADATA_TYPES = new Set(["oracle", "dameng", "oceanbase-oracle"]); const ORACLE_DEFERRED_LOB_TYPES = new Set(["CLOB", "NCLOB", "BLOB", "BFILE", "XMLTYPE", "SYS.XMLTYPE"]); // Bounded concurrency for grouped-query display column loads, scoped per // connection so different connections never block each other. Matches the // project's existing completion-metadata concurrency policy (<2>). const GROUPED_DISPLAY_METADATA_CONCURRENCY = 2; const GROUPED_DISPLAY_LIMITER_SCOPE_PREFIX = "query-column-comments:"; const groupedDisplayMetadataLimiter = new MetadataTaskLimiter(GROUPED_DISPLAY_METADATA_CONCURRENCY, (event) => { console.debug("[DBX][metadata-load:grouped-display-limiter]", event); }); const UPPERCASE_FOLDED_METADATA_TYPES = new Set([...ORACLE_LIKE_METADATA_TYPES, "saphana"]); const HIDDEN_QUERY_KEY_DATABASE_TYPES = new Set(["mysql", "postgres", "sqlserver", "oracle"]); const QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR = "Streaming export is unsupported for this query. Simplify it or use a supported driver."; const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const; const CANCEL_QUERY_TIMEOUT_MS = 10_000; const CANCEL_ACK_SETTLE_TIMEOUT_MS = 2_000; const ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS = 1_000; const ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT = Symbol("oracle-query-metadata-preflight-timeout"); const SAVED_SQL_EDITOR_POSITION_PERSIST_DELAY_MS = 500; type CloseConfirmContext = "tab" | "batch" | "app"; interface BatchSqlResumeOptions { batch: BatchSqlExecution; previousResults: QueryResult[]; startStatementIndex: number; continueOnError: boolean; } function hasHiddenPhysicalRowKey(databaseType: DatabaseType | undefined, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[]): boolean { return hiddenPrimaryKeys.some((projection) => !usesSyntheticRowIdKey(databaseType, [projection.sourceName])); } function cloneTabDraft(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } interface BuildQueryResultExportRequestOptions { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt" | "sql"; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array; } interface OpenSavedSqlOptions { targetMode?: SavedSqlOpenTargetMode; } interface OpenObjectSourceTabOptions { connectionId: string; database: string; title: string; schema?: string; catalog?: string; sql: string; objectSource: NonNullable; } interface UpdateExecutionTargetOptions { persistSavedSqlTarget?: boolean; } type DroppedTableObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW"; interface DroppedTableObjectTarget { connectionId: string; database: string; schema?: string; schemaCandidates?: Array; name: string; objectType?: DroppedTableObjectType; } interface TableDataRefreshTarget { connectionId: string; database: string; schema?: string; schemaCandidates?: Array; catalog?: string; name: string; } function tabClientSessionId(tab: Pick, suffix?: (typeof BACKGROUND_CLIENT_SESSION_SUFFIXES)[number]): string { return suffix ? `${tab.id}:${suffix}` : tab.id; } function resultRunCacheKey(tabId: string, runId: string): string { return `tab:${tabId}:run:${runId}`; } function normalizeOptionalSchema(schema: string | null | undefined): string { return schema?.trim() ?? ""; } function droppedTableObjectSchemaCandidates(target: DroppedTableObjectTarget): Set { const schemas = target.schemaCandidates?.length ? target.schemaCandidates : [target.schema]; return new Set(schemas.map(normalizeOptionalSchema)); } function markQueryResultRowsRaw(result: QueryResult): QueryResult { markRaw(result.rows); if (result.large_value_cells) markRaw(result.large_value_cells); if (result.mongo_documents) markRaw(result.mongo_documents); if (result.mongo_copy_documents) markRaw(result.mongo_copy_documents); return result; } function markQueryResultsRowsRaw(results: QueryResult[]): QueryResult[] { for (const result of results) markQueryResultRowsRaw(result); return results; } function exactTotalFromIncompletePage(result: QueryResult, pageLimit: number | undefined, pageOffset: number | undefined, useAgentResultSession: boolean | undefined): number | undefined { if (typeof pageLimit !== "number" || result.rows.length >= pageLimit || result.truncated === true) return undefined; // Cursor-backed drivers must explicitly confirm exhaustion; an omitted // has_more value may come from an older driver or an exhausted row cap. if (useAgentResultSession && result.has_more !== false) return undefined; return (pageOffset ?? 0) + result.rows.length; } export function appendQueryResultSegment(previous: QueryResult, segment: QueryResult, maxRows: number): QueryResult { if (segment.execution_error) { throw segment.error ? new BackendErrorException(segment.error) : new BackendErrorException(String(segment.rows[0]?.[0] ?? "Failed to load the next result segment")); } if (previous.columns.length !== segment.columns.length || previous.columns.some((column, index) => column !== segment.columns[index])) { throw new Error("Result columns changed while loading the next segment"); } const remainingRows = Math.max(0, maxRows - previous.rows.length); const appendedRowCount = Math.min(remainingRows, segment.rows.length); const appendParallelValues = (existing: T[] | undefined, next: T[] | undefined): T[] | undefined => { if (!existing || !next) return undefined; if (existing.length !== previous.rows.length || next.length !== segment.rows.length) return undefined; return [...existing, ...next.slice(0, appendedRowCount)]; }; const sridByColumn = new Map(); for (const column of previous.spatial_columns ?? []) { sridByColumn.set(column.column_index, column.srid); } for (const column of segment.spatial_columns ?? []) { const existing = sridByColumn.get(column.column_index) ?? null; sridByColumn.set(column.column_index, existing ?? column.srid); } const spatial_columns = Array.from(sridByColumn.entries()) .map(([column_index, srid]) => ({ column_index, srid })) .sort((a, b) => a.column_index - b.column_index); // Keep prior row objects intact so source-index based dirty/new/deleted state // remains valid, while bounding the in-memory result by the configured cap. return markQueryResultRowsRaw({ ...segment, appended_from_row_count: previous.rows.length, rows: [...previous.rows, ...segment.rows.slice(0, appendedRowCount)], spatial_columns: spatial_columns.length > 0 ? spatial_columns : undefined, spatial_values: appendParallelValues(previous.spatial_values, segment.spatial_values), large_value_cells: appendLargeValueCells(previous.large_value_cells, segment.large_value_cells, previous.rows.length, appendedRowCount), mongo_documents: appendParallelValues(previous.mongo_documents, segment.mongo_documents), mongo_copy_documents: appendParallelValues(previous.mongo_copy_documents, segment.mongo_copy_documents), execution_time_ms: (previous.execution_time_ms ?? 0) + (segment.execution_time_ms ?? 0), has_more: previous.rows.length + appendedRowCount >= maxRows ? false : segment.has_more, }); } function markQueryResultRunsRowsRaw(resultRuns: NonNullable): NonNullable { for (const run of resultRuns) { if (run.result) markQueryResultRowsRaw(run.result); if (run.results) markQueryResultsRowsRaw(run.results); if (run.resultLocalSortOriginalRows) markRaw(run.resultLocalSortOriginalRows); if (run.resultLocalSortOriginalLargeValueCells) markRaw(run.resultLocalSortOriginalLargeValueCells); if (run.resultLocalSortOriginalMongoDocuments) markRaw(run.resultLocalSortOriginalMongoDocuments); if (run.resultLocalSortOriginalMongoCopyDocuments) markRaw(run.resultLocalSortOriginalMongoCopyDocuments); } return resultRuns; } /** Drop heavyweight fields even when a deactivated DataGrid still retains the result object. */ function releaseResultObjectPayload(result: QueryResult): void { result.columns = []; result.rows = []; result.column_types = undefined; result.column_sortables = undefined; result.spatial_columns = undefined; result.spatial_values = undefined; result.hidden_column_indexes = undefined; result.local_column_filters = undefined; result.local_hidden_column_keys = undefined; result.mongo_documents = undefined; result.mongo_copy_documents = undefined; result.large_value_cells = undefined; result.elasticsearch_raw_body = undefined; result.messages = undefined; result.error = undefined; result.sourceLabel = undefined; result.sourceStatement = undefined; } function preservedResultIndex(results: QueryResult[], currentIndex: number | undefined, preserve: boolean | undefined): number | undefined { if (preserve !== true || typeof currentIndex !== "number" || currentIndex < 0 || currentIndex >= results.length) return undefined; return currentIndex; } function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType, sourceOffset?: number): { results: QueryResult[]; sqlServerUseDatabase?: string } { const statements = splitSqlStatementRanges(sql, databaseType); let statementIndex = 0; let sourceDatabase = database; let sqlServerUseDatabase: string | undefined; for (const result of results) { const explicitIndex = Number.isInteger(result.statement_index) && result.statement_index! >= 0 ? result.statement_index : undefined; const sourceIndex = explicitIndex ?? statementIndex; statementIndex = Math.max(statementIndex, sourceIndex + 1); const statement = statements[sourceIndex]; if (!statement) continue; annotateQueryResultSource(result, statement.sql, sourceDatabase, databaseType, sourceOffset === undefined ? undefined : { from: sourceOffset + statement.from, to: sourceOffset + statement.to }); const customName = queryResultNameFromPreamble(sql.slice(statement.hitFrom, statement.from)); if (customName) result.sourceLabel = customName; const successfulUseDatabase = databaseType === "sqlserver" && result.execution_error !== true ? sqlServerUseDatabaseFromStatement(statement.sql) : undefined; if (successfulUseDatabase) { sourceDatabase = successfulUseDatabase; sqlServerUseDatabase = successfulUseDatabase; } } return { results, sqlServerUseDatabase }; } const NON_STREAMING_BATCH_DATABASE_TYPES = new Set(["sqlserver", "turso", "cloudflare-d1"]); const liveBatchSqlExecutions = new WeakMap(); function cloneBatchSqlExecution(batch: BatchSqlExecution | undefined): BatchSqlExecution | undefined { return batch ? { ...batch, executionTarget: batch.executionTarget ? { ...batch.executionTarget } : undefined, items: batch.items.map((item) => ({ ...item })) } : undefined; } function batchSqlExecutionFor(tab: QueryTab, executionId: string): BatchSqlExecution | undefined { const liveBatch = liveBatchSqlExecutions.get(tab); if (liveBatch?.executionId === executionId) return liveBatch; return tab.batchSqlExecution?.executionId === executionId ? tab.batchSqlExecution : undefined; } function clearLiveBatchSqlExecution(tab: QueryTab, executionId: string) { if (liveBatchSqlExecutions.get(tab)?.executionId === executionId) liveBatchSqlExecutions.delete(tab); } function createBatchSqlExecution(executionId: string, editorSql: string, submittedSql: string, databaseType: DatabaseType | undefined, sourceOffset: number | undefined, executionTarget: MultiDbExecutionTarget): BatchSqlExecution | undefined { const statements = databaseType === "mongodb" ? splitMongoCommandRanges(submittedSql).map(({ from, to, text }) => ({ from, to, sql: text })) : splitSqlStatementRanges(submittedSql, databaseType); if (statements.length === 0) return undefined; if (statements.length > 1 && databaseType && NON_STREAMING_BATCH_DATABASE_TYPES.has(databaseType)) return undefined; const offset = sourceOffset ?? 0; return { executionId, submittedSql, editorFingerprint: sqlTextFingerprint(editorSql), sourceOffset: offset, completed: 0, total: statements.length, startedAt: Date.now(), executionTarget: { ...executionTarget }, items: statements.map((statement, statementIndex) => ({ statementIndex, sql: statement.sql, from: offset + statement.from, to: offset + statement.to, status: statementIndex === 0 ? "running" : "pending", })), }; } function applyBatchSqlProgress( tab: QueryTab, progress: { executionId: string; statementIndex: number; completed: number; total: number; success: boolean; executionTimeMs: number; affectedRows: number; error?: BackendError; }, continueOnError: boolean, statementOffset = 0, ) { const batch = batchSqlExecutionFor(tab, progress.executionId); if (!batch) return; const statementIndex = statementOffset + progress.statementIndex; const item = batch.items[statementIndex]; if (!item) return; if (progress.completed > 1) { for (let index = statementOffset; index < statementOffset + progress.completed - 1; index += 1) { const completedItem = batch.items[index]; if (completedItem && (completedItem.status === "pending" || completedItem.status === "running")) { completedItem.status = "success"; } } } item.status = progress.success ? "success" : "error"; item.executionTimeMs = progress.executionTimeMs; item.affectedRows = progress.affectedRows; item.errorDetails = progress.error; item.error = progress.error ? translateBackendError(i18n.global.t, progress.error) : undefined; batch.completed = batch.items.filter((candidate) => candidate.status === "success" || candidate.status === "error").length; if ((progress.success || continueOnError) && progress.completed < progress.total) { const next = batch.items[statementOffset + progress.completed]; if (next?.status === "pending") next.status = "running"; } } function reconcileBatchSqlResults(tab: QueryTab, executionId: string, results: QueryResult[]) { const batch = batchSqlExecutionFor(tab, executionId); if (!batch) return; let fallbackIndex = 0; for (const result of results) { const statementIndex = Number.isInteger(result.statement_index) && result.statement_index! >= 0 ? result.statement_index! : fallbackIndex; fallbackIndex = Math.max(fallbackIndex, statementIndex + 1); const item = batch.items[statementIndex]; if (!item) continue; const failed = result.execution_error === true; item.status = failed ? "error" : "success"; item.executionTimeMs = result.execution_time_ms; item.affectedRows = result.affected_rows; item.errorDetails = failed ? result.error : undefined; item.error = failed ? (result.error ? translateBackendError(i18n.global.t, result.error) : String(result.rows[0]?.[0] ?? "")) : undefined; } batch.completed = batch.items.filter((item) => item.status === "success" || item.status === "error").length; } function failBatchSqlExecution(tab: QueryTab, executionId: string, error: unknown, cancelled: boolean) { const batch = batchSqlExecutionFor(tab, executionId); if (!batch) return; const item = batch.items.find((candidate) => candidate.status === "running") ?? batch.items.find((candidate) => candidate.status === "pending"); if (!item) return; item.status = cancelled ? "cancelled" : "error"; item.errorDetails = cancelled ? undefined : (normalizeBackendError(error) ?? undefined); item.error = cancelled ? undefined : translateBackendError(i18n.global.t, error); batch.completed = batch.items.filter((candidate) => candidate.status === "success" || candidate.status === "error").length; } function finishBatchSqlExecution(tab: QueryTab, executionId: string, cancelled: boolean) { const batch = batchSqlExecutionFor(tab, executionId); if (!batch) return; if (cancelled) { const cancelledError = [...batch.items].reverse().find((item) => item.status === "error" && (item.errorDetails?.code === "DBX-JDBC-2003" || /cancel|取消/i.test(item.error ?? ""))); if (cancelledError) { cancelledError.status = "cancelled"; cancelledError.error = undefined; } } let markedCancelled = false; for (const item of batch.items) { if (item.status === "running" && cancelled && !markedCancelled) { item.status = "cancelled"; markedCancelled = true; } else if (item.status === "running" || item.status === "pending") { item.status = "skipped"; } } batch.completed = batch.items.filter((item) => item.status === "success" || item.status === "error").length; batch.finishedAt = Date.now(); } function sqlStatementWithoutLeadingComments(statement: string | undefined): string { let remaining = statement?.trimStart() ?? ""; while (remaining) { if (remaining.startsWith("--")) { const newline = remaining.indexOf("\n"); remaining = newline < 0 ? "" : remaining.slice(newline + 1).trimStart(); continue; } if (remaining.startsWith("/*")) { const end = remaining.indexOf("*/", 2); if (end < 0) return ""; remaining = remaining.slice(end + 2).trimStart(); continue; } break; } return remaining; } function isOracleCurrentSchemaStatement(statement: string | undefined): boolean { return /^ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\s*=/i.test(sqlStatementWithoutLeadingComments(statement)); } function isSapHanaSetSchemaStatement(statement: string | undefined): boolean { return /^SET\s+SCHEMA\s+(?:"(?:[^"]|"")*"|[A-Za-z_][\w$#]*)\s*;?\s*$/i.test(sqlStatementWithoutLeadingComments(statement)); } function sapHanaCurrentSchemaFromResult(result: QueryResult): string | undefined { const schema = result.rows[0]?.[0]; return typeof schema === "string" && schema.trim() ? schema.trim() : undefined; } function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string, databaseType?: DatabaseType, sourceRange?: { from: number; to: number }): QueryResult { result.sourceStatement = sourceStatement; if (sourceRange) { result.sourceFrom = sourceRange.from; result.sourceTo = sourceRange.to; } const label = databaseType ? queryResultSourceLabel(sourceStatement, { database, databaseType }) : undefined; if (label) result.sourceLabel = label; return result; } function elasticsearchHttpErrorStatus(result: QueryResult): number | undefined { const statusIndex = result.columns.findIndex((column) => column.toLowerCase() === "status"); if (statusIndex < 0) return undefined; const value = result.rows[0]?.[statusIndex]; const status = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN; return Number.isInteger(status) && status >= 400 ? status : undefined; } function displayedQueryMetadataSql(tab: QueryTab, fallbackSql: string): string { return tab.results?.length ? (tab.result?.sourceStatement ?? fallbackSql) : fallbackSql; } async function withFrontendQueryTimeout(promise: Promise, timeoutSecs: number, message: string): Promise { const timeoutMs = frontendQueryTimeoutDelayMs(timeoutSecs); if (timeoutMs === undefined) return promise; let timer: ReturnType | undefined; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }), ]); } finally { if (timer) clearTimeout(timer); } } async function withCancelQueryTimeout(promise: Promise): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Cancel request timed out after 10s.")), CANCEL_QUERY_TIMEOUT_MS); }), ]); } finally { if (timer) clearTimeout(timer); } } async function waitForOracleQueryMetadataPreflight(promise: Promise): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ promise, new Promise((resolve) => { timer = setTimeout(() => resolve(ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT), ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS); }), ]); } finally { if (timer) clearTimeout(timer); } } function normalizeUppercaseFoldedMetadataIdentifier(dbType: string, identifier: string | undefined, quoted?: boolean) { if (!identifier || quoted || !UPPERCASE_FOLDED_METADATA_TYPES.has(dbType)) return identifier; return identifier.toUpperCase(); } function normalizeUppercaseFoldedQueryAnalysis(dbType: string, analysis: EditableQueryInfo, schema: string | undefined, tableName: string): EditableQueryInfo { if (!UPPERCASE_FOLDED_METADATA_TYPES.has(dbType)) return analysis; return { ...analysis, schema, tableName, sources: analysis.sources?.map((source) => ({ ...source, schema: normalizeUppercaseFoldedMetadataIdentifier(dbType, source.schema, source.schemaQuoted), tableName: normalizeUppercaseFoldedMetadataIdentifier(dbType, source.tableName, source.tableNameQuoted)!, })), columns: analysis.columns.map((column) => ({ ...column, sourceName: normalizeUppercaseFoldedMetadataIdentifier(dbType, column.sourceName, column.sourceNameQuoted), })), }; } function editableQuerySources(analysis: EditableQueryInfo): EditableQuerySource[] { return analysis.sources?.length ? analysis.sources : [ { key: `${analysis.tableAlias ?? analysis.tableName}:0`, catalog: analysis.catalog, catalogQuoted: analysis.catalogQuoted, schema: analysis.schema, schemaQuoted: analysis.schemaQuoted, tableName: analysis.tableName, tableNameQuoted: analysis.tableNameQuoted, alias: analysis.tableAlias, }, ]; } function projectsAllColumnsForSource(analysis: EditableQueryInfo, sourceKey: string): boolean { return analysis.selectStar || analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === sourceKey)); } function oracleQueryProjectsDeferredLob(analysis: EditableQueryInfo, sourceKey: string, columns: readonly { name: string; data_type: string }[]): boolean { const deferredColumns = new Set(columns.filter((column) => ORACLE_DEFERRED_LOB_TYPES.has(column.data_type.trim().toUpperCase())).map((column) => column.name.toLowerCase())); if (deferredColumns.size === 0) return false; if (projectsAllColumnsForSource(analysis, sourceKey)) return true; return analysis.columns.some((column) => column.sourceName && column.sourceKey === sourceKey && deferredColumns.has(column.sourceName.toLowerCase())); } function oracleColumnsAllowDeferredLobMarkers(columns: readonly { name: string }[]): boolean { return !columns.some((column) => column.name.toUpperCase().startsWith("__DBX_LARGE_VALUE_BYTES_")); } function cloneAnalysisForSource(analysis: EditableQueryInfo, source: EditableQuerySource): EditableQueryInfo { return { ...analysis, catalog: source.catalog, catalogQuoted: source.catalogQuoted, schema: source.schema, schemaQuoted: source.schemaQuoted, tableName: source.tableName, tableNameQuoted: source.tableNameQuoted, tableAlias: source.alias, editableSourceKey: source.key, allowInsertDelete: analysis.sources?.length || analysis.distinct ? false : analysis.allowInsertDelete, }; } function resolveSourceColumnName(dbType: string, columnName: string, quoted: boolean | undefined, tableColumns: readonly { name: string }[]): string | undefined { return resolveMetadataColumnName( dbType, columnName, quoted, tableColumns.map((column) => column.name), ); } function bindColumnsForSource( dbType: string, analysis: EditableQueryInfo, source: EditableQuerySource, tableColumns: readonly { name: string }[], allSourceColumns: Array<{ source: EditableQuerySource; columns: readonly { name: string }[] }> = [{ source, columns: tableColumns }], ): EditableQueryInfo { return { ...analysis, columns: analysis.columns.map((column) => { if (!column.sourceName) return column; if (column.sourceKey) { if (column.sourceKey !== source.key) return column; const canonicalName = resolveSourceColumnName(dbType, column.sourceName, column.sourceNameQuoted, tableColumns); return { ...column, sourceName: canonicalName }; } if (column.sourceQualifier) return column; const matchingSources = allSourceColumns.flatMap((entry) => { const canonicalName = resolveSourceColumnName(dbType, column.sourceName!, column.sourceNameQuoted, entry.columns); return canonicalName ? [{ source: entry.source, canonicalName }] : []; }); if (matchingSources.length !== 1 || matchingSources[0]?.source.key !== source.key) return column; return { ...column, sourceName: matchingSources[0].canonicalName, sourceKey: source.key }; }), }; } function primaryKeysPresentForSource(dbType: string, primaryKeys: string[], resultColumns: string[], analysis: EditableQueryInfo, sourceKey: string, tableColumns: readonly { name: string }[]): boolean { if (!analysis.selectStar) return allPrimaryKeysPresent(primaryKeys, resultColumns, analysis, sourceKey); const metadataNames = tableColumns.map((column) => column.name); const canonicalResultColumns = resultColumns.flatMap((column) => { const canonicalName = resolveMetadataColumnName(dbType, column, undefined, metadataNames); return canonicalName ? [canonicalName] : []; }); return allPrimaryKeysPresent(primaryKeys, canonicalResultColumns); } function expandStarProjectionColumnsForSource(analysis: EditableQueryInfo, source: EditableQuerySource, tableColumns: readonly { name: string }[]): EditableQueryInfo { if (analysis.selectStar || !analysis.columns.some((column) => column.star)) return analysis; return { ...analysis, columns: analysis.columns.flatMap((column) => { if (!column.star) return [column]; if (column.sourceKey && column.sourceKey !== source.key) return [column]; return tableColumns.map((tableColumn) => ({ sourceName: tableColumn.name, sourceNameQuoted: false, ...(column.sourceQualifier ? { sourceQualifier: column.sourceQualifier } : {}), sourceKey: source.key, resultName: tableColumn.name, expression: column.sourceQualifier ? `${column.sourceQualifier}.${tableColumn.name}` : tableColumn.name, })); }), }; } let saveTabsQueue = Promise.resolve(); let persistTimer: ReturnType | null = null; let persistGeneration = 0; function saveTabs(tabs: QueryTab[], activeTabId: string | null): Promise { const payload = { tabs: serializeOpenTabs(tabs), activeTabId }; saveTabsQueue = saveTabsQueue.catch(() => undefined).then(() => api.saveOpenTabsState(payload)); return saveTabsQueue; } function loadLegacySavedTabs(): { rawTabs: string | null; rawActiveTabId: string | null } { return { rawTabs: safeLocalStorageGet(OPEN_TABS_STORAGE_KEY), rawActiveTabId: safeLocalStorageGet(ACTIVE_TAB_STORAGE_KEY), }; } function clearLegacySavedTabs() { safeLocalStorageRemove(OPEN_TABS_STORAGE_KEY); safeLocalStorageRemove(ACTIVE_TAB_STORAGE_KEY); } function restoreSavedTabsFromPayload(payload: { tabs?: unknown; activeTabId?: unknown } | null | undefined, options: { validConnectionIds?: Iterable } = {}): { tabs: QueryTab[]; activeTabId: string | null } { const restoreMode = useSettingsStore().editorSettings.openTabsRestoreMode; if (restoreMode === "none") return { tabs: [], activeTabId: null }; return restoreOpenTabsPayload(payload, { filter: restoreMode === "pinned" ? "pinned" : "all", validConnectionIds: options.validConnectionIds, }); } function restoreLegacySavedTabs(options: { validConnectionIds?: Iterable } = {}): { tabs: QueryTab[]; activeTabId: string | null } { const restoreMode = useSettingsStore().editorSettings.openTabsRestoreMode; if (restoreMode === "none") return { tabs: [], activeTabId: null }; const legacy = loadLegacySavedTabs(); return restoreOpenTabsState(legacy.rawTabs, legacy.rawActiveTabId, { filter: restoreMode === "pinned" ? "pinned" : "all", validConnectionIds: options.validConnectionIds, }); } 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 settingsStore = useSettingsStore(); const tabs = ref([]); // New tabs are created by many feature-specific entry points. Tracking id // changes here gives every one a stable timestamp without duplicating the // field at each call site; restored legacy tabs retain their saved order. function ensureTabCreationTimes() { const migrationBase = Date.now() - tabs.value.length; tabs.value.forEach((tab, index) => { if (typeof tab.createdAt !== "number" || !Number.isFinite(tab.createdAt)) tab.createdAt = migrationBase + index; }); } watch(() => tabs.value.map((tab) => tab.id), ensureTabCreationTimes, { immediate: true, flush: "sync" }); // Default auto-commit for a fresh query tab follows the user's global // "默认事务提交方式" setting (Settings > Editor), not a hard-coded constant. const defaultAutoCommitForDbTypeWithSetting = (dbType?: string) => defaultAutoCommitForDbType(dbType, settingsStore.editorSettings.defaultTransactionMode); // A stable Set of "connectionId\x00database" keys. Computed only from the // minimal tab identity fields so that it does NOT invalidate when other // properties change (isExecuting, result, sql, tableMeta...). Previously // isDatabaseOpen() called tabs.value.some() which tracked the full reactive // array — every mutation during openData() forced all database-type sidebar // TreeItems to recompute showsDatabaseOpenIndicator. const openDatabaseKeys = computed(() => { const keys = new Set(); for (const tab of tabs.value) { if (tab.connectionId && tab.database != null) { keys.add(`${tab.connectionId}\x00${tab.database}`); } } return keys; }); const activeTabId = ref(null); const isOpenTabsLoaded = ref(false); const activeTabHistory = ref([]); // Most-recently-activated tab ids, oldest first. Read-only view for the // Ctrl+Tab switcher, which renders them in reverse. const recentTabIds = computed(() => activeTabHistory.value); const showCloseConfirm = ref(false); const pendingCloseTabId = ref(null); const pendingBatchCloseTabIds = ref(null); const pendingBatchCloseFinalActiveTabId = ref(undefined); let pendingBatchCloseComplete: (() => void) | null = null; const isConfirmingAppClose = ref(false); const closeConfirmContext = ref("tab"); const tableStructureRefreshVersions = ref>({}); const savedSqlEditorPositionTimers = new Map>(); const pendingTabSessionResets = new Map>(); const pendingResultRunRestores = new Map(); const multiDbExecutionWorkers = new Map(); const multiDbExecutionWorkerScopes = new Map>(); let resultCacheTrimScheduled = false; let resultCacheTrimRunning = false; let resultCacheTrimRequested = false; function tableStructureKey(connectionId: string, database: string, schema: string | undefined, tableName: string): string { return [connectionId, database, schema || "", tableName].map((part) => part.toLowerCase()).join("\u0000"); } function invalidateTableStructure(connectionId: string, database: string, schema: string | undefined, tableName: string) { if (!tableName) return; const key = tableStructureKey(connectionId, database, schema, tableName); tableStructureRefreshVersions.value = { ...tableStructureRefreshVersions.value, [key]: (tableStructureRefreshVersions.value[key] ?? 0) + 1, }; for (const tab of tabs.value) { if (tab.mode !== "query" || tab.connectionId !== connectionId || tab.database !== database) continue; tab.completionContextVersion = (tab.completionContextVersion ?? 0) + 1; } } function tableStructureRefreshVersion(connectionId: string, database: string, schema: string | undefined, tableName: string): number { return tableStructureRefreshVersions.value[tableStructureKey(connectionId, database, schema, tableName)] ?? 0; } const MAX_CACHED_RESULTS = 5; const MAX_CACHED_RESULT_BYTES = 128 * 1024 * 1024; function queryExecutionLog(level: "debug" | "info" | "warn" | "error", event: string, details: Record) { appendDebugLog(level, `[DBX][executeTabSql:${event}]`, details); } function findExecutionTab(id: string): QueryTab | undefined { return tabs.value.find((tab) => tab.id === id) ?? multiDbExecutionWorkers.get(id); } function getExecutionTab(id: string): QueryTab | undefined { return findExecutionTab(id); } function activeResultExecutionTarget(id: string): MultiDbExecutionTarget | undefined { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.activeResultRunId) return undefined; const run = tab.resultRuns?.find((candidate) => candidate.id === tab.activeResultRunId); return run?.multiDbExecution?.kind === "multi-db" ? run.multiDbExecution.target : undefined; } function queryResultExecutionLocation(tab: QueryTab) { const target = activeResultExecutionTarget(tab.id); return { connectionId: target?.connectionId ?? tab.connectionId, database: target?.database ?? tab.database, schema: target?.schema ?? tab.schema, catalog: target?.catalog ?? tab.catalog, }; } function createMultiDbExecutionWorker(sourceTabId: string, target: MultiDbExecutionTarget, scopeId: string): string | undefined { const source = tabs.value.find((tab) => tab.id === sourceTabId); if (!source) return undefined; const id = `multi-worker-${uuid()}`; const worker = reactive({ ...source, id, connectionId: target.connectionId, catalog: target.catalog, database: target.database, schema: target.schema, sql: source.sql, result: undefined, results: undefined, activeResultIndex: undefined, resultRuns: undefined, activeResultRunId: undefined, resultCacheKey: undefined, resultSessionId: undefined, resultClientSessionId: undefined, resultCacheState: undefined, resultEvicted: undefined, isExecuting: false, isCancelling: false, executionId: undefined, queryExecutionStartedAt: undefined, batchSqlExecution: undefined, cancelRequestCount: 0, autoCommit: true, txnSessionId: undefined, txnAutoRolledBack: undefined, }); multiDbExecutionWorkers.set(id, worker); const workers = multiDbExecutionWorkerScopes.get(scopeId) ?? new Set(); workers.add(id); multiDbExecutionWorkerScopes.set(scopeId, workers); return id; } async function removeMultiDbExecutionWorker(workerId: string, scopeId?: string): Promise { const worker = multiDbExecutionWorkers.get(workerId); const transferredResultSessionId = worker?.resultSessionId && tabs.value.some((tab) => tab.resultRuns?.some((run) => run.resultSessionId === worker.resultSessionId)) ? worker.resultSessionId : undefined; await closeResultSession(worker, transferredResultSessionId); if (transferredResultSessionId && worker) { const connection = useConnectionStore().getConfig(worker.connectionId); const executionDatabase = dataTabExecutionDatabase(connection, worker.database, worker.catalog); for (const suffix of BACKGROUND_CLIENT_SESSION_SUFFIXES) { await closeClientSessionId(worker.connectionId, executionDatabase, tabClientSessionId(worker, suffix), worker.catalog, { tabId: worker.id }); } } else { await closeClientConnectionSession(worker); } pendingTabSessionResets.delete(workerId); liveBatchSqlExecutions.delete(worker as QueryTab); multiDbExecutionWorkers.delete(workerId); if (scopeId) { const workers = multiDbExecutionWorkerScopes.get(scopeId); workers?.delete(workerId); if (workers?.size === 0) multiDbExecutionWorkerScopes.delete(scopeId); } else { for (const [scope, workers] of multiDbExecutionWorkerScopes) { workers.delete(workerId); if (workers.size === 0) multiDbExecutionWorkerScopes.delete(scope); } } } async function cancelMultiDbExecutionScope(scopeId: string): Promise { const workers = [...(multiDbExecutionWorkerScopes.get(scopeId) ?? [])]; await Promise.all(workers.map((workerId) => cancelTabExecution(workerId))); } async function refreshLoadedMongoIndexesAfterMutation(connectionId: string, database: string, collection: string, traceId: string) { const connStore = useConnectionStore(); try { await refreshLoadedMongoIndexes(connStore, { connectionId, database, collection }); } catch (error) { queryExecutionLog("warn", "mongo-indexes:refresh-failed", { traceId, connectionId, database, collection, error: formatError(error), }); } } async function closeResultSession(tab: QueryTab | undefined, preserveSessionId?: string, throwOnError = false) { const sessionId = tab?.resultSessionId ?? tab?.result?.session_id; if (!tab || !sessionId || sessionId === preserveSessionId) return; const resultClientSessionId = tab.resultClientSessionId; const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog; const location = tab.mode === "query" ? queryResultExecutionLocation(tab) : { connectionId: tab.connectionId, database: tab.database, catalog }; const connection = location.catalog ? useConnectionStore().getConfig(location.connectionId) : undefined; const executionDatabase = dataTabExecutionDatabase(connection, location.database, location.catalog); try { const clientSessionId = tab.resultClientSessionId ?? tab.id; if (location.catalog) await api.closeQuerySession(location.connectionId, executionDatabase, sessionId, clientSessionId, location.catalog); else await api.closeQuerySession(location.connectionId, executionDatabase, sessionId, clientSessionId); } catch (error) { console.warn("[DBX][query-session:close:error]", { tabId: tab.id, sessionId, error }); if (throwOnError) throw error; } finally { if (tab.resultSessionId === sessionId) tab.resultSessionId = undefined; if (!tab.resultSessionId) tab.resultClientSessionId = undefined; if (tab.result?.session_id === sessionId) { tab.result.session_id = undefined; // 原地修改了负载,让持有它的 tab 与 run 的估算值都失效 invalidateResultEstimateForPayload(tab.result); } if (resultClientSessionId && resultClientSessionId !== tab.id) { await closeClientSessionId(location.connectionId, executionDatabase, resultClientSessionId, location.catalog, { tabId: tab.id }, throwOnError); } } } async function closeClientSessionId(connectionId: string, database: string, clientSessionId: string, catalog: string | undefined, logContext: Record = {}, throwOnError = false) { try { if (catalog) await api.closeClientConnectionSession(connectionId, database, clientSessionId, catalog); else await api.closeClientConnectionSession(connectionId, database, clientSessionId); } catch (error) { console.warn("[DBX][client-session:close:error]", { ...logContext, clientSessionId, error }); if (throwOnError) throw error; } } async function closeClientConnectionSession(tab: QueryTab | undefined, throwOnError = false) { if (!tab?.connectionId) return; const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog; const connection = catalog ? useConnectionStore().getConfig(tab.connectionId) : undefined; const executionDatabase = dataTabExecutionDatabase(connection, tab.database, catalog); const clientSessionIds = [...new Set([tabClientSessionId(tab), ...BACKGROUND_CLIENT_SESSION_SUFFIXES.map((suffix) => tabClientSessionId(tab, suffix)), tab.explainClientSessionId].filter((sessionId): sessionId is string => !!sessionId))]; for (const clientSessionId of clientSessionIds) { await closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, catalog, { tabId: tab.id }, throwOnError); } } function queueTabSessionReset(tab: QueryTab) { tab.completionContextVersion = (tab.completionContextVersion ?? 0) + 1; const previousReset = pendingTabSessionResets.get(tab.id); const reset = (async () => { if (previousReset) await previousReset; // A schema reset must fail closed: reusing the old session would retain Oracle CURRENT_SCHEMA. await closeResultSession(tab, undefined, true); await closeClientConnectionSession(tab, true); })(); pendingTabSessionResets.set(tab.id, reset); const clearPendingReset = () => { if (pendingTabSessionResets.get(tab.id) === reset) pendingTabSessionResets.delete(tab.id); }; void reset.then(clearPendingReset, clearPendingReset); } async function waitForTabSessionReset(tabId: string) { while (true) { const pendingReset = pendingTabSessionResets.get(tabId); if (!pendingReset) return; await pendingReset; if (pendingTabSessionResets.get(tabId) === pendingReset) pendingTabSessionResets.delete(tabId); } } function touchResult(tab: QueryTab | undefined, accessedAt = Date.now(), options: { reuseEstimatedBytes?: boolean } = {}) { if (tab?.result || tab?.results) { tab.resultAccessedAt = accessedAt; // 纯访问路径(如切换标签页)可复用已算好的估算值:estimateQueryResultsBytes // 会同步深遍历整份结果集,挂在 sync watch 上会直接阻塞切页交互。 if (!options.reuseEstimatedBytes || tab.resultEstimatedBytes === undefined) { tab.resultEstimatedBytes = estimateQueryResultsBytes(tab.result, tab.results); } tab.resultCacheState = "memory"; tab.resultEvicted = undefined; } } /** 结果负载被原地修改(如保存后写回单元格)时,让持有它的 tab/run 的字节估算失效,下次访问按需重算。 */ function invalidateResultEstimateForPayload(result: QueryResult | undefined) { if (!result) return; for (const tab of tabs.value) { if (tab.result === result || tab.results?.includes(result)) tab.resultEstimatedBytes = undefined; for (const run of tab.resultRuns ?? []) { if (run.result === result || run.results?.includes(result)) run.resultEstimatedBytes = undefined; } } } function clearResultPayload(tab: QueryTab, options: { evicted?: boolean; preserveCacheSnapshot?: boolean } = {}) { tab.result = undefined; tab.results = undefined; tab.activeResultIndex = undefined; tab.batchSqlExecution = undefined; tab.resultEditorFingerprint = undefined; tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; tab.resultSortMode = undefined; tab.resultSessionId = undefined; tab.resultClientSessionId = undefined; tab.resultAccessedAt = undefined; tab.resultEstimatedBytes = undefined; tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.resultColumnComments = undefined; tab.queryDisplaySourceColumns = undefined; tab.queryEditabilityReason = undefined; tab.mongoEditTarget = undefined; if (tab.mode === "query") tab.tableMeta = undefined; tab.resultEvicted = options.evicted ? true : undefined; tab.resultCacheState = options.evicted ? tab.resultCacheState : undefined; if (!options.evicted) { if (tab.resultCacheKey && !options.preserveCacheSnapshot) void deleteTabResultSnapshot(tab.resultCacheKey); tab.resultCacheKey = undefined; } } function clearResultNavigationState(tab: QueryTab) { tab.resultSortedSql = undefined; tab.resultSortColumn = undefined; tab.resultSortColumnIndex = undefined; tab.resultSortDirection = undefined; tab.resultSortMode = undefined; tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; tab.orderByInput = undefined; tab.resultPageSql = undefined; tab.resultPageLimit = undefined; tab.resultPageOffset = undefined; tab.resultCountSql = undefined; tab.resultTotalRowCount = undefined; tab.resultTotalRowCountLoading = false; tab.resultSessionId = undefined; tab.resultClientSessionId = undefined; } function clearResultRunSnapshots(tab: QueryTab) { for (const run of tab.resultRuns ?? []) { if (run.resultSessionId) void closeResultRunSession(tab, run); if (run.resultCacheKey) void deleteTabResultSnapshot(run.resultCacheKey); } } function releaseTabResultObjectPayloads(tab: QueryTab) { const results = new Set(); if (tab.result) results.add(tab.result); for (const result of tab.results ?? []) results.add(result); for (const run of tab.resultRuns ?? []) { if (run.result) results.add(run.result); for (const result of run.results ?? []) results.add(result); } for (const result of results) releaseResultObjectPayload(result); } function clearResultRuns(tab: QueryTab) { for (const run of tab.resultRuns ?? []) clearResultRunPayload(run); tab.resultRuns = undefined; tab.activeResultRunId = undefined; } function clearResultRunPayload(run: NonNullable[number], options: { evicted?: boolean } = {}) { if (run.result) releaseResultObjectPayload(run.result); for (const result of run.results ?? []) releaseResultObjectPayload(result); run.result = undefined; run.results = undefined; run.resultLocalSortOriginalRows = undefined; run.resultLocalSortOriginalLargeValueCells = undefined; run.resultLocalSortOriginalMongoDocuments = undefined; run.resultLocalSortOriginalMongoCopyDocuments = undefined; run.resultSessionId = undefined; run.resultClientSessionId = undefined; run.resultEstimatedBytes = undefined; run.queryAnalysis = undefined; run.querySourceColumns = undefined; run.resultColumnComments = undefined; run.queryDisplaySourceColumns = undefined; run.queryEditabilityReason = undefined; run.mongoEditTarget = undefined; run.tableMeta = undefined; run.resultEvicted = options.evicted ? true : undefined; run.resultCacheState = options.evicted ? "disk" : undefined; } function projectResultRun(tab: QueryTab, run: NonNullable[number]) { const activeIndex = run.activeResultIndex ?? 0; tab.activeResultRunId = run.id; tab.result = run.result ?? run.results?.[activeIndex]; tab.results = run.results; tab.activeResultIndex = run.activeResultIndex; tab.resultGridRevision = run.resultGridRevision; tab.batchSqlExecution = cloneBatchSqlExecution(run.batchSqlExecution); tab.resultBaseSql = run.resultBaseSql; tab.resultEditorFingerprint = run.resultEditorFingerprint; tab.resultSortedSql = run.resultSortedSql; tab.resultSortColumn = run.resultSortColumn; tab.resultSortColumnIndex = run.resultSortColumnIndex; tab.resultSortDirection = run.resultSortDirection; tab.resultSortMode = run.resultSortMode; tab.resultLocalSortOriginalRows = run.resultLocalSortOriginalRows; tab.resultLocalSortOriginalLargeValueCells = run.resultLocalSortOriginalLargeValueCells; tab.resultLocalSortOriginalMongoDocuments = run.resultLocalSortOriginalMongoDocuments; tab.resultLocalSortOriginalMongoCopyDocuments = run.resultLocalSortOriginalMongoCopyDocuments; tab.orderByInput = run.orderByInput; tab.resultPageSql = run.resultPageSql; tab.resultPageLimit = run.resultPageLimit; tab.resultPageOffset = run.resultPageOffset; tab.resultCountSql = run.resultCountSql; tab.resultTotalRowCount = run.resultTotalRowCount; tab.resultTotalRowCountLoading = run.resultTotalRowCountLoading; tab.resultSessionId = run.resultSessionId; tab.resultClientSessionId = run.resultClientSessionId; tab.resultAccessedAt = run.resultAccessedAt; tab.resultCacheKey = run.resultCacheKey; tab.resultCacheState = run.resultCacheState; tab.resultEstimatedBytes = run.resultEstimatedBytes ?? estimateQueryResultsBytes(run.result, run.results); tab.resultEvicted = run.resultEvicted; tab.queryAnalysis = run.queryAnalysis; tab.querySourceColumns = run.querySourceColumns; tab.resultColumnComments = run.resultColumnComments; tab.queryDisplaySourceColumns = run.queryDisplaySourceColumns; tab.queryEditabilityReason = run.queryEditabilityReason; tab.mongoEditTarget = run.mongoEditTarget; tab.tableMeta = run.tableMeta; touchResult(tab, Date.now(), { reuseEstimatedBytes: true }); } function restorePendingResultRun(tab: QueryTab, executionId: string): boolean { const runId = pendingResultRunRestores.get(executionId); pendingResultRunRestores.delete(executionId); if (!runId) return false; const run = tab.resultRuns?.find((item) => item.id === runId); if (!run || !resultRunHasPayload(run)) return false; projectResultRun(tab, run); evictInactiveResultRunPayloads(tab); return true; } async function restoreResultRunPayload(tab: QueryTab, runId: string) { const run = tab.resultRuns?.find((item) => item.id === runId); if (!run || run.result || run.results?.length) return run; const cacheKey = run.resultCacheKey ?? tab.resultCacheKey; if (!cacheKey) return run; const snapshot = await readTabResultSnapshot(cacheKey); const snapshotRun = snapshot?.resultRuns?.find((item) => item.id === runId); if (!snapshotRun) return run; const restoredRun = markQueryResultRunsRowsRaw([ { ...run, ...snapshotRun, id: run.id, title: run.title, sequence: run.sequence, sql: run.sql, createdAt: run.createdAt, pinned: run.pinned, activeResultIndex: run.activeResultIndex, resultCacheKey: run.resultCacheKey ?? snapshotRun.resultCacheKey, result: snapshotRun.result ? markQueryResultRowsRaw(snapshotRun.result) : undefined, results: snapshotRun.results ? markQueryResultsRowsRaw(snapshotRun.results) : undefined, resultCacheState: "memory" as const, resultEvicted: undefined, // 快照编解码会重建负载(如省略 session_id),落盘前的估算值不再对应 // 恢复后的对象,置空以便 projectResultRun 按当前负载重算 resultEstimatedBytes: undefined, }, ])[0]!; tab.resultRuns = tab.resultRuns?.map((item) => (item.id === runId ? restoredRun : item)); return restoredRun; } async function setActiveResultRun(id: string, runId: string, options: { evictInactive?: boolean } = {}) { const tab = findExecutionTab(id); if (!tab) return false; const existingRun = tab.resultRuns?.find((item) => item.id === runId); const run = existingRun && resultRunHasPayload(existingRun) ? existingRun : await restoreResultRunPayload(tab, runId); if (!run?.result && !run?.results?.length) return false; projectResultRun(tab, run); if (options.evictInactive !== false) evictInactiveResultRunPayloads(tab); return true; } function toggleResultRunPinned(id: string, runId: string): boolean | undefined { const tab = tabs.value.find((item) => item.id === id); const runIndex = tab?.resultRuns?.findIndex((run) => run.id === runId) ?? -1; if (!tab?.resultRuns || runIndex < 0) return undefined; const run = { ...tab.resultRuns[runIndex]!, pinned: tab.resultRuns[runIndex]!.pinned ? undefined : true }; tab.resultRuns[runIndex] = run; void persistResultRun(tab, run); return run.pinned === true; } function unpinAllResultRuns(id: string): number { const tab = tabs.value.find((item) => item.id === id); if (!tab?.resultRuns?.length) return 0; let changed = 0; tab.resultRuns = tab.resultRuns.map((run) => { if (!run.pinned) return run; changed += 1; const updated = { ...run, pinned: undefined }; void persistResultRun(tab, updated); return updated; }); return changed; } async function closeOtherResultRuns(id: string, keepRunId: string): Promise { const tab = tabs.value.find((item) => item.id === id); if (!tab?.resultRuns?.some((run) => run.id === keepRunId)) return false; const runIds = tab.resultRuns.filter((run) => run.id !== keepRunId).map((run) => run.id); if (runIds.length === 0) return false; // Do not delete otherwise usable runs until the run the user chose to keep // has been restored successfully. Disk-backed snapshots can be unavailable. if (!(await setActiveResultRun(id, keepRunId, { evictInactive: false }))) return false; for (const runId of runIds) { await removeResultRun(id, runId); } return true; } async function closeResultRunsToLeft(id: string, runId: string): Promise { const tab = tabs.value.find((item) => item.id === id); const runIndex = tab?.resultRuns?.findIndex((run) => run.id === runId) ?? -1; if (!tab?.resultRuns || runIndex <= 0) return false; if (!(await setActiveResultRun(id, runId, { evictInactive: false }))) return false; for (const run of tab.resultRuns.slice(0, runIndex)) { await removeResultRun(id, run.id); } return true; } async function closeResultRunsToRight(id: string, runId: string): Promise { const tab = tabs.value.find((item) => item.id === id); const runIndex = tab?.resultRuns?.findIndex((run) => run.id === runId) ?? -1; if (!tab?.resultRuns || runIndex < 0 || runIndex >= tab.resultRuns.length - 1) return false; if (!(await setActiveResultRun(id, runId, { evictInactive: false }))) return false; for (const run of tab.resultRuns.slice(runIndex + 1)) { await removeResultRun(id, run.id); } return true; } async function removeResultRun(id: string, runId: string) { const tab = tabs.value.find((t) => t.id === id); const runIndex = tab?.resultRuns?.findIndex((run) => run.id === runId) ?? -1; if (!tab || !tab.resultRuns || runIndex < 0) return false; const removedRun = tab.resultRuns[runIndex]; if (removedRun?.resultSessionId) void closeResultRunSession(tab, removedRun); if (removedRun?.resultCacheKey) void deleteTabResultSnapshot(removedRun.resultCacheKey); if (removedRun) clearResultRunPayload(removedRun); const wasActive = tab.activeResultRunId === runId; const remainingRuns = tab.resultRuns.filter((run) => run.id !== runId); tab.resultRuns = remainingRuns; if (!wasActive) return true; const adjacentIndex = Math.min(runIndex, remainingRuns.length - 1); for (let offset = 0; offset < remainingRuns.length; offset += 1) { const candidate = remainingRuns[(adjacentIndex + offset) % remainingRuns.length]; // Disk-backed runs may have missing or unreadable snapshots; keep searching before clearing output. if (candidate && (await setActiveResultRun(id, candidate.id))) return true; } tab.activeResultRunId = undefined; clearResultPayload(tab); return true; } async function closeQueryResult(id: string) { const tab = tabs.value.find((item) => item.id === id); if (!tab || tab.mode !== "query" || tab.isExecuting) return false; if (tab.activeResultRunId) return removeResultRun(id, tab.activeResultRunId); if (!tab.result && !tab.results?.length && !tab.resultEvicted) return false; const closeSession = closeResultSession(tab); releaseTabResultObjectPayloads(tab); clearResultPayload(tab); await closeSession; return true; } async function clearQueryResults(id: string) { const tab = tabs.value.find((item) => item.id === id); if (!tab || tab.mode !== "query" || tab.isExecuting) return false; const resultRuns = tab.resultRuns ?? []; if (!tab.result && !tab.results?.length && !tab.resultEvicted && resultRuns.length === 0) return false; const closedSessionIds = new Set(); const currentSessionId = tab.resultSessionId ?? tab.result?.session_id; if (currentSessionId) closedSessionIds.add(currentSessionId); const closeOperations = [closeResultSession(tab)]; for (const run of resultRuns) { if (run.resultCacheKey) void deleteTabResultSnapshot(run.resultCacheKey); if (!run.resultSessionId || closedSessionIds.has(run.resultSessionId)) continue; closedSessionIds.add(run.resultSessionId); closeOperations.push(closeResultRunSession(tab, run)); } releaseTabResultObjectPayloads(tab); clearResultRuns(tab); clearResultPayload(tab); await Promise.all(closeOperations); return true; } function nextResultRunSequence(tab: QueryTab): number { return (tab.resultRuns?.reduce((max, run) => Math.max(max, run.sequence), 0) ?? 0) + 1; } async function closeResultRunSession(tab: QueryTab, run: NonNullable[number]) { if (!run.resultSessionId) return; const target = run.multiDbExecution?.target; const sessionOwner = { ...tab, id: `result-run:${run.id}`, connectionId: target?.connectionId ?? tab.connectionId, database: target?.database ?? tab.database, schema: target?.schema ?? tab.schema, catalog: target?.catalog ?? tab.catalog, result: run.result, resultSessionId: run.resultSessionId, resultClientSessionId: run.resultClientSessionId, activeResultRunId: undefined, } as QueryTab; await closeResultSession(sessionOwner); run.resultSessionId = undefined; run.resultClientSessionId = undefined; } function persistResultRun(tab: QueryTab, run: NonNullable[number]): Promise { // An evicted run only has metadata in memory. Writing it back here would // replace its valid disk snapshot with an empty payload. if (!resultRunHasPayload(run)) return Promise.resolve(false); const key = run.resultCacheKey ?? resultRunCacheKey(tab.id, run.id); run.resultCacheKey = key; run.resultCacheState = "memory"; return writeTabResultSnapshot( key, { result: run.result, results: run.results, activeResultIndex: run.activeResultIndex, resultEditorFingerprint: run.resultEditorFingerprint, resultRuns: [run], activeResultRunId: run.id, queryAnalysis: run.queryAnalysis, querySourceColumns: run.querySourceColumns, resultColumnComments: run.resultColumnComments, queryDisplaySourceColumns: run.queryDisplaySourceColumns, queryEditabilityReason: run.queryEditabilityReason, tableMeta: run.tableMeta, resultPageSql: run.resultPageSql, resultPageLimit: run.resultPageLimit, resultPageOffset: run.resultPageOffset, resultCountSql: run.resultCountSql, resultTotalRowCount: run.resultTotalRowCount, cachedAt: Date.now(), }, tab.connectionId, ); } function evictInactiveResultRunPayloads(tab: QueryTab) { const activeRunId = tab.activeResultRunId; if (!activeRunId || !tab.resultRuns?.length) return; for (const run of tab.resultRuns) { if (run.id === activeRunId || !resultRunHasPayload(run)) continue; const runId = run.id; void closeResultRunSession(tab, run) .then(() => persistResultRun(tab, run)) .then((cached) => { const currentRun = tab.resultRuns?.find((item) => item.id === runId); if (!cached || !currentRun || currentRun.id === tab.activeResultRunId || !resultRunHasPayload(currentRun)) return; if (tab.result === currentRun.result || (currentRun.results && tab.results === currentRun.results)) return; clearResultRunPayload(currentRun, { evicted: true }); }); } } type ResultRunCaptureOptions = { reuseResultCacheKey?: boolean; title?: string; multiDbExecution?: MultiDbResultRunExecution; persist?: boolean; }; function captureDisplayedResultRun(tab: QueryTab, sql: string, createdAt = Date.now(), options: ResultRunCaptureOptions = {}) { if (tab.mode !== "query" || !tab.result) return; const sequence = nextResultRunSequence(tab); const run: NonNullable[number] = { id: uuid(), title: options.title ?? `Run ${sequence}`, sequence, sql, createdAt, result: tab.result, results: tab.results, activeResultIndex: tab.activeResultIndex, resultGridRevision: tab.resultGridRevision, batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution), resultBaseSql: tab.resultBaseSql, resultEditorFingerprint: tab.resultEditorFingerprint, resultSortedSql: tab.resultSortedSql, resultSortColumn: tab.resultSortColumn, resultSortColumnIndex: tab.resultSortColumnIndex, resultSortDirection: tab.resultSortDirection, resultSortMode: tab.resultSortMode, resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows, resultLocalSortOriginalLargeValueCells: tab.resultLocalSortOriginalLargeValueCells, resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments, resultLocalSortOriginalMongoCopyDocuments: tab.resultLocalSortOriginalMongoCopyDocuments, orderByInput: tab.orderByInput, resultPageSql: tab.resultPageSql, resultPageLimit: tab.resultPageLimit, resultPageOffset: tab.resultPageOffset, resultCountSql: tab.resultCountSql, resultTotalRowCount: tab.resultTotalRowCount, resultTotalRowCountLoading: tab.resultTotalRowCountLoading, resultSessionId: tab.resultSessionId, resultClientSessionId: tab.resultClientSessionId, resultAccessedAt: tab.resultAccessedAt, resultEstimatedBytes: tab.resultEstimatedBytes, resultCacheKey: options.reuseResultCacheKey === false ? undefined : tab.resultCacheKey, resultCacheState: tab.resultCacheState, resultEvicted: tab.resultEvicted, queryAnalysis: tab.queryAnalysis, querySourceColumns: tab.querySourceColumns, resultColumnComments: tab.resultColumnComments, queryDisplaySourceColumns: tab.queryDisplaySourceColumns, queryEditabilityReason: tab.queryEditabilityReason, mongoEditTarget: tab.mongoEditTarget, tableMeta: tab.tableMeta, multiDbExecution: options.multiDbExecution, }; if (options.persist !== false) void persistResultRun(tab, run); tab.resultRuns = [...(tab.resultRuns ?? []), run]; tab.activeResultRunId = run.id; if (options.reuseResultCacheKey === false) { tab.resultCacheKey = run.resultCacheKey; tab.resultCacheState = run.resultCacheState; } evictInactiveResultRunPayloads(tab); } function captureMultiDbExecutionWorkerResult(sourceTabId: string, workerId: string, sql: string, execution: MultiDbResultRunExecution): string | undefined { const source = tabs.value.find((tab) => tab.id === sourceTabId); const worker = multiDbExecutionWorkers.get(workerId); if (!source || !worker?.result) return undefined; // Preserve a pre-existing displayed result once, so the source tab can // show the original result alongside the multi-db result pages. if (source.result && !source.activeResultRunId) { captureDisplayedResultRun(source, source.resultBaseSql ?? source.lastExecutedSql ?? source.sql); } const title = execution.title ?? (execution.target.database || execution.target.connectionId); captureDisplayedResultRun(worker, sql, Date.now(), { title, multiDbExecution: execution, persist: false }); const workerRun = worker.resultRuns?.find((run) => run.id === worker.activeResultRunId); if (!workerRun) return undefined; const runId = uuid(); const run = { ...workerRun, id: runId, title, sequence: nextResultRunSequence(source), multiDbExecution: execution, resultCacheKey: undefined, resultCacheState: "memory" as const, resultEvicted: undefined, }; source.resultRuns = [...(source.resultRuns ?? []), run]; projectResultRun(source, run); void persistResultRun(source, run); evictInactiveResultRunPayloads(source); return runId; } function toggleResultAutoSave(id: string): boolean { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.mode !== "query") return false; tab.resultAutoSave = tab.resultAutoSave ? undefined : true; if (tab.resultAutoSave && tab.result && !tab.activeResultRunId) { captureDisplayedResultRun(tab, tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql); } return tab.resultAutoSave === true; } function syncActiveResultRunFromDisplayed(tab: QueryTab, sql?: string) { if (!tab.activeResultRunId || !tab.resultRuns?.length) return; const index = tab.resultRuns.findIndex((run) => run.id === tab.activeResultRunId); if (index < 0) return; const run = { ...tab.resultRuns[index], ...(sql ? { sql } : {}), result: tab.result, results: tab.results, activeResultIndex: tab.activeResultIndex, resultGridRevision: tab.resultGridRevision, batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution), resultBaseSql: tab.resultBaseSql, resultEditorFingerprint: tab.resultEditorFingerprint, resultSortedSql: tab.resultSortedSql, resultSortColumn: tab.resultSortColumn, resultSortColumnIndex: tab.resultSortColumnIndex, resultSortDirection: tab.resultSortDirection, resultSortMode: tab.resultSortMode, resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows, resultLocalSortOriginalLargeValueCells: tab.resultLocalSortOriginalLargeValueCells, resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments, resultLocalSortOriginalMongoCopyDocuments: tab.resultLocalSortOriginalMongoCopyDocuments, orderByInput: tab.orderByInput, resultPageSql: tab.resultPageSql, resultPageLimit: tab.resultPageLimit, resultPageOffset: tab.resultPageOffset, resultCountSql: tab.resultCountSql, resultTotalRowCount: tab.resultTotalRowCount, resultTotalRowCountLoading: tab.resultTotalRowCountLoading, resultSessionId: tab.resultSessionId, resultClientSessionId: tab.resultClientSessionId, resultAccessedAt: tab.resultAccessedAt, resultEstimatedBytes: tab.resultEstimatedBytes, resultCacheKey: tab.resultCacheKey, resultCacheState: tab.resultCacheState, resultEvicted: tab.resultEvicted, queryAnalysis: tab.queryAnalysis, querySourceColumns: tab.querySourceColumns, resultColumnComments: tab.resultColumnComments, queryDisplaySourceColumns: tab.queryDisplaySourceColumns, queryEditabilityReason: tab.queryEditabilityReason, mongoEditTarget: tab.mongoEditTarget, tableMeta: tab.tableMeta, }; void persistResultRun(tab, run); tab.resultRuns[index] = run; } function syncDisplayedResultRun(tab: QueryTab, sql: string, captureNewRun = false, options: ResultRunCaptureOptions = {}) { if (tab.mode !== "query" || !tab.result) return; if (captureNewRun) { captureDisplayedResultRun(tab, sql, Date.now(), { ...options, reuseResultCacheKey: false }); } else if (tab.activeResultRunId) { syncActiveResultRunFromDisplayed(tab, sql); } else if (tab.resultAutoSave) { captureDisplayedResultRun(tab, sql); } } function assignDisplayedResult(tab: QueryTab, result: QueryResult) { tab.result = markQueryResultRowsRaw(result); if (tab.results?.length) { const activeIndex = tab.activeResultIndex ?? 0; if (activeIndex >= 0 && activeIndex < tab.results.length) { tab.results[activeIndex] = tab.result; } } } function sortTabResultLocally(id: string, column: string, columnIndex: number, direction: DataGridSortDirection | null) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.result) return; if (!tab.resultLocalSortOriginalRows) { tab.resultLocalSortOriginalRows = tab.result.rows.slice(); tab.resultLocalSortOriginalLargeValueCells = tab.result.large_value_cells?.map((cell) => ({ ...cell })); tab.resultLocalSortOriginalMongoDocuments = tab.result.mongo_documents?.slice(); tab.resultLocalSortOriginalMongoCopyDocuments = tab.result.mongo_copy_documents?.slice(); } const originalRows = tab.resultLocalSortOriginalRows; const columnType = tab.result.column_types?.[columnIndex]; const rowIndexes = direction ? sortDataGridRowIndexes(originalRows, columnIndex, direction, columnType) : originalRows.map((_, index) => index); const rows = rowIndexes.map((index) => originalRows[index]!); const originalMongoDocuments = tab.resultLocalSortOriginalMongoDocuments; const mongo_documents = originalMongoDocuments ? rowIndexes.map((index) => originalMongoDocuments[index]) : undefined; const originalMongoCopyDocuments = tab.resultLocalSortOriginalMongoCopyDocuments; const mongo_copy_documents = originalMongoCopyDocuments ? rowIndexes.map((index) => originalMongoCopyDocuments[index]) : undefined; const large_value_cells = remapLargeValueCells(tab.resultLocalSortOriginalLargeValueCells, rowIndexes); assignDisplayedResult(tab, { ...tab.result, rows, large_value_cells, mongo_documents, mongo_copy_documents }); tab.resultSortColumn = direction ? column : undefined; tab.resultSortColumnIndex = direction ? columnIndex : undefined; tab.resultSortDirection = direction ?? undefined; tab.resultSortMode = direction ? "local" : undefined; tab.resultSortedSql = undefined; if (!direction) { tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; } // 本地排序只是重排既有行/文档,字节规模不变,可复用估算值 touchResult(tab, Date.now(), { reuseEstimatedBytes: true }); syncDisplayedResultRun(tab, tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql); } function resultRunHasPayload(run: NonNullable[number]): boolean { return !!run.result || !!run.results?.length; } function resultSnapshotHasPayload(snapshot: NonNullable>): boolean { return !!snapshot.result || !!snapshot.results?.length || !!snapshot.resultRuns?.some(resultRunHasPayload); } async function evictCachedResult(tab: QueryTab) { await closeResultSession(tab); const cacheKey = tabResultCacheKey(tab.id); const cached = await writeTabResultSnapshot(cacheKey, buildTabResultSnapshot(tab), tab.connectionId); tab.resultCacheKey = cached ? cacheKey : undefined; tab.resultCacheState = cached ? "disk" : "missing"; clearResultPayload(tab, { evicted: true }); } function applyRestoredOpenTabs(restored: { tabs: QueryTab[]; activeTabId: string | null }) { const connectionStore = useConnectionStore(); for (const tab of restored.tabs) { const connection = connectionStore.getConfig(tab.connectionId); if (tab.mode === "query" && tab.autoCommit === undefined) { tab.autoCommit = defaultAutoCommitForDbTypeWithSetting(connection?.db_type); } else if (tab.mode === "data" && connection) { tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema); } } tabs.value = restored.tabs; activeTabId.value = restored.activeTabId; activeTabHistory.value = restored.activeTabId ? [restored.activeTabId] : []; for (const tab of restored.tabs) { if (tab.mode === "data") void deleteTabResultSnapshot(tabResultCacheKey(tab.id)); } } function scheduleResultCacheMaintenance() { const maintain = () => { const liveKeys = tabs.value.flatMap((tab) => [tab.resultCacheKey, ...(tab.resultRuns?.map((run) => run.resultCacheKey) ?? [])]).filter((key): key is string => !!key); void pruneTabResultSnapshots(liveKeys).catch((error) => console.warn("[DBX][result-cache:maintenance:error]", error)); }; if (typeof requestIdleCallback !== "undefined") requestIdleCallback(maintain, { timeout: 5000 }); else if (typeof window !== "undefined") window.setTimeout(maintain, 0); else setTimeout(maintain, 0); } async function initOpenTabs(options: { validConnectionIds?: Iterable } = {}) { if (isOpenTabsLoaded.value) return; const saved = await api.loadOpenTabsState().catch(() => null); if (saved?.tabs && Array.isArray(saved.tabs)) { const restored = restoreSavedTabsFromPayload(saved, options); applyRestoredOpenTabs(restored); if (useSettingsStore().editorSettings.openTabsRestoreMode === "none") { // Restore is explicitly disabled, so stale saved payloads should not // reappear if the user later changes the setting. clearLegacySavedTabs(); await saveTabs(tabs.value, activeTabId.value).catch(() => undefined); } isOpenTabsLoaded.value = true; scheduleResultCacheMaintenance(); return; } const legacy = loadLegacySavedTabs(); if (legacy.rawTabs || legacy.rawActiveTabId) { const restored = restoreLegacySavedTabs(options); applyRestoredOpenTabs(restored); if (useSettingsStore().editorSettings.openTabsRestoreMode === "none") { // Restore is explicitly disabled, so keeping the legacy startup payload // would resurrect old tabs if the user later changes the setting. clearLegacySavedTabs(); isOpenTabsLoaded.value = true; scheduleResultCacheMaintenance(); return; } try { await saveTabs(tabs.value, activeTabId.value); // Keep old desktop installs readable until the async store has the // migrated state; only then remove the synchronous startup payload. clearLegacySavedTabs(); } catch { /* keep legacy values for a later migration attempt */ } } isOpenTabsLoaded.value = true; scheduleResultCacheMaintenance(); } 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, externalSqlPath: t.externalSqlPath, externalSqlFileVersion: t.externalSqlFileVersion, externalSqlIgnoredFileVersion: t.externalSqlIgnoredFileVersion, externalSqlFileMissing: t.externalSqlFileMissing, lastExecutedSql: t.lastExecutedSql, resultBaseSql: t.resultBaseSql, resultSortedSql: t.resultSortedSql, resultSortColumn: t.resultSortColumn, resultSortColumnIndex: t.resultSortColumnIndex, resultSortDirection: t.resultSortDirection, resultSortMode: t.resultSortMode, orderByInput: t.orderByInput, resultPageLimit: t.resultPageLimit, resultPageOffset: t.resultPageOffset, whereInput: t.whereInput, pinned: t.pinned, mode: t.mode, autoCommit: t.autoCommit, resultAutoSave: t.resultAutoSave, structureTableName: t.structureTableName, objectBrowser: t.objectBrowser, objectSource: t.objectSource, tableMeta: t.tableMeta, mongoEditTarget: t.mongoEditTarget, resultEvicted: t.resultEvicted, resultCacheKey: t.resultCacheKey, // Keep the watch dependency limited to the metadata that is serialized // for each result run, without tracking the potentially large payload. resultRuns: t.resultRuns?.map((run) => ({ id: run.id, title: run.title, sequence: run.sequence, sql: run.sql, createdAt: run.createdAt, pinned: run.pinned, activeResultIndex: run.activeResultIndex, resultCacheKey: run.resultCacheKey, resultEvicted: run.resultEvicted, })), activeResultRunId: t.activeResultRunId, })), ); const storePersistGeneration = ++persistGeneration; watch( [_persistSnapshot, activeTabId], () => { if (storePersistGeneration !== persistGeneration) return; if (persistTimer) clearTimeout(persistTimer); persistTimer = setTimeout(() => { void saveTabs(tabs.value, activeTabId.value).catch(() => {}); persistTimer = null; }, 300); }, { flush: "post" }, ); onScopeDispose(() => { if (persistTimer) clearTimeout(persistTimer); persistTimer = null; void disposeAllSqlServerActivityTraces(); }); // Immediately flush any pending debounced persist so the on-disk content // reflects the latest in-memory tabs without waiting for the 300ms debounce. // Lets callers (e.g. tests that reload the store) read back persisted state // deterministically instead of racing the debounce timer. function flushPendingPersist(): Promise { if (storePersistGeneration !== persistGeneration) return Promise.resolve(); if (persistTimer) { clearTimeout(persistTimer); persistTimer = null; } return saveTabs(tabs.value, activeTabId.value); } function findTabByIdentity(connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, catalog?: string) { if (mode === "meilisearch-system") { return tabs.value.find((tab) => tab.connectionId === connectionId && tab.mode === mode); } return tabs.value.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || "")); } function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string, catalog?: string, options: { forceNew?: boolean; activate?: boolean; forceWordWrap?: boolean; insertAfterActive?: boolean } = {}) { if (title && !options.forceNew) { const existing = findTabByIdentity(connectionId, database, title, mode, schema, catalog); if (existing) { switchTab(existing.id); return existing.id; } } const id = uuid(); const dbType = useConnectionStore().getConfig(connectionId)?.db_type; const tab: QueryTab = { id, title: title || `query_${tabs.value.length + 1}`, customTitle: mode === "query" && title ? true : undefined, forceWordWrap: options.forceWordWrap, connectionId, database, schema, catalog, sql: initialSql ?? "", isExecuting: false, isCancelling: false, isExplaining: false, mode, ...(mode === "query" ? { autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType) } : {}), }; if (mode === "query") tab.originalSql = initialSql ?? ""; const activeIndex = options.insertAfterActive ? tabs.value.findIndex((item) => item.id === activeTabId.value) : -1; if (activeIndex >= 0) tabs.value.splice(activeIndex + 1, 0, tab); else tabs.value.push(tab); if (options.activate !== false) activeTabId.value = id; return id; } function openObjectSourceTab(options: OpenObjectSourceTabOptions) { const existing = tabs.value.find( (tab) => tab.mode === "query" && tab.connectionId === options.connectionId && tab.database === options.database && (tab.schema || "") === (options.schema || "") && (tab.catalog || "") === (options.catalog || "") && tab.objectSource?.name === options.objectSource.name && tab.objectSource.objectType === options.objectSource.objectType && (tab.objectSource.schema || "") === (options.objectSource.schema || "") && (tab.objectSource.signature || "") === (options.objectSource.signature || ""), ); if (existing) { switchTab(existing.id); if (!isTabDirty(existing)) { updateSql(existing.id, options.sql); markTabClean(existing); } return existing.id; } const id = createTab(options.connectionId, options.database, options.title, "query", options.schema, options.sql, options.catalog, { forceNew: true }); setObjectSource(id, options.objectSource); return id; } function showExecutedQueryResults(connectionId: string, database: string, sql: string, queryResults: QueryResult[]) { const id = createTab(connectionId, database, undefined, "query", undefined, sql); const tab = tabs.value.find((item) => item.id === id); if (!tab) return id; const results = markQueryResultsRowsRaw(queryResults); const firstDataResult = results.findIndex((result) => result.columns.length > 0); const activeIndex = firstDataResult >= 0 ? firstDataResult : 0; tab.lastExecutedSql = sql; tab.resultBaseSql = sql; tab.results = results.length > 1 ? results : undefined; tab.activeResultIndex = results.length > 1 ? activeIndex : undefined; tab.result = results[activeIndex]; tab.isExecuting = false; tab.isCancelling = false; tab.executionId = undefined; tab.queryExecutionStartedAt = undefined; if (tab.result) touchResult(tab); return id; } function refreshExternalSqlFileTitles() { const externalTabs = tabs.value.filter((tab) => tab.mode === "query" && tab.externalSqlPath); const titles = externalSqlFileDisplayTitles(externalTabs.map((tab) => tab.externalSqlPath!)); externalTabs.forEach((tab, index) => { tab.title = titles[index]; tab.customTitle = true; }); } function openExternalSqlFile(connectionId: string, database: string, path: string, sql: string, version?: QueryTab["externalSqlFileVersion"], catalog?: string) { const normalizedPath = normalizeExternalSqlPath(path); const existing = tabs.value.find((tab) => tab.mode === "query" && tab.externalSqlPath && normalizeExternalSqlPath(tab.externalSqlPath) === normalizedPath); if (existing) { switchTab(existing.id); return existing.id; } // File-backed tabs are identified by their full path, not their basename. // Bypassing createTab avoids overwriting another file with the same name. const id = uuid(); const dbType = useConnectionStore().getConfig(connectionId)?.db_type; const tab: QueryTab = { id, title: "", customTitle: true, connectionId, database, catalog, sql, originalSql: sql, externalSqlPath: path, externalSqlFileVersion: version, isExecuting: false, isCancelling: false, isExplaining: false, mode: "query", autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), }; tabs.value.push(tab); activeTabId.value = id; refreshExternalSqlFileTitles(); return id; } function openObjectBrowser(connectionId: string, database: string, schema?: string, catalog?: string, eventName?: string, eventReadOnly = false, initialObjectFilter?: "tables" | "events", eventCreateRequestId?: number) { const title = catalog ? `${catalog}.${database} objects` : schema ? `${schema} objects` : `${database} objects`; const existing = tabs.value.find((tab) => tab.mode === "objects" && tab.connectionId === connectionId && tab.database === database && (tab.objectBrowser?.catalog || "") === (catalog || "") && (tab.objectBrowser?.schema || "") === (schema || "")); if (existing) { if (eventCreateRequestId !== undefined) { // 新建事件:显式 CREATE 请求优先,并清掉可能残留的"编辑已有事件"状态, // 保证同一 tab 被复用时每次点击都能重新进入 CREATE 编辑器(请求号单调递增)。 existing.objectBrowser = { ...existing.objectBrowser, eventName: undefined, eventReadOnly: false, eventOpenRequestId: undefined, eventCreateRequestId, initialObjectFilter: initialObjectFilter ?? "events", }; } else if (eventName) { existing.objectBrowser = { ...existing.objectBrowser, eventName, eventReadOnly, eventCreateRequestId: undefined, initialObjectFilter: initialObjectFilter ?? (eventName ? "events" : existing.objectBrowser?.initialObjectFilter), eventOpenRequestId: (existing.objectBrowser?.eventOpenRequestId ?? 0) + 1, }; } switchTab(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: { catalog, schema, objectType: "tables", eventName: eventCreateRequestId !== undefined ? undefined : eventName, eventReadOnly: eventCreateRequestId !== undefined ? false : eventReadOnly, initialObjectFilter: initialObjectFilter ?? (eventName || eventCreateRequestId !== undefined ? "events" : undefined), eventOpenRequestId: eventName ? 1 : undefined, eventCreateRequestId, }, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openDatabaseBrowser(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "databases" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const id = uuid(); tabs.value.push({ id, title: "Databases", connectionId, database: "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "databases", }); activeTabId.value = id; return id; } function openDriverProfileWorkspace(connectionId: string, database: string, title: string, mode: QueryTab["mode"], tabScope: DriverProfileWorkspaceScope = "database", workspaceBranch?: string) { const existing = tabs.value.find((tab) => tab.mode === mode && tab.connectionId === connectionId && (tabScope === "connection" || tab.database === database)); if (existing) { if (existing.database !== database) updateDatabase(existing.id, database); existing.workspaceBranch = workspaceBranch; switchTab(existing.id); return existing.id; } const id = uuid(); tabs.value.push({ id, title, connectionId, database, workspaceBranch, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode, }); activeTabId.value = id; return id; } function switchTab(tabId: string) { activeTabId.value = tabId; settingsStore.settingsPageActive = false; if (typeof window !== "undefined") window.dispatchEvent(new Event(QUERY_SURFACE_ACTIVATION_EVENT)); } function openUserAdmin(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "users" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: t("userAdmin.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "users", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openProcessList(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "processlist" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: conn?.name ? `${conn.name} - ${t("processList.title")}` : t("processList.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "processlist", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openSqlServerActivityTrace(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "sqlserver-trace" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: conn?.name ? `${conn.name} - ${t("sqlServerTrace.title")}` : t("sqlServerTrace.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "sqlserver-trace", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openMysqlDashboard(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "mysql-dashboard" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: conn?.name ? `${conn.name} - ${t("serverDashboard.title")}` : t("serverDashboard.title"), connectionId, database: "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "mysql-dashboard", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openPostgresDashboard(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "postgres-dashboard" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: conn?.name ? `${conn.name} - ${t("serverDashboard.title")}` : t("serverDashboard.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "postgres-dashboard", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openNacosDashboard(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "nacos-dashboard" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: conn?.name ? `${conn.name} - ${t("serverDashboard.title")}` : t("serverDashboard.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "nacos-dashboard", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openDamengJobAdmin(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "dameng-jobs" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: t("damengJobAdmin.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "dameng-jobs", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openDamengUsers(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "dameng-users" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: t("damengUserAdmin.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "dameng-users", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openDamengRoles(connectionId: string) { const existing = tabs.value.find((tab) => tab.mode === "dameng-roles" && tab.connectionId === connectionId); if (existing) { switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: t("damengRoleAdmin.title"), connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "dameng-roles", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openMongoBucket(connectionId: string, database: string, bucketName: string) { const title = `${database}.${bucketName}`; const existing = tabs.value.find((tab) => tab.mode === "mongo-bucket" && tab.connectionId === connectionId && tab.database === database && tab.mongoBucket?.bucketName === bucketName); if (existing) { switchTab(existing.id); return existing.id; } const id = uuid(); const tab: QueryTab = { id, title, connectionId, database, sql: bucketName, isExecuting: false, isCancelling: false, isExplaining: false, mode: "mongo-bucket", mongoBucket: { bucketName, }, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openMongoGridFs(connectionId: string, database: string) { const existing = tabs.value.find((tab) => tab.mode === "mongo-gridfs" && tab.connectionId === connectionId && tab.database === database); if (existing) { switchTab(existing.id); return existing.id; } const id = uuid(); const tab: QueryTab = { id, title: "GridFS", connectionId, database, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "mongo-gridfs", }; tabs.value.push(tab); activeTabId.value = id; return id; } function openMqAdmin(connectionId: string, target?: { tenant?: string; initialTab?: QueryTab["mqInitialTab"] }) { const existing = tabs.value.find((tab) => tab.mode === "mq" && tab.connectionId === connectionId); if (existing) { if (target?.tenant) existing.mqTenant = target.tenant; if (target?.initialTab) existing.mqInitialTab = target.initialTab; switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: `${conn?.name || "Message Queue"} Admin`, connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "mq", mqTenant: target?.tenant, mqInitialTab: target?.initialTab, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openNacosAdmin(connectionId: string, target?: { namespace?: string; namespaceName?: string; dataId?: string; group?: string; keyword?: string }) { const namespace = target?.namespace ?? ""; const namespaceName = target?.namespaceName || (namespace ? namespace : "public"); const existing = tabs.value.find((tab) => tab.mode === "nacos" && tab.connectionId === connectionId && (tab.nacosNamespace || "") === namespace); if (existing) { existing.nacosNamespaceName = namespaceName; if (target?.dataId) { existing.nacosTargetDataId = target.dataId; existing.nacosTargetGroup = target.group || "DEFAULT_GROUP"; existing.nacosTargetKeyword = target.keyword; existing.nacosTargetRequestId = (existing.nacosTargetRequestId ?? 0) + 1; } if (!existing.customTitle) existing.title = `${useConnectionStore().getConfig(connectionId)?.name || "Nacos"}:${namespaceName}`; switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: `${conn?.name || "Nacos"}:${namespaceName}`, connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "nacos", nacosNamespace: namespace, nacosNamespaceName: namespaceName, nacosTargetDataId: target?.dataId, nacosTargetGroup: target?.group, nacosTargetKeyword: target?.keyword, nacosTargetRequestId: target?.dataId ? 1 : undefined, }; tabs.value.push(tab); activeTabId.value = id; return id; } function openMqttAdmin(connectionId: string, target?: { initialTopic?: string }) { const existing = tabs.value.find((tab) => tab.mode === "mqtt" && tab.connectionId === connectionId); if (existing) { if (target?.initialTopic) existing.mqttInitialTopic = target.initialTopic; switchTab(existing.id); return existing.id; } const conn = useConnectionStore().getConfig(connectionId); const id = uuid(); const tab: QueryTab = { id, title: "connection.mqttConsoleTitle", connectionId, database: conn?.database || "", sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "mqtt", mqttInitialTopic: target?.initialTopic, }; tabs.value.push(tab); activeTabId.value = id; return id; } function clearNacosNavigationTarget(connectionId: string, namespace: string, requestId?: number) { const tab = tabs.value.find((candidate) => candidate.mode === "nacos" && candidate.connectionId === connectionId && (candidate.nacosNamespace || "") === namespace); if (!tab || (requestId !== undefined && tab.nacosTargetRequestId !== requestId)) return; tab.nacosTargetDataId = undefined; tab.nacosTargetGroup = undefined; tab.nacosTargetKeyword = undefined; } function applyTableStructureInitialTab(tab: QueryTab, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget) { if (!initialTab && !initialTarget?.name) return; if (initialTab) tab.structureInitialTab = initialTab; tab.structureInitialTarget = initialTarget?.name ? initialTarget : undefined; tab.structureInitialTabRequestId = (tab.structureInitialTabRequestId ?? 0) + 1; } function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget, catalog?: string) { const resolvedTableName = tableName || ""; if (resolvedTableName) { const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.catalog || "") === (catalog || "") && (tab.schema || "") === (schema || "") && (tab.structureTableName || "") === resolvedTableName); if (existing) { applyTableStructureInitialTab(existing, initialTab, initialTarget); switchTab(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, catalog, sql: "", isExecuting: false, isCancelling: false, isExplaining: false, mode: "structure", structureTableName: resolvedTableName, structureInitialTab: initialTab, structureInitialTabRequestId: initialTab || initialTarget?.name ? 1 : undefined, structureInitialTarget: initialTarget?.name ? initialTarget : undefined, }; tabs.value.push(tab); activeTabId.value = id; return id; } function isTabDirty(tab: QueryTab): boolean { if (tab.mode === "structure") { // Legacy persisted structure drafts predate the dirty flag; treat them as dirty until the editor rehydrates them. return !!tab.structureDraft && tab.structureDraft.dirty !== false; } if (tab.mode !== "query") return false; if (!tab.externalSqlPath && !tab.sql.trim()) return false; const original = tab.originalSql; if (original === undefined) return !!tab.savedSqlId; return tab.sql !== original; } const hasDirtyTabs = computed(() => tabs.value.some((tab) => isTabDirty(tab))); const shouldConfirmUnsavedSqlClose = computed(() => useSettingsStore().editorSettings.confirmUnsavedSqlClose); const keepUnsavedTabsDraftsOnAppClose = computed(() => useSettingsStore().editorSettings.appCloseUnsavedTabsMode === "keep-drafts"); const requiresAppCloseDraftPersist = computed(() => shouldConfirmUnsavedSqlClose.value && keepUnsavedTabsDraftsOnAppClose.value && tabs.value.some((tab) => tab.mode === "query" && isTabDirty(tab))); function shouldConfirmTabOnAppClose(tab: QueryTab): boolean { if (!shouldConfirmTabClose(tab)) return false; return !keepUnsavedTabsDraftsOnAppClose.value || tab.mode !== "query"; } const closeConfirmDirtyTabIds = computed(() => { if (isConfirmingAppClose.value) return tabs.value.filter((tab) => shouldConfirmTabOnAppClose(tab)).map((tab) => tab.id); if (pendingBatchCloseTabIds.value) { return pendingBatchCloseTabIds.value .map((id) => tabs.value.find((tab) => tab.id === id)) .filter((tab): tab is QueryTab => !!tab && shouldConfirmTabClose(tab)) .map((tab) => tab.id); } const pendingTab = pendingCloseTabId.value ? tabs.value.find((tab) => tab.id === pendingCloseTabId.value) : undefined; return pendingTab && isTabDirty(pendingTab) ? [pendingTab.id] : []; }); function showDirtyTabCloseConfirm(tab: QueryTab, context: CloseConfirmContext) { pendingCloseTabId.value = tab.id; closeConfirmContext.value = context; activeTabId.value = tab.id; showCloseConfirm.value = true; } function markTabClean(tab: QueryTab | undefined) { if (tab) tab.originalSql = tab.sql; } function applyExternalSqlFileSnapshot(id: string, sql: string, version: NonNullable) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.externalSqlPath) return; tab.sql = sql; tab.originalSql = sql; tab.externalSqlFileVersion = version; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; } function markExternalSqlFileSaved(id: string, version: NonNullable) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.externalSqlPath) return; tab.originalSql = tab.sql; tab.externalSqlFileVersion = version; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; } function updateExternalSqlFileVersion(id: string, version: NonNullable) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.externalSqlPath) return; tab.externalSqlFileVersion = version; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; } function ignoreExternalSqlFileVersion(id: string, version: NonNullable) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.externalSqlPath) return; tab.externalSqlIgnoredFileVersion = version; tab.externalSqlFileMissing = undefined; } function acknowledgeExternalSqlFileMissing(id: string) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.externalSqlPath) return; tab.externalSqlFileMissing = true; } function relocateExternalSqlFilePath(previousPath: string, nextPath: string, version?: QueryTab["externalSqlFileVersion"]) { const previous = normalizeExternalSqlPath(previousPath); if (!previous) return; for (const tab of tabs.value) { if (tab.mode !== "query" || !tab.externalSqlPath || normalizeExternalSqlPath(tab.externalSqlPath) !== previous) continue; tab.externalSqlPath = nextPath; if (version) tab.externalSqlFileVersion = version; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; } refreshExternalSqlFileTitles(); } function markExternalSqlFileMissingForPath(path: string) { const normalizedPath = normalizeExternalSqlPath(path); if (!normalizedPath) return; for (const tab of tabs.value) { if (tab.mode === "query" && tab.externalSqlPath && normalizeExternalSqlPath(tab.externalSqlPath) === normalizedPath) { tab.externalSqlFileMissing = true; } } } function persistSavedSqlEditorPosition(tab: QueryTab | undefined) { if (!tab?.savedSqlId || tab.mode !== "query") return; const pending = savedSqlEditorPositionTimers.get(tab.savedSqlId); if (pending) { clearTimeout(pending); savedSqlEditorPositionTimers.delete(tab.savedSqlId); } saveSavedSqlEditorPosition( createSavedSqlEditorPosition({ savedSqlId: tab.savedSqlId, sql: tab.sql, selection: tab.editorSelection, viewport: tab.editorViewport, }), ); } function queueSavedSqlEditorPositionPersist(tab: QueryTab | undefined) { if (!tab?.savedSqlId || tab.mode !== "query") return; const pending = savedSqlEditorPositionTimers.get(tab.savedSqlId); if (pending) clearTimeout(pending); const tabId = tab.id; const savedSqlId = tab.savedSqlId; const timer = setTimeout(() => { savedSqlEditorPositionTimers.delete(savedSqlId); persistSavedSqlEditorPosition(tabs.value.find((item) => item.id === tabId)); }, SAVED_SQL_EDITOR_POSITION_PERSIST_DELAY_MS); savedSqlEditorPositionTimers.set(savedSqlId, timer); } function discardTabChanges(id: string) { const tab = tabs.value.find((item) => item.id === id); if (!tab) return false; if (tab.mode === "structure") { tab.structureDraft = undefined; return true; } if (tab.mode !== "query") return false; if (tab.originalSql !== undefined) { tab.sql = tab.originalSql; return true; } if (tab.savedSqlId) { tab.sql = ""; return true; } tab.sql = ""; tab.originalSql = ""; return true; } function finishPendingBatchClose() { const finalActiveTabId = pendingBatchCloseFinalActiveTabId.value; const onComplete = pendingBatchCloseComplete; pendingBatchCloseTabIds.value = null; pendingBatchCloseFinalActiveTabId.value = undefined; pendingBatchCloseComplete = null; if (finalActiveTabId !== undefined) { activeTabId.value = finalActiveTabId && tabs.value.some((tab) => tab.id === finalActiveTabId) ? finalActiveTabId : null; } return onComplete; } function continuePendingBatchClose() { const pendingIds = pendingBatchCloseTabIds.value; if (!pendingIds) return; const remainingIds = pendingIds.filter((id) => tabs.value.some((tab) => tab.id === id)); pendingBatchCloseTabIds.value = remainingIds; if (remainingIds.length === 0) { finishPendingBatchClose()?.(); return; } const dirtyTab = remainingIds.map((id) => tabs.value.find((tab) => tab.id === id)).find((tab): tab is QueryTab => !!tab && shouldConfirmTabClose(tab)); if (dirtyTab) { // Batch close must pause before dropping dirty tabs so the shared save/discard dialog protects every editable surface. showDirtyTabCloseConfirm(dirtyTab, "batch"); return; } const onComplete = finishPendingBatchClose(); for (const id of remainingIds) closeTab(id, { force: true }); onComplete?.(); } function beginBatchClose(ids: string[], finalActiveTabId?: string | null, onComplete?: () => void) { const uniqueIds = [...new Set(ids)].filter((id) => tabs.value.some((tab) => tab.id === id)); if (uniqueIds.length === 0) { onComplete?.(); return; } const existingIds = pendingBatchCloseTabIds.value; if (existingIds) { // Sidebar bulk disconnects settle independently, so later scopes must join the open dialog instead of replacing it. const combinedIds = [...new Set([...existingIds, ...uniqueIds])]; const preferredFinalActiveTabId = finalActiveTabId !== undefined ? finalActiveTabId : pendingBatchCloseFinalActiveTabId.value; pendingBatchCloseTabIds.value = combinedIds; pendingBatchCloseFinalActiveTabId.value = preferredFinalActiveTabId && combinedIds.includes(preferredFinalActiveTabId) ? activeTabAfterClosing(combinedIds, preferredFinalActiveTabId) : preferredFinalActiveTabId; if (onComplete) { const previousComplete = pendingBatchCloseComplete; pendingBatchCloseComplete = previousComplete ? () => { try { previousComplete(); } finally { onComplete(); } } : onComplete; } } else { pendingBatchCloseTabIds.value = uniqueIds; pendingBatchCloseFinalActiveTabId.value = finalActiveTabId; pendingBatchCloseComplete = onComplete ?? null; } continuePendingBatchClose(); } function resumePendingBatchCloseAfter(id: string) { const pendingIds = pendingBatchCloseTabIds.value; if (!pendingIds?.includes(id)) return; pendingBatchCloseTabIds.value = pendingIds.filter((pendingId) => pendingId !== id); continuePendingBatchClose(); } function closeTab(id: string, { force = false }: { force?: boolean } = {}) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; if (!force && shouldConfirmTabClose(tab)) { showDirtyTabCloseConfirm(tab, "tab"); return; } const idx = tabs.value.findIndex((t) => t.id === id); if (idx < 0) return; persistSavedSqlEditorPosition(tabs.value[idx]); if (tab.mode === "sqlserver-trace") void disposeSqlServerActivityTrace(tab.id); clearDataGridPendingSnapshotsForTab(id); clearDataGridStructuredFilterStatesForTab(id); if (tabs.value[idx].txnSessionId) void rollbackTransaction(id); 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]); clearResultRunSnapshots(tabs.value[idx]); void deleteTabResultSnapshot(tabResultCacheKey(id)); releaseTabResultObjectPayloads(tabs.value[idx]); clearResultRuns(tabs.value[idx]); clearResultPayload(tabs.value[idx]); tabs.value.splice(idx, 1); if (tab.externalSqlPath) refreshExternalSqlFileTitles(); if (activeTabId.value === id) { activeTabId.value = fallbackActiveTabAfterClose(id, idx); } if (force) resumePendingBatchCloseAfter(id); } function shouldConfirmTabClose(tab: QueryTab): boolean { if (tab.mode === "structure") return isTabDirty(tab); return shouldConfirmUnsavedSqlClose.value && isTabDirty(tab); } function forceClosePendingTab() { const id = pendingCloseTabId.value; const confirmingAppClose = isConfirmingAppClose.value; pendingCloseTabId.value = null; showCloseConfirm.value = false; closeConfirmContext.value = "tab"; if (confirmingAppClose) { if (id) discardTabChanges(id); isConfirmingAppClose.value = false; return; } if (id) closeTab(id, { force: true }); } function forceCloseAllPendingTabs() { const dirtyIds = closeConfirmDirtyTabIds.value; const pendingId = pendingCloseTabId.value; const batchIds = pendingBatchCloseTabIds.value?.filter((id) => tabs.value.some((tab) => tab.id === id)) ?? null; const finalActiveTabId = pendingBatchCloseFinalActiveTabId.value; const onBatchComplete = pendingBatchCloseComplete; const confirmingAppClose = isConfirmingAppClose.value; pendingCloseTabId.value = null; showCloseConfirm.value = false; pendingBatchCloseTabIds.value = null; pendingBatchCloseFinalActiveTabId.value = undefined; pendingBatchCloseComplete = null; isConfirmingAppClose.value = false; closeConfirmContext.value = "tab"; for (const id of dirtyIds) discardTabChanges(id); if (confirmingAppClose) return; const idsToClose = batchIds ?? (pendingId ? [pendingId] : []); for (const id of idsToClose) closeTab(id, { force: true }); if (finalActiveTabId !== undefined) { activeTabId.value = finalActiveTabId && tabs.value.some((tab) => tab.id === finalActiveTabId) ? finalActiveTabId : null; } if (batchIds) onBatchComplete?.(); } function cancelClosePendingTab() { pendingCloseTabId.value = null; showCloseConfirm.value = false; pendingBatchCloseTabIds.value = null; pendingBatchCloseFinalActiveTabId.value = undefined; pendingBatchCloseComplete = null; isConfirmingAppClose.value = false; closeConfirmContext.value = "tab"; } function saveAndClosePendingTab() { const id = pendingCloseTabId.value; pendingCloseTabId.value = null; showCloseConfirm.value = false; isConfirmingAppClose.value = false; closeConfirmContext.value = "tab"; if (id) return id; return null; } function suspendCloseConfirm() { showCloseConfirm.value = false; } function resumeCloseConfirm() { const dirtyId = closeConfirmDirtyTabIds.value[0]; const dirtyTab = dirtyId ? tabs.value.find((tab) => tab.id === dirtyId) : undefined; if (!dirtyTab) return false; pendingCloseTabId.value = dirtyTab.id; activeTabId.value = dirtyTab.id; showCloseConfirm.value = true; return true; } function completePendingCloseAfterSaveAll() { const pendingId = pendingCloseTabId.value; const batchIds = pendingBatchCloseTabIds.value?.filter((id) => tabs.value.some((tab) => tab.id === id)) ?? null; const finalActiveTabId = pendingBatchCloseFinalActiveTabId.value; const onBatchComplete = pendingBatchCloseComplete; const confirmingAppClose = isConfirmingAppClose.value; pendingCloseTabId.value = null; showCloseConfirm.value = false; pendingBatchCloseTabIds.value = null; pendingBatchCloseFinalActiveTabId.value = undefined; pendingBatchCloseComplete = null; isConfirmingAppClose.value = false; closeConfirmContext.value = "tab"; if (confirmingAppClose) return "app" as const; const idsToClose = batchIds ?? (pendingId ? [pendingId] : []); for (const id of idsToClose) closeTab(id, { force: true }); if (finalActiveTabId !== undefined) { activeTabId.value = finalActiveTabId && tabs.value.some((tab) => tab.id === finalActiveTabId) ? finalActiveTabId : null; } if (batchIds) onBatchComplete?.(); return "tabs" as const; } function closeOtherTabs(id: string) { if (!tabs.value.some((tab) => tab.id === id)) return; beginBatchClose( tabs.value.filter((tab) => tab.id !== id).map((tab) => tab.id), id, ); } function closeTabsByIds(ids: Iterable, finalActiveTabId?: string | null, onComplete?: () => void) { beginBatchClose([...ids], finalActiveTabId, onComplete); } function closeRightTabs(id: string, onComplete?: () => void) { const target = tabs.value.find((tab) => tab.id === id); if (!target) return; const groupedTabs = tabs.value.filter((tab) => Boolean(tab.pinned) === Boolean(target.pinned)); const targetIndex = groupedTabs.findIndex((tab) => tab.id === id); const ids = groupedTabs.slice(targetIndex + 1).map((tab) => tab.id); if (ids.length === 0) { onComplete?.(); return; } const finalActiveTabId = activeTabId.value && !ids.includes(activeTabId.value) ? activeTabId.value : id; closeTabsByIds(ids, finalActiveTabId, onComplete); } function finalActiveTabAfterClosing(ids: string[]) { const closingIds = new Set(ids); const activeTab = activeTabId.value ? tabs.value.find((tab) => tab.id === activeTabId.value) : undefined; if (activeTab && !closingIds.has(activeTab.id)) return activeTab.id; return tabs.value.find((tab) => !closingIds.has(tab.id))?.id ?? null; } function activeTabAfterClosing(ids: string[], preferredActiveTabId = activeTabId.value) { const closingIds = new Set(ids); if (preferredActiveTabId && !closingIds.has(preferredActiveTabId) && tabs.value.some((tab) => tab.id === preferredActiveTabId)) { return preferredActiveTabId; } const preferredIndex = preferredActiveTabId ? tabs.value.findIndex((tab) => tab.id === preferredActiveTabId) : -1; const remainingTabs = tabs.value.filter((tab) => !closingIds.has(tab.id)); if (preferredIndex < 0) return remainingTabs[0]?.id ?? null; return remainingTabs[Math.min(preferredIndex, remainingTabs.length - 1)]?.id ?? null; } function closeOtherRegularTabs(id: string) { const tab = tabs.value.find((item) => item.id === id); if (!tab || tab.pinned) return; beginBatchClose( tabs.value.filter((item) => !item.pinned && item.id !== id).map((item) => item.id), id, ); } function closeRegularTabs() { const ids = tabs.value.filter((tab) => !tab.pinned).map((tab) => tab.id); beginBatchClose(ids, finalActiveTabAfterClosing(ids)); } function closeOtherFixedTabs(id: string) { const tab = tabs.value.find((item) => item.id === id); if (!tab || !tab.pinned) return; beginBatchClose( tabs.value.filter((item) => item.pinned && item.id !== id).map((item) => item.id), id, ); } function closeFixedTabs() { const ids = tabs.value.filter((tab) => tab.pinned).map((tab) => tab.id); beginBatchClose(ids, finalActiveTabAfterClosing(ids)); } function closeAllTabs() { beginBatchClose( tabs.value.map((tab) => tab.id), null, ); } function requestAppCloseConfirmation() { const dirtyTab = tabs.value.find((tab) => shouldConfirmTabOnAppClose(tab)); if (!dirtyTab) return false; isConfirmingAppClose.value = true; showDirtyTabCloseConfirm(dirtyTab, "app"); return true; } function duplicateTab(id: string) { const idx = tabs.value.findIndex((t) => t.id === id); if (idx < 0) return; const original = tabs.value[idx]; const newId = uuid(); const newTab: QueryTab = { id: newId, title: original.title, customTitle: original.customTitle, connectionId: original.connectionId, database: original.database, schema: original.schema, catalog: original.catalog, sql: original.sql, originalSql: "", savedSqlId: undefined, externalSqlPath: undefined, lastExecutedSql: undefined, resultBaseSql: original.resultBaseSql, resultSortedSql: undefined, resultSortColumn: undefined, resultSortColumnIndex: undefined, resultSortDirection: undefined, resultSortMode: undefined, resultLocalSortOriginalRows: undefined, resultLocalSortOriginalLargeValueCells: undefined, resultLocalSortOriginalMongoDocuments: undefined, resultLocalSortOriginalMongoCopyDocuments: undefined, orderByInput: undefined, resultPageSql: undefined, resultPageLimit: undefined, resultPageOffset: undefined, resultCountSql: undefined, resultTotalRowCount: undefined, resultTotalRowCountLoading: undefined, resultSessionId: undefined, resultClientSessionId: undefined, resultAccessedAt: undefined, resultCacheKey: undefined, resultCacheState: undefined, pinned: false, result: undefined, results: undefined, activeResultIndex: undefined, explainPlan: undefined, explainError: undefined, explainSql: undefined, lastExplainedSql: undefined, isExecuting: false, isCancelling: false, queryExecutionStartedAt: undefined, editorViewport: undefined, editorSelection: undefined, executionId: undefined, isExplaining: false, explainExecutionId: undefined, mode: original.mode, mqTenant: original.mqTenant, mqInitialTab: original.mqInitialTab, nacosNamespace: original.nacosNamespace, nacosNamespaceName: original.nacosNamespaceName, structureTableName: original.structureTableName, structureDraft: original.structureDraft ? cloneTabDraft(original.structureDraft) : undefined, objectBrowser: original.objectBrowser ? { ...original.objectBrowser } : undefined, objectSource: original.objectSource ? { ...original.objectSource } : undefined, tableMeta: original.tableMeta ? { ...original.tableMeta, columns: [...original.tableMeta.columns], primaryKeys: [...original.tableMeta.primaryKeys] } : undefined, queryAnalysis: original.queryAnalysis ? { ...original.queryAnalysis, sources: original.queryAnalysis.sources?.map((source) => ({ ...source })), columns: original.queryAnalysis.columns.map((c) => ({ ...c })) } : undefined, querySourceColumns: original.querySourceColumns ? [...original.querySourceColumns] : undefined, resultColumnComments: original.resultColumnComments ? [...original.resultColumnComments] : undefined, queryDisplaySourceColumns: original.queryDisplaySourceColumns ? [...original.queryDisplaySourceColumns] : undefined, queryEditabilityReason: original.queryEditabilityReason, resultEvicted: undefined, whereInput: original.whereInput, previewSql: original.previewSql, }; tabs.value.splice(idx + 1, 0, newTab); activeTabId.value = newId; } function closeTabsWhere(predicate: (tab: QueryTab) => boolean) { const closingIds = new Set(tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id)); if (closingIds.size === 0) return; tabs.value .filter((tab) => closingIds.has(tab.id)) .forEach((tab) => { if (tab.mode === "sqlserver-trace") void disposeSqlServerActivityTrace(tab.id); clearDataGridPendingSnapshotsForTab(tab.id); clearDataGridStructuredFilterStatesForTab(tab.id); if (tab.txnSessionId) void rollbackTransaction(tab.id); if (tab.isExecuting) void cancelTabExecution(tab.id); if (tab.isExplaining) void cancelTabExplain(tab.id); void closeResultSession(tab); void closeClientConnectionSession(tab); clearResultRunSnapshots(tab); void deleteTabResultSnapshot(tabResultCacheKey(tab.id)); releaseTabResultObjectPayloads(tab); clearResultRuns(tab); clearResultPayload(tab); }); const activeClosingIndex = tabs.value.findIndex((tab) => tab.id === activeTabId.value && closingIds.has(tab.id)); tabs.value = tabs.value.filter((tab) => !closingIds.has(tab.id)); if (activeClosingIndex >= 0) { activeTabId.value = tabs.value[Math.min(activeClosingIndex, tabs.value.length - 1)]?.id ?? null; } } function closeScopedTabsWhere(predicate: (tab: QueryTab) => boolean, options: { force?: boolean } = {}) { const ids = tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id); if (options.force) { closeTabsWhere(predicate); return; } beginBatchClose(ids, activeTabAfterClosing(ids)); } function closeConnectionTabs(connectionId: string, options?: { force?: boolean }) { closeScopedTabsWhere((tab) => tab.connectionId === connectionId, options); } function closeDatabaseTabs(connectionId: string, database: string) { closeScopedTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database); } function tabMatchesDroppedTableObject(tab: QueryTab, target: DroppedTableObjectTarget): boolean { if (tab.connectionId !== target.connectionId || tab.database !== target.database) return false; const targetSchemas = droppedTableObjectSchemaCandidates(target); if ((target.objectType ?? "TABLE") === "TABLE" && tab.mode === "hbase") { return tab.sql === target.name; } if (tab.mode === "data") { const tableMeta = tableMetaForDataTab(tab); if (!tableMeta || tableMeta.tableName !== target.name) return false; return targetSchemas.has(normalizeOptionalSchema(tableMeta.schema ?? tab.schema)); } if ((target.objectType ?? "TABLE") === "TABLE" && tab.mode === "structure") { if ((tab.structureTableName || "") !== target.name) return false; return targetSchemas.has(normalizeOptionalSchema(tab.schema)); } return false; } function tabMatchesTableDataRefreshTarget(tab: QueryTab, target: TableDataRefreshTarget): boolean { if (tab.mode !== "data" || tab.connectionId !== target.connectionId || tab.database !== target.database) return false; const tableMeta = tableMetaForDataTab(tab); if (!tableMeta || tableMeta.tableName !== target.name) return false; if ((tableMeta.catalog || "") !== (target.catalog || "")) return false; const targetSchemas = droppedTableObjectSchemaCandidates(target); return targetSchemas.has(normalizeOptionalSchema(tableMeta.schema ?? tab.schema)); } function closeDroppedTableObjectTabs(target: DroppedTableObjectTarget) { // A dropped table-like object makes existing data/structure tabs stale; close // them immediately instead of letting the next refresh fail against a missing object. closeTabsWhere((tab) => tabMatchesDroppedTableObject(tab, target)); } async function refreshDataTabInternal(id: string, options?: { supersedeBusy?: boolean; propagateBuildError?: boolean }): Promise { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab || tab.mode !== "data" || (tab.isExecuting && !options?.supersedeBusy)) return false; const connStore = useConnectionStore(); const conn = connStore.getConfig(tab.connectionId); const effectiveDbType = effectiveDatabaseTypeForConnection(conn); const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId); const refreshPreparationId = uuid(); // Reserve the tab synchronously before SQL construction yields so repeated // refresh requests cannot build and execute duplicate queries. setExecutingWithId(tab.id, refreshPreparationId); try { let tableMeta = tableMetaForDataTab(tab); if (!tableMeta?.tableName) return false; // 生命周期代次校验:disconnect / 关库 / 死池重连后的首次刷新必须先从 // 新连接源头重建结构,否则旧显式列列表会生成错误 SELECT(issue #6623 / // PR #6640 review blocker 2)。reload 路径不读 tableMetaUpdatedAt 风干 // 判定、只读 tableMeta 本身,因此在这里显式强制重建。 const connectionGeneration = connStore.metadataGenerationFor(tab.connectionId, tab.database); if (isDataTabMetadataLifecycleStale(tab, connectionGeneration)) { const metadataGenerationAtStart = connectionGeneration; const reloadedMetadata = await loadTableMetadata({ connectionId: tab.connectionId, database: tab.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, catalog: tableMeta.catalog, databaseType: effectiveDbType ?? conn?.db_type ?? "", driverProfile: conn?.driver_profile, force: true, }); // 重建期间又跨越了一次连接生命周期边界 → 放弃本次刷新,避免旧结果 // 二次写回 tab(PR #6640 review blocker 1 的 tab-local 半边) if (connStore.metadataGenerationFor(tab.connectionId, tab.database) !== metadataGenerationAtStart) return false; const current = tabs.value.find((candidate) => candidate.id === id); if (!current || current.executionId !== refreshPreparationId) return false; setTableMeta(tab.id, tableMetadataToDataTabMeta(reloadedMetadata.metadata, { schema: tableMeta.schema })); tableMeta = tableMetaForDataTab(tab) ?? tableMeta; } clearInvalidDataTabSortState(tab, tableMeta.columns); const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys; const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined; const orderBy = tab.orderByInput?.trim() || sortOrder; const limit = tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize); const offset = tab.resultPageOffset ?? 0; const useDriverRowOffset = conn?.db_type === "jdbc" && effectiveDbType === "iris"; const sql = await buildTableSelectSql({ databaseType: effectiveDbType, driverProfile: conn?.driver_profile, identifierQuote, database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, catalog: tableMeta.catalog, columns: tableMeta.columns.map((column) => column.name), primaryKeys, ...tableDataLargeValuePreviewOptions(effectiveDbType, tableMeta.columns, primaryKeys, limit), includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, includeRowId: usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableMeta.tableType), whereInput: tab.whereInput, orderBy, limit, offset, ...(useDriverRowOffset ? { useDriverRowOffset: true } : {}), }); if (!sql.trim()) throw new Error("Failed to build table refresh SQL"); const current = tabs.value.find((candidate) => candidate.id === id); if (!current || current.executionId !== refreshPreparationId) return false; updateSql(tab.id, sql); await executeTabSql(tab.id, sql, { pagination: { limit, offset }, preserveResultDuringExecution: true, }); return true; } catch (error) { const current = tabs.value.find((candidate) => candidate.id === id); if (current?.executionId === refreshPreparationId) setErrorResult(id, error); if (options?.propagateBuildError) throw error; return false; } } function refreshDataTab(id: string): Promise { return refreshDataTabInternal(id); } async function refreshDataTabsForTable(target: TableDataRefreshTarget): Promise { const matchingTabs = tabs.value.filter((tab) => tabMatchesTableDataRefreshTarget(tab, target)); if (matchingTabs.length === 0) return 0; let refreshed = 0; for (const tab of matchingTabs) { if (await refreshDataTabInternal(tab.id, { supersedeBusy: true, propagateBuildError: true })) refreshed += 1; } return refreshed; } function releaseTabsWhere(predicate: (tab: QueryTab) => boolean) { closeTabsWhere((tab) => predicate(tab) && tab.mode !== "query"); tabs.value .filter((tab) => predicate(tab)) .forEach((tab) => { rollbackTabTransaction(tab, { resetAutoCommit: true }); clearDataGridPendingSnapshotsForTab(tab.id); clearDataGridStructuredFilterStatesForTab(tab.id); if (tab.isExecuting) void cancelTabExecution(tab.id); if (tab.isExplaining) void cancelTabExplain(tab.id); void closeResultSession(tab); void closeClientConnectionSession(tab); clearResultRunSnapshots(tab); void deleteTabResultSnapshot(tabResultCacheKey(tab.id)); releaseTabResultObjectPayloads(tab); clearResultRuns(tab); clearResultPayload(tab); }); } function releaseConnectionTabs(connectionId: string) { releaseTabsWhere((tab) => tab.connectionId === connectionId); } function releaseDatabaseTabs(connectionId: string, database: string) { releaseTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database); } /** * 连接生命周期边界(断开连接 / 关闭数据库连接 / 后端连接池失效重连)。 * 数据标签页的 tableMeta 为展示/编辑保留(不清除 UI、主键与编辑安全门控), * 但清掉 freshness 戳:此后 openData 的 tab-local 暖缓存判定、激活路径的 * stale 判定、以及网格 reload 的 metadata age 判定全部按"冷缓存"处理, * 即使位于 30s TTL 窗口内也会重新拉取结构(issue #6623)。 */ function staleConnectionDataTabMetadata(connectionId: string, database?: string) { for (const tab of tabs.value) { if (tab.mode !== "data" || tab.connectionId !== connectionId) continue; if (database != null && tab.database !== database) continue; tab.tableMetaUpdatedAt = undefined; } } function isDatabaseOpen(connectionId: string, database: string) { return openDatabaseKeys.value.has(`${connectionId}\x00${database}`); } function rollbackTabsWhere(predicate: (tab: QueryTab) => boolean, options?: { resetAutoCommit?: boolean }) { tabs.value.filter((tab) => predicate(tab)).forEach((tab) => rollbackTabTransaction(tab, options)); } function rollbackConnectionTransactions(connectionId: string) { rollbackTabsWhere((tab) => tab.connectionId === connectionId, { resetAutoCommit: true }); } function rollbackDatabaseTransactions(connectionId: string, database: string) { rollbackTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database, { resetAutoCommit: true }); } function updateSql(id: string, sql: string) { const tab = tabs.value.find((t) => t.id === id); if (tab) { tab.sql = sql; queueSavedSqlEditorPositionPersist(tab); } } function updateDataGridLocalColumnFilters(id: string, filters: Record) { const tab = tabs.value.find((item) => item.id === id); if (!tab?.result) return; if (Object.keys(filters).length === 0) { delete tab.result.local_column_filters; } else { tab.result.local_column_filters = Object.fromEntries(Object.entries(filters).map(([columnIndex, values]) => [columnIndex, [...values]])); } } function updateDataGridHiddenColumnKeys(id: string, keys: string[]) { const tab = tabs.value.find((item) => item.id === id); if (!tab?.result) return; if (keys.length === 0) { delete tab.result.local_hidden_column_keys; } else { tab.result.local_hidden_column_keys = [...keys]; } } function setAutoCommit(id: string, autoCommit: boolean) { const tab = tabs.value.find((t) => t.id === id); if (tab) { const wasManual = tab.autoCommit === false; tab.autoCommit = autoCommit; if (autoCommit && wasManual) { if (tab.txnSessionId) { void rollbackTransaction(id); } else { clearManualTransactionSession(tab); } } } } /** Reset only the Oracle sticky-dirty bit. Used when a session continues but * the old dirty state must be discarded (e.g. idle-expiry recovery where the * replacement session starts fresh). Full session cleanup goes through * `clearManualTransactionSession`. */ function clearOracleTxnPossiblyDirty(tab: { oracleTxnPossiblyDirty?: boolean }) { if (tab.oracleTxnPossiblyDirty !== undefined) tab.oracleTxnPossiblyDirty = false; } /** Centralized manual-session cleanup. Clears every field tied to a manual * transaction session exactly when that session is conclusively ended or * discarded. Callers must not assign these fields individually. */ function clearManualTransactionSession(tab: QueryTab) { tab.txnSessionId = undefined; tab.txnAutoRolledBack = false; if (tab.oracleTxnPossiblyDirty !== undefined) tab.oracleTxnPossiblyDirty = false; } function rollbackTabTransaction(tab: QueryTab, options?: { resetAutoCommit?: boolean }) { if (tab.txnSessionId) void rollbackTransaction(tab.id); if (options?.resetAutoCommit) { const dbType = useConnectionStore().getConfig(tab.connectionId)?.db_type; tab.autoCommit = defaultAutoCommitForDbTypeWithSetting(dbType); } clearOracleTxnPossiblyDirty(tab); tab.txnAutoRolledBack = false; } async function commitTransaction(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.txnSessionId) return; try { await api.commitManualTransaction(tab.txnSessionId); } finally { clearManualTransactionSession(tab); } } async function rollbackTransaction(id: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.txnSessionId) return; try { await api.rollbackManualTransaction(tab.txnSessionId); } finally { clearManualTransactionSession(tab); } } function updateEditorViewport(id: string, viewport: { scrollTop: number; scrollLeft: number }) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; if (tab.editorViewport?.scrollTop === viewport.scrollTop && tab.editorViewport?.scrollLeft === viewport.scrollLeft) return; tab.editorViewport = viewport; queueSavedSqlEditorPositionPersist(tab); } function updateEditorSelection(id: string, selection: { anchor: number; head: number }) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.editorSelection = selection; queueSavedSqlEditorPositionPersist(tab); } function updateObjectBrowserViewport(id: string, viewport: ObjectBrowserViewport) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.mode !== "objects") return; const previous = tab.objectBrowser?.viewport; if (previous?.scrollTop === viewport.scrollTop && previous.viewMode === viewport.viewMode) return; tab.objectBrowser = { ...tab.objectBrowser, viewport }; } function renameTab(id: string, title: string) { const trimmed = title.trim(); if (!trimmed) return false; const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.mode !== "query") return false; const normalizedTitle = tab.savedSqlId ? ensureSqlExtension(trimmed) : trimmed; const previousTitle = tab.title; tab.title = normalizedTitle; tab.customTitle = true; if (tab.savedSqlId) { const savedSqlStore = useSavedSqlStore(); const existing = savedSqlStore.getFile(tab.savedSqlId); if (existing && existing.name !== normalizedTitle) { void savedSqlStore.renameFile(tab.savedSqlId, normalizedTitle).catch((error) => { console.warn("[DBX][saved-sql:rename:error]", error); tab.title = previousTitle; }); } } return true; } function linkSavedSql(id: string, savedSqlId: string, title?: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.savedSqlId = savedSqlId; tab.externalSqlPath = undefined; tab.externalSqlFileVersion = undefined; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; if (title) { tab.title = title; tab.customTitle = true; } } function linkExternalSqlPath(id: string, path: string, title?: string, version?: QueryTab["externalSqlFileVersion"]) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.externalSqlPath = path; tab.externalSqlFileVersion = version; tab.externalSqlIgnoredFileVersion = undefined; tab.externalSqlFileMissing = undefined; tab.savedSqlId = undefined; if (title) { tab.title = title; tab.customTitle = true; } markTabClean(tab); refreshExternalSqlFileTitles(); } function currentSavedSqlExecutionTarget(): SavedSqlExecutionTarget | undefined { const activeTab = tabs.value.find((tab) => tab.id === activeTabId.value); const target = savedSqlExecutionTargetFromTab(activeTab); if (!target || !useConnectionStore().getConfig(target.connectionId)) return undefined; return target; } function applySavedSqlExecutionTarget(tab: QueryTab, target: SavedSqlExecutionTarget) { const options = { persistSavedSqlTarget: false }; updateConnection(tab.id, target.connectionId, target.database, options); if (tab.catalog !== target.catalog || tab.database !== target.database) { if (tab.catalog !== undefined || target.catalog !== undefined) updateCatalog(tab.id, target.catalog, target.database, options); else updateDatabase(tab.id, target.database, options); } updateSchema(tab.id, target.schema, options); } function openSavedSql(file: SavedSqlFile, options: OpenSavedSqlOptions = {}) { const targetMode = options.targetMode ?? useSettingsStore().editorSettings.savedSqlOpenTargetMode; const currentTarget = targetMode === "current" ? currentSavedSqlExecutionTarget() : undefined; const target = resolveSavedSqlExecutionTarget(file, targetMode, currentTarget); const existing = tabs.value.find((tab) => tab.savedSqlId === file.id); if (existing) { persistSavedSqlEditorPosition(existing); if (!existing.sql && file.sql) { existing.sql = file.sql; existing.originalSql = file.sql; const restored = restoreSavedSqlEditorPosition(file.id, file.sql); existing.editorSelection = restored.selection; existing.editorViewport = restored.viewport; } applySavedSqlExecutionTarget(existing, target); switchTab(existing.id); return existing.id; } const id = uuid(); const restoredPosition = restoreSavedSqlEditorPosition(file.id, file.sql); const dbType = useConnectionStore().getConfig(target.connectionId)?.db_type; const tab: QueryTab = { id, title: file.name, customTitle: true, connectionId: target.connectionId, database: target.database, schema: target.schema, catalog: target.catalog, sql: file.sql, savedSqlId: file.id, originalSql: file.sql, isExecuting: false, isCancelling: false, isExplaining: false, mode: "query", autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), editorSelection: restoredPosition.selection, editorViewport: restoredPosition.viewport, }; tabs.value.push(tab); activeTabId.value = id; return id; } async function hydrateSavedSqlTabs() { await initSavedSqlEditorPositions(); const savedSqlStore = useSavedSqlStore(); const linkedTabs = tabs.value.filter((tab) => tab.savedSqlId && tab.sql === ""); for (const tab of linkedTabs) { const file = await savedSqlStore.ensureFileContent(tab.savedSqlId!); if (!file) continue; tab.title = tab.customTitle ? tab.title : file.name; tab.sql = file.sql; tab.originalSql = file.sql; const restored = restoreSavedSqlEditorPosition(file.id, file.sql); tab.editorSelection = restored.selection; tab.editorViewport = restored.viewport; } } 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 reorderTab(id: string, targetId: string, position: "before" | "after") { const fromIdx = tabs.value.findIndex((t) => t.id === id); const toIdx = tabs.value.findIndex((t) => t.id === targetId); if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return false; const reordered = [...tabs.value]; const [tab] = reordered.splice(fromIdx, 1); const newToIdx = reordered.findIndex((t) => t.id === targetId); reordered.splice(newToIdx + (position === "after" ? 1 : 0), 0, tab); const nextTabs = orderPinnedFirst(reordered, (item) => !!item.pinned); if (nextTabs.every((item, index) => item.id === tabs.value[index]?.id)) return false; tabs.value = nextTabs; return true; } function persistSavedSqlExecutionTarget(tab: QueryTab, options: UpdateExecutionTargetOptions) { if (options.persistSavedSqlTarget === false || tab.mode !== "query" || !tab.savedSqlId) return; const savedSqlStore = useSavedSqlStore(); void savedSqlStore .updateFileExecutionTarget(tab.savedSqlId, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog, schema: tab.schema, }) .catch((error) => console.warn("[DBX][saved-sql:target:error]", error)); } function updateDatabase(id: string, database: string, options: UpdateExecutionTargetOptions = {}) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.database === database) return; rollbackTabTransaction(tab); 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; persistSavedSqlExecutionTarget(tab, options); } function updateCatalog(id: string, catalog: string | undefined, database: string, options: UpdateExecutionTargetOptions = {}) { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab || (tab.catalog === catalog && tab.database === database)) return; rollbackTabTransaction(tab); void closeResultSession(tab); void closeClientConnectionSession(tab); tab.catalog = catalog; 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; persistSavedSqlExecutionTarget(tab, options); } function updateSchema(id: string, schema: string | undefined, options: UpdateExecutionTargetOptions = {}) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.schema === schema) return; rollbackTabTransaction(tab); const clearsQuerySchema = tab.mode === "query" && tab.schema && !schema && supportsClearableQuerySchema(useConnectionStore().getConfig(tab.connectionId)?.db_type); if (clearsQuerySchema) { queueTabSessionReset(tab); clearResultPayload(tab); tab.lastExecutedSql = undefined; tab.resultBaseSql = undefined; tab.resultSortedSql = undefined; clearExplain(tab); } tab.schema = schema; if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema, viewport: undefined }; persistSavedSqlExecutionTarget(tab, options); } function updateConnection(id: string, connectionId: string, database = "", options: UpdateExecutionTargetOptions = {}) { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.connectionId === connectionId) return; rollbackTabTransaction(tab, { resetAutoCommit: true }); void closeResultSession(tab); void closeClientConnectionSession(tab); tab.connectionId = connectionId; tab.catalog = undefined; tab.database = database; tab.catalog = undefined; tab.objectBrowser = undefined; tab.schema = undefined; clearResultPayload(tab); tab.lastExecutedSql = undefined; tab.resultBaseSql = undefined; tab.resultSortedSql = undefined; clearExplain(tab); tab.tableMeta = undefined; persistSavedSqlExecutionTarget(tab, options); } function clearInvalidDataTabSortState(tab: QueryTab, columns: NonNullable["columns"]): boolean { if (tab.mode !== "data") return false; const hasColumn = (name: string) => columns.some((column) => column.name === name); const structuredSortMissing = !!tab.resultSortColumn && !hasColumn(tab.resultSortColumn); const simpleOrderMissing = simpleDataGridOrderByReferencesMissingColumn( tab.orderByInput, columns.map((column) => column.name), ); if (!structuredSortMissing && !simpleOrderMissing) return false; if (structuredSortMissing) { tab.resultSortColumn = undefined; tab.resultSortColumnIndex = undefined; tab.resultSortDirection = undefined; tab.resultSortMode = undefined; tab.resultSortedSql = undefined; tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; } if (simpleOrderMissing) tab.orderByInput = undefined; return true; } function clearInvalidDataTabSort(id: string): boolean { const tab = tabs.value.find((candidate) => candidate.id === id); if (!tab?.tableMeta?.columns.length) return false; return clearInvalidDataTabSortState(tab, tab.tableMeta.columns); } function setTableMeta(id: string, meta: NonNullable) { const tab = tabs.value.find((t) => t.id === id); if (tab) { tab.tableMeta = meta; // 记录写入时的连接元数据代次:disconnect/关库/死池重连会使该代次递增, // 代次失配视同冷缓存,即使位于 30s TTL 窗口内也会重建结构(issue #6623 / // PR #6640)。同一代次内的多次写入与连接Store保持一致,无需额外入参。 tab.tableMetaGeneration = useConnectionStore().metadataGenerationFor(tab.connectionId, tab.database) ?? 0; tab.tableMetaUpdatedAt = Date.now(); if (meta.columns.length > 0) clearInvalidDataTabSortState(tab, meta.columns); // 只有真实元数据(columns 非空)落地才结束行标识等待;多处调用方会先写 // columns/primaryKeys 为空的占位身份(如 useNavigationTargets),不得 // 借此提前解除编辑门控。失败/中止路径不清除——标签页保持只读是安全 // 兜底,刷新或重开表会重新加载元数据恢复 if (meta.columns.length > 0) tab.tableMetaPending = false; } } 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; tab.queryExecutionStartedAt = isExecuting ? Date.now() : undefined; if (!isExecuting) { tab.isCancelling = false; tab.executionId = undefined; } } function setExecutingWithId(id: string, executionId: string) { const tab = tabs.value.find((t) => t.id === id); if (!tab) return; tab.isExecuting = true; tab.executionId = executionId; tab.isCancelling = false; tab.queryExecutionStartedAt = Date.now(); } function clearExplain(tab: QueryTab) { tab.explainPlan = undefined; tab.explainTableResult = undefined; tab.explainError = undefined; tab.explainTableError = undefined; tab.explainSql = undefined; tab.explainTableSql = undefined; tab.lastExplainedSql = undefined; tab.isExplaining = false; tab.explainExecutionId = undefined; tab.explainClientSessionId = undefined; } function toErrorResult(e: any): NonNullable { // Single funnel for every query execution failure, so backend messages DBX // knows about are shown in the active locale rather than as raw English. const error = normalizeBackendError(e) ?? undefined; const message = translateBackendError(i18n.global.t, e); return markQueryResultRowsRaw({ columns: ["Error"], execution_error: true, 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.resultClientSessionId = undefined; tab.isExecuting = false; tab.isCancelling = false; tab.queryExecutionStartedAt = undefined; tab.executionId = undefined; touchResult(tab); } function clearAcknowledgedCancelIfStillRunning(id: string, executionId: string) { setTimeout(() => { const current = tabs.value.find((t) => t.id === id); if (!current || current.executionId !== executionId || !current.isCancelling) { pendingResultRunRestores.delete(executionId); return; } finishBatchSqlExecution(current, executionId, true); current.isExecuting = false; current.isCancelling = false; current.executionId = undefined; current.queryExecutionStartedAt = undefined; if (!restorePendingResultRun(current, executionId)) { current.result = toErrorResult(new Error("Query canceled")); current.results = undefined; current.activeResultIndex = undefined; current.resultSessionId = undefined; current.resultClientSessionId = undefined; touchResult(current); } clearLiveBatchSqlExecution(current, executionId); }, CANCEL_ACK_SETTLE_TIMEOUT_MS); } 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, options?: { skipRedisSafetyCheck?: boolean; sourceOffset?: number; openInNewResultTab?: boolean; onExecutionStarted?: () => void }) { const executionTabId = activeTabId.value; if (!executionTabId) return; const tab = tabs.value.find((item) => item.id === executionTabId); const previousGridKey = tab ? resultGridInstanceKey(tab) : undefined; if (tab?.mode === "query") { tab.resultSortColumn = undefined; tab.resultSortColumnIndex = undefined; tab.resultSortDirection = undefined; tab.resultSortMode = undefined; tab.resultSortedSql = undefined; } const producedResult = await executeTabSql(executionTabId, sql, { resultBaseSql: sql, resultSortedSql: undefined, ...options }); if (producedResult !== false) { const current = tabs.value.find((item) => item.id === executionTabId); if (current?.result) { current.resultGridRevision = uuid(); if (current.activeResultRunId) syncActiveResultRunFromDisplayed(current); await nextTick(); if (previousGridKey && options?.openInNewResultTab !== true) clearDataGridPendingSnapshot(previousGridKey); } } return producedResult; } type QueryMetadataPatch = Pick; type LoadedEditableSource = { source: EditableQuerySource; analysis: EditableQueryInfo; tableMeta: NonNullable; }; type EditableSourceMetadataTarget = { source: EditableQuerySource; analysis: EditableQueryInfo; request: TableMetadataRequest; writeSchema?: string; }; /** * Resolve result columns (by projection ordinal) back to exactly one base * column per source, then surface the resolved column comments and a * result->source mapping. Reuses the same database-aware binder * as the editability analysis, so `name AS username` (uniquely resolvable * unqualified alias) maps back to its physical column and quoted mixed-case * identifiers keep exact casing. Ambiguous or unresolved columns yield * `undefined` (no comment) instead of first-source-wins on a shared name. */ function resolveResultColumnInfo(dbType: string, analysis: EditableQueryInfo, resultColumns: string[], loadedSources: LoadedEditableSource[]): { comments: Array; mapping: Array } { const refs = resolveSourceColumnsByOrdinal( dbType, analysis, loadedSources.map((loaded) => ({ source: loaded.source, columns: loaded.tableMeta.columns })), resultColumns.length, ); const comments: Array = []; const mapping: Array = []; for (const ref of refs) { if (!ref) { comments.push(undefined); mapping.push(undefined); continue; } const loaded = loadedSources.find((entry) => entry.source.key === ref.sourceKey); const comment = loaded?.tableMeta.columns.find((column) => column.name === ref.sourceColumn)?.comment?.trim(); comments.push(comment || undefined); mapping.push({ ...ref, database: loaded?.tableMeta.database, schema: loaded?.tableMeta.schema, tableName: loaded?.tableMeta.tableName, }); } return { comments, mapping }; } function mysqlColumnIsGenerated(column: { extra: string | null }): boolean { const extra = column.extra?.trim().toLowerCase() ?? ""; return extra.includes("virtual generated") || extra.includes("stored generated"); } function groupedByExactlyOneSourcePrimaryKey(loaded: LoadedEditableSource, groupByRefs: Array): boolean { const primaryKeys = loaded.tableMeta.primaryKeys; if (!primaryKeys.length || groupByRefs.length !== primaryKeys.length) return false; const groupedColumns = groupByRefs.flatMap((ref) => (ref?.sourceKey === loaded.source.key ? [ref.sourceColumn] : [])); return groupedColumns.length === primaryKeys.length && new Set(groupedColumns).size === primaryKeys.length && primaryKeys.every((primaryKey) => groupedColumns.includes(primaryKey)); } function canInsertIntoEditableQuerySource(tab: QueryTab, databaseType: DatabaseType | undefined, loaded: LoadedEditableSource, sourceColumns: readonly (string | undefined)[] | undefined): boolean { if (!canInsertTableRows(databaseType) || !sourceColumns?.length || !sourceColumns.every(Boolean)) return false; const knownTableType = knownEditableQuerySourceTableType(tab, loaded); return !knownTableType?.toUpperCase().includes("VIEW"); } function knownEditableQuerySourceTableType(tab: QueryTab, loaded: LoadedEditableSource): string | undefined { return ( loaded.tableMeta.tableType ?? useConnectionStore() .lookupLocalCompletionTables(tab.connectionId!, loaded.tableMeta.database ?? tab.database, loaded.tableMeta.tableName, 20, loaded.tableMeta.schema, loaded.tableMeta.catalog) .find((table) => table.name.toLowerCase() === loaded.tableMeta.tableName.toLowerCase())?.type ); } interface EditableQueryExecutionPreparation { sql: string; metadataSql: string; hiddenPrimaryKeys: HiddenPrimaryKeyProjection[]; oracleLobPreview: boolean; } function applyQueryMetadataPatch(tab: QueryTab, patch: QueryMetadataPatch) { tab.queryAnalysis = patch.queryAnalysis; tab.querySourceColumns = patch.querySourceColumns; tab.queryEditabilityReason = patch.queryEditabilityReason; tab.mongoEditTarget = undefined; tab.tableMeta = patch.tableMeta; tab.resultColumnComments = patch.resultColumnComments; tab.queryDisplaySourceColumns = patch.queryDisplaySourceColumns; } function resolveEditableSourceMetadataTarget(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string): EditableSourceMetadataTarget { // Metadata must resolve in the same namespace as the query execution. An // empty query-tab database still executes in the connection's default DB, // while database-tree dialects and SQL Server 3-part names may override it // with a qualified source. const qualifiedSourceDatabase = dbType === "sqlserver" ? source.catalog : connectionUsesDatabaseObjectTreeMode(conn) ? source.schema : undefined; const metadataDatabase = qualifiedSourceDatabase || executionDatabase || conn?.database || tab.database; // SQL Server does not apply the query tab's selected schema to an // unqualified object reference. Resolve metadata through the login's // default schema (with the driver's dbo fallback) so metadata and writes // target the same object as the original SELECT. const schema = source.schema || (dbType === "sqlserver" ? "" : tab.schema) || ""; // Oracle-family connection databases are service names, not schemas. When // the query does not qualify a schema, let the driver resolve the current // login user's schema instead of looking up metadata under the service name. // Unqualified agent-backed PostgreSQL-family queries run in the // connection's current search_path. Do not reinterpret the selected // database as a schema; the agent reports the visible relation's actual // namespace with the columns. const resolveAgentSearchPathSchema = (dbType === "vastbase" || dbType === "kingbase") && !source.schema && !tab.schema; // PostgreSQL-compatible unqualified names also resolve through the // connection's search_path. Keep the metadata request unqualified when no // schema was selected instead of assuming public (or the database name). const useCurrentPostgresSchema = (dbType === "postgres" || dbType === "kwdb") && !source.schema && !tab.schema; const resolvedSchema = (dbType === "sqlserver" && !source.schema) || (ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema) || resolveAgentSearchPathSchema || useCurrentPostgresSchema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined); const metadataSchema = normalizeUppercaseFoldedMetadataIdentifier(dbType, resolvedSchema || undefined, source.schema ? source.schemaQuoted : false) || ""; const metadataTableName = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.tableName, source.tableNameQuoted)!; const metadataCatalog = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.catalog, source.catalogQuoted); const metadataSource: EditableQuerySource = { ...source, catalog: metadataCatalog, schema: metadataSchema || undefined, tableName: metadataTableName, }; // Keep SQL Server writes unqualified unless the SELECT source explicitly // named a schema, so SELECT and UPDATE resolve the same object. const writeSchema = dbType === "sqlserver" && !source.schema ? undefined : metadataSchema || undefined; const knownTableType = tab.tableMeta?.tableName.toLowerCase() === metadataTableName.toLowerCase() && normalizeOptionalSchema(tab.tableMeta.schema) === normalizeOptionalSchema(metadataSchema) ? tab.tableMeta.tableType : undefined; return { source: metadataSource, analysis: normalizeUppercaseFoldedQueryAnalysis(dbType, cloneAnalysisForSource(analysis, metadataSource), metadataSchema || undefined, metadataTableName), writeSchema, request: { connectionId: tab.connectionId!, database: metadataDatabase, schema: metadataSchema, tableName: metadataTableName, tableType: knownTableType, databaseType: dbType, driverProfile: conn?.driver_profile || conn?.db_type, catalog: metadataCatalog, }, }; } function loadedEditableSourceFromMetadata(target: EditableSourceMetadataTarget, metadata: Awaited>["metadata"]): LoadedEditableSource { const usesReportedSchema = target.request.databaseType === "vastbase" || target.request.databaseType === "kingbase"; const writeSchema = usesReportedSchema && !target.writeSchema ? metadata.schema : target.writeSchema; return { source: target.source, analysis: target.analysis, tableMeta: { catalog: target.request.catalog, database: target.request.database, schema: writeSchema, tableName: target.request.tableName, tableType: metadata.tableType, columns: metadata.columns, primaryKeys: metadata.primaryKeys, }, }; } function loadedEditableSourceFromColumns(target: EditableSourceMetadataTarget, loadedColumns: Awaited>): LoadedEditableSource { return { source: target.source, analysis: target.analysis, tableMeta: { catalog: target.request.catalog, database: target.request.database, schema: target.writeSchema, tableName: target.request.tableName, tableType: loadedColumns.tableType, columns: loadedColumns.columns, // MySQL getColumns already marks declared primary-key columns. Keep the // columns-only path free of index discovery while allowing grouped // results to prove that one physical row is uniquely identifiable. primaryKeys: target.request.databaseType === "mysql" && target.request.driverProfile === "mysql" ? loadedColumns.columns.filter((column) => column.is_primary_key).map((column) => column.name) : [], }, }; } async function loadEditableQuerySource(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string, traceId?: string, elapsed?: () => string): Promise { const target = resolveEditableSourceMetadataTarget(tab, analysis, source, conn, dbType, executionDatabase); queryExecutionLog("info", "metadata:table:start", { traceId, schema: target.request.schema, table: target.request.tableName, alias: source.alias, elapsed: elapsed?.(), }); const loadedMetadata = await loadTableMetadata({ ...target.request, traceLogger: (event) => queryExecutionLog("debug", "metadata:table-trace", { sourceTraceId: traceId, ...event }), }); const columns = loadedMetadata.metadata.columns; const primaryKeys = loadedMetadata.metadata.primaryKeys; queryExecutionLog("info", "metadata:table:done", { traceId, columnCount: columns.length, primaryKeyCount: primaryKeys.length, cacheStatus: loadedMetadata.cacheStatus, ageMs: Math.round(loadedMetadata.ageMs), elapsed: elapsed?.(), }); return loadedEditableSourceFromMetadata(target, loadedMetadata.metadata); } function missingPrimaryKeysForSource(primaryKeys: string[], analysis: EditableQueryInfo, sourceKey: string): string[] { if (analysis.selectStar) return []; const selectedColumns = new Set(analysis.columns.flatMap((column) => (column.sourceName && column.sourceKey === sourceKey ? [column.sourceName] : []))); return primaryKeys.filter((primaryKey) => !selectedColumns.has(primaryKey)); } function oracleRowIdIsSafeForQuery(tab: QueryTab, loaded: LoadedEditableSource): boolean { const knownType = loaded.tableMeta.tableType?.trim().toUpperCase(); if (knownType) return knownType === "TABLE"; const connectionStore = useConnectionStore(); const normalizeIdentifier = (value: string | undefined) => value?.trim().toLowerCase() ?? ""; const targetName = normalizeIdentifier(loaded.tableMeta.tableName); const resolvedSchema = loaded.tableMeta.schema?.trim() || tab.schema?.trim() || connectionStore.getConfig(tab.connectionId!)?.default_schema?.trim(); if (!resolvedSchema) return false; const targetSchema = normalizeIdentifier(resolvedSchema); const targetCatalog = loaded.tableMeta.catalog?.trim() ? normalizeIdentifier(loaded.tableMeta.catalog) : undefined; const matches = connectionStore .lookupLocalCompletionTables(tab.connectionId!, loaded.tableMeta.database ?? tab.database, loaded.tableMeta.tableName, 20, resolvedSchema, loaded.tableMeta.catalog) .filter((table) => normalizeIdentifier(table.name) === targetName && normalizeIdentifier(table.schema) === targetSchema && (!targetCatalog || normalizeIdentifier(table.catalog) === targetCatalog)); return matches.length === 1 && matches[0]?.type === "table"; } async function resolveOracleRowIdSafety(tab: QueryTab, loaded: LoadedEditableSource): Promise { if (oracleRowIdIsSafeForQuery(tab, loaded)) return true; if (loaded.tableMeta.tableType?.trim()) return false; const connection = useConnectionStore().getConfig(tab.connectionId!); const schema = loaded.tableMeta.schema?.trim() || tab.schema?.trim() || connection?.default_schema?.trim() || ""; const tables = await api.listTables(tab.connectionId!, loaded.tableMeta.database ?? tab.database, schema, loaded.tableMeta.tableName); const exactMatches = tables.filter((table) => table.name === loaded.tableMeta.tableName); if (exactMatches.length !== 1) return false; loaded.tableMeta.tableType = exactMatches[0]!.table_type; updateCachedTableMetadataType( { connectionId: tab.connectionId!, database: loaded.tableMeta.database ?? tab.database, schema: loaded.tableMeta.schema, tableName: loaded.tableMeta.tableName, databaseType: "oracle", driverProfile: connection?.driver_profile || connection?.db_type, catalog: loaded.tableMeta.catalog, }, loaded.tableMeta.tableType, ); return oracleRowIdIsSafeForQuery(tab, loaded); } function primaryKeyIndex(indexes: IndexInfo[]): IndexInfo | undefined { return indexes.find((index) => !index.filter && index.columns.length > 0 && index.is_primary); } function buildHiddenPrimaryKeyPreparation(tab: QueryTab, sql: string, databaseType: DatabaseType, loaded: LoadedEditableSource, primaryKeys: string[], declaredPrimaryKeys: string[], traceId: string, elapsed: () => string): EditableQueryExecutionPreparation { const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(databaseType, loaded.analysis, loaded.source, loaded.tableMeta.columns), loaded.source, loaded.tableMeta.columns); const oracleLobPreview = databaseType === "oracle" && primaryKeys.length > 0 && oracleRowIdIsSafeForQuery(tab, loaded) && oracleColumnsAllowDeferredLobMarkers(loaded.tableMeta.columns) && oracleQueryProjectsDeferredLob(metadataAnalysis, loaded.source.key, loaded.tableMeta.columns); const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [], oracleLobPreview }; const missingPrimaryKeys = declaredPrimaryKeys.length === 0 ? primaryKeys : missingPrimaryKeysForSource(primaryKeys, metadataAnalysis, loaded.source.key); if (missingPrimaryKeys.length === 0) return unchanged; const primaryKeySet = new Set(primaryKeys); const hasWritableProjection = metadataAnalysis.selectStar ? loaded.tableMeta.columns.some((column) => !primaryKeySet.has(column.name)) : metadataAnalysis.columns.some((column) => column.sourceName && column.sourceKey === loaded.source.key && !primaryKeySet.has(column.sourceName)); if (!hasWritableProjection) return unchanged; const rewritten = buildQueryWithHiddenPrimaryKeys({ sql, databaseType, primaryKeys: missingPrimaryKeys, existingResultNames: metadataAnalysis.selectStar ? loaded.tableMeta.columns.map((column) => column.name) : metadataAnalysis.columns.map((column) => column.resultName), sourceExpressions: databaseType === "oracle" && missingPrimaryKeys.includes(DBX_ROWID_COLUMN) ? { [DBX_ROWID_COLUMN]: "ROWIDTOCHAR(ROWID)" } : undefined, }); if (!rewritten) return unchanged; queryExecutionLog("info", "hidden-primary-keys", { traceId, table: loaded.tableMeta.tableName, keyCount: rewritten.projections.length, elapsed: elapsed(), }); return { sql: rewritten.sql, metadataSql: rewritten.sql, hiddenPrimaryKeys: rewritten.projections, oracleLobPreview }; } async function prepareEditableQueryExecution(tab: QueryTab, sql: string, conn: ConnectionConfig | undefined, databaseType: DatabaseType | undefined, executionDatabase: string, traceId: string, elapsed: () => string): Promise { const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [], oracleLobPreview: false }; if (!databaseType || !HIDDEN_QUERY_KEY_DATABASE_TYPES.has(databaseType) || !tab.connectionId) return unchanged; try { const editability = analyzeEditableQueryEditability(sql); if (!editability.editable || !editability.analysis) return unchanged; const analysis = editability.analysis; const sources = editableQuerySources(analysis); if (sources.length !== 1 || analysis.distinct) return unchanged; const source = sources[0]!; const wholeSourceProjected = projectsAllColumnsForSource(analysis, source.key); const hasDirectSourceProjection = analysis.columns.some((column) => Boolean(column.sourceName) && (!column.sourceKey || column.sourceKey === source.key)); if (!wholeSourceProjected && !hasDirectSourceProjection) return unchanged; // Whole-source projections already include declared primary keys. Only // Oracle needs preflight metadata here to add ROWID for a keyless table. if (databaseType !== "oracle" && wholeSourceProjected) return unchanged; const target = resolveEditableSourceMetadataTarget(tab, analysis, source, conn, databaseType, executionDatabase); const cached = getCachedTableMetadata(target.request); let loaded = cached ? loadedEditableSourceFromMetadata(target, cached.metadata) : undefined; if (!cached && databaseType === "oracle") { // Oracle column discovery can be slow. A star projection over a table // with a declared primary key already returns the complete row identity, // so SQL can start while the full metadata needed for editing loads. const fullMetadataPromise = loadTableMetadata({ ...target.request, traceLogger: (event) => queryExecutionLog("debug", "metadata:table-trace", { sourceTraceId: traceId, ...event }), }); void fullMetadataPromise.catch((error) => queryExecutionLog("warn", "metadata:table-prefetch:failed", { traceId, error, elapsed: elapsed() })); const wholeSourceAutoCommit = projectsAllColumnsForSource(target.analysis, target.source.key) && tab.autoCommit !== false; if (wholeSourceAutoCommit) { const indexes = await waitForOracleQueryMetadataPreflight(loadTableIndexes(target.request)); if (indexes === ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT) { queryExecutionLog("info", "metadata:preflight:timeout", { traceId, table: target.request.tableName, budgetMs: ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS, elapsed: elapsed(), }); return unchanged; } if (primaryKeyIndex(indexes)) return unchanged; } loaded = loadedEditableSourceFromMetadata(target, (await fullMetadataPromise).metadata); } loaded ??= await loadEditableQuerySource(tab, analysis, source, conn, databaseType, executionDatabase, traceId, elapsed); if (loaded.tableMeta.columns.length === 0) return unchanged; if (loaded.tableMeta.tableType?.toUpperCase().includes("VIEW")) return unchanged; const columnPrimaryKeys = loaded.tableMeta.columns.filter((column) => column.is_primary_key).map((column) => column.name); const primaryKeys = databaseType === "oracle" ? loaded.tableMeta.primaryKeys : editablePrimaryKeys(databaseType, loaded.tableMeta.columns, loaded.tableMeta.tableType); const syntheticOracleRowId = databaseType === "oracle" && usesSyntheticRowIdKey(databaseType, primaryKeys, loaded.tableMeta.tableType); // Oracle base tables without a natural identifier use the same ROWID // identity as table-data tabs. Confirm the object is a base table because // selecting ROWID from a view can fail with ORA-01445. if (syntheticOracleRowId && !(await resolveOracleRowIdSafety(tab, loaded))) return unchanged; const declaredPrimaryKeys = databaseType === "oracle" && !syntheticOracleRowId ? primaryKeys : columnPrimaryKeys; return buildHiddenPrimaryKeyPreparation(tab, sql, databaseType, loaded, primaryKeys, declaredPrimaryKeys, traceId, elapsed); } catch (error) { // Metadata enrichment is optional. Query execution must retain its prior // behavior when metadata is unavailable or the SQL cannot be rewritten. queryExecutionLog("warn", "hidden-primary-keys:skip", { traceId, error, elapsed: elapsed() }); return unchanged; } } /** * Resolve grouped result columns by projection ordinal. All databases use the * mapping for comments. MySQL may additionally edit direct columns from one * uniquely identifiable base table; aggregate expressions and columns from * every other source remain read-only. */ async function resolveAggregationQueryMetadata(tab: QueryTab, sql: string, executionDatabase: string, traceId: string | undefined): Promise { if (tab.mode !== "query" || !tab.connectionId || !tab.result || !tab.result.columns.length) return undefined; const conn = useConnectionStore().getConfig(tab.connectionId); const dbType = conn?.db_type || ""; const analysis = analyzeSelectStructureForDisplay(sql); if (!analysis) return undefined; const sources = editableQuerySources(analysis); if (!sources.length) return undefined; try { // Resolve every source's metadata target up front, then load *columns // only* (display payload — no index discovery) under a connection-scoped // bounded-concurrency limiter. The shared table-column cache/in-flight // coordinator deduplicates identical tables across concurrent callers, // so this path issues no indexes requests and never loads full editable // metadata. const loadedSources: LoadedEditableSource[] = []; const targets = sources.map((source) => resolveEditableSourceMetadataTarget(tab, analysis, source, conn, dbType, executionDatabase)); const limiterScope = `${GROUPED_DISPLAY_LIMITER_SCOPE_PREFIX}${tab.connectionId}`; await Promise.all( targets.map((target) => groupedDisplayMetadataLimiter.run(limiterScope, "query-column-comments", async () => { const loadedColumns = await loadTableColumns({ ...target.request, traceLogger: (event) => queryExecutionLog("debug", "metadata:table-columns-trace", { sourceTraceId: traceId, ...event }), }); loadedSources.push(loadedEditableSourceFromColumns(target, loadedColumns)); }), ), ); const displayInfo = resolveResultColumnInfo(dbType, analysis, tab.result.columns, loadedSources); const readOnlyPatch: QueryMetadataPatch = { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "aggregation", tableMeta: undefined, resultColumnComments: displayInfo.comments, queryDisplaySourceColumns: displayInfo.mapping, }; if (dbType !== "mysql" || (conn?.driver_profile || conn?.db_type) !== "mysql") return readOnlyPatch; // Mutation safety boundary: the FROM root must remain on the preserved // side of the join tree, and GROUP BY must resolve to exactly that table's // declared primary key. This makes every editable result row identify one // physical root row even when joined rows are collapsed by aggregation. if (analysis.distinct || analysis.hasHavingClause || analysis.hasWindowClause || analysis.hasRightJoinClause || !analysis.groupByColumns?.length) return readOnlyPatch; const groupByRefs = resolveSourceColumnsByOrdinal( dbType, { ...analysis, selectStar: false, columns: analysis.groupByColumns }, loadedSources.map((loaded) => ({ source: loaded.source, columns: loaded.tableMeta.columns })), analysis.groupByColumns.length, ); const candidates = loadedSources .map((loaded) => { const sourceColumns = displayInfo.mapping.map((ref) => { if (ref?.sourceKey !== loaded.source.key) return undefined; const column = loaded.tableMeta.columns.find((candidate) => candidate.name === ref.sourceColumn); return column && !mysqlColumnIsGenerated(column) ? ref.sourceColumn : undefined; }); const primaryKeySet = new Set(loaded.tableMeta.primaryKeys); const hasCompletePrimaryKey = loaded.tableMeta.primaryKeys.length > 0 && loaded.tableMeta.primaryKeys.every((primaryKey) => sourceColumns.includes(primaryKey)); const editableSourceColumnCount = sourceColumns.filter((column) => column && !primaryKeySet.has(column)).length; const hasExactPrimaryKeyGrouping = groupedByExactlyOneSourcePrimaryKey(loaded, groupByRefs); return { ...loaded, sourceColumns, isRootSource: loaded.source.key === sources[0]!.key, hasCompletePrimaryKey, hasExactPrimaryKeyGrouping, editableSourceColumnCount }; }) .filter((loaded) => loaded.isRootSource && loaded.hasCompletePrimaryKey && loaded.hasExactPrimaryKeyGrouping && loaded.editableSourceColumnCount > 0 && !knownEditableQuerySourceTableType(tab, loaded)?.toUpperCase().includes("VIEW")); // More than one writable source is ambiguous. Refuse the entire result // instead of guessing which table an edit should mutate. if (candidates.length !== 1) return readOnlyPatch; const target = candidates[0]!; return { queryAnalysis: { ...target.analysis, editableSourceKey: target.source.key, allowInsert: false, allowInsertDelete: false, multiSource: sources.length > 1, }, querySourceColumns: target.sourceColumns, queryEditabilityReason: undefined, tableMeta: target.tableMeta, resultColumnComments: displayInfo.comments, queryDisplaySourceColumns: displayInfo.mapping, }; } catch (err) { console.error("[DBX] ERROR fetching columns for grouped query metadata:", err); return undefined; } } async function buildQueryMetadataPatch(tab: QueryTab, sql: string, executionDatabase: string, traceId?: string, elapsed?: () => string, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []): Promise { if (tab.mode !== "query") return; if (!tab.result || !tab.result.columns.length) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: undefined, tableMeta: undefined, }; } queryExecutionLog("info", "metadata:editability:start", { traceId, elapsed: elapsed?.() }); const editability = await api.analyzeEditableQueryEditability(sql); queryExecutionLog("info", "metadata:editability:done", { traceId, editable: editability.editable, reason: editability.editable ? undefined : editability.reason, elapsed: elapsed?.(), }); if (!editability.editable) { const aggregationPatch = editability.reason === "aggregation" ? await resolveAggregationQueryMetadata(tab, sql, executionDatabase, traceId) : undefined; if (aggregationPatch) return aggregationPatch; return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: editability.reason, tableMeta: undefined, }; } const analysis = editability.analysis; if (!tab.connectionId) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "metadata-unavailable", tableMeta: undefined, }; } const connStore = useConnectionStore(); const conn = connStore.getConfig(tab.connectionId); const dbType = conn?.db_type || ""; const sources = editableQuerySources(analysis); const loadedSources: LoadedEditableSource[] = []; try { for (const source of sources) { loadedSources.push(await loadEditableQuerySource(tab, analysis, source, conn, dbType, executionDatabase, traceId, elapsed)); } const allSourceColumns = loadedSources.map((source) => ({ source: source.source, columns: source.tableMeta.columns })); // Match DBeaver's safety model: a joined result is writable only when one // source table has a complete row identifier and at least one writable column. // A keyless source has no row identifier: allPrimaryKeysPresent is vacuously // true for an empty key set, so joined results must exclude such sources. const candidates = loadedSources .map((loaded) => { const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(dbType, loaded.analysis, loaded.source, loaded.tableMeta.columns, allSourceColumns), loaded.source, loaded.tableMeta.columns); const primaryKeys = loaded.tableMeta.primaryKeys; const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result!.columns, loaded.source.key); const primaryKeysPresent = primaryKeysPresentForSource(dbType, primaryKeys, tab.result!.columns, metadataAnalysis, loaded.source.key, loaded.tableMeta.columns); const keylessAllowed = sources.length === 1 && canUseKeylessRowPredicate(dbType as DatabaseType, primaryKeys); const primaryKeySet = new Set(primaryKeys); const editableSourceColumnCount = (sourceColumns ?? []).filter((column) => column && !primaryKeySet.has(column)).length; return { ...loaded, analysis: metadataAnalysis, sourceColumns, primaryKeysPresent, keylessAllowed, editableSourceColumnCount, }; }) .filter((loaded) => ((loaded.primaryKeysPresent && loaded.tableMeta.primaryKeys.length > 0) || loaded.keylessAllowed) && !!loaded.sourceColumns && loaded.editableSourceColumnCount > 0); if (loadedSources.length === 1) { const loaded = loadedSources[0]!; const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(dbType, loaded.analysis, loaded.source, loaded.tableMeta.columns, allSourceColumns), loaded.source, loaded.tableMeta.columns); const syntheticRowIdProjection = hiddenPrimaryKeys.find((projection) => projection.sourceName.toUpperCase() === DBX_ROWID_COLUMN); const primaryKeys = loaded.tableMeta.primaryKeys.length === 0 && syntheticRowIdProjection ? [DBX_ROWID_COLUMN] : loaded.tableMeta.primaryKeys; const displaySourceInfo = resolveResultColumnInfo(dbType, analysis, tab.result.columns, loadedSources); const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result.columns, loaded.source.key); if (sourceColumns && syntheticRowIdProjection) { const resultIndex = tab.result.columns.findIndex((column) => column.toLowerCase() === syntheticRowIdProjection.alias.toLowerCase()); if (resultIndex >= 0) sourceColumns[resultIndex] = DBX_ROWID_COLUMN; } if (primaryKeys.length === 0 && !canUseKeylessRowPredicate(dbType as DatabaseType, primaryKeys)) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "no-primary-key", tableMeta: loaded.tableMeta, resultColumnComments: displaySourceInfo.comments, queryDisplaySourceColumns: displaySourceInfo.mapping, }; } const primaryKeysPresent = syntheticRowIdProjection ? sourceColumns?.some((column) => column?.toUpperCase() === DBX_ROWID_COLUMN) === true : primaryKeysPresentForSource(dbType, primaryKeys, tab.result.columns, metadataAnalysis, loaded.source.key, loaded.tableMeta.columns); if (!primaryKeysPresent) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "primary-key-not-returned", tableMeta: loaded.tableMeta, queryDisplaySourceColumns: displaySourceInfo.mapping, }; } if (!allEditableColumnsWriteable(metadataAnalysis, tab.result.columns)) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "aliased-columns", tableMeta: loaded.tableMeta, queryDisplaySourceColumns: displaySourceInfo.mapping, }; } return { queryAnalysis: metadataAnalysis.distinct && canInsertIntoEditableQuerySource(tab, dbType as DatabaseType, loaded, sourceColumns) ? { ...metadataAnalysis, allowInsert: true } : metadataAnalysis, querySourceColumns: sourceColumns, queryEditabilityReason: undefined, tableMeta: primaryKeys === loaded.tableMeta.primaryKeys ? loaded.tableMeta : { ...loaded.tableMeta, primaryKeys }, resultColumnComments: primaryKeys.length === 0 ? displaySourceInfo.comments : undefined, queryDisplaySourceColumns: displaySourceInfo.mapping, }; } // Multi-source results cannot carry a single tableMeta, but every source // table's metadata is already loaded. Surface per-ordinal column comments // and a display-only result->source mapping so the data grid can still // show comments for joined results (fixes #2129 / #6352). const multiSourceInfo = loadedSources.length > 1 ? resolveResultColumnInfo(dbType, analysis, tab.result.columns, loadedSources) : undefined; if (candidates.length === 0) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: loadedSources.some((loaded) => loaded.tableMeta.primaryKeys.length > 0) ? "primary-key-not-returned" : "no-primary-key", tableMeta: undefined, resultColumnComments: multiSourceInfo?.comments, queryDisplaySourceColumns: multiSourceInfo?.mapping, }; } if (candidates.length > 1) { return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "complex-source", tableMeta: undefined, resultColumnComments: multiSourceInfo?.comments, queryDisplaySourceColumns: multiSourceInfo?.mapping, }; } const target = candidates[0]!; const queryAnalysis = { ...target.analysis, ...(target.analysis.distinct && canInsertIntoEditableQuerySource(tab, dbType as DatabaseType, target, target.sourceColumns) ? { allowInsert: true } : {}), allowDelete: !target.analysis.distinct, allowInsertDelete: false, multiSource: true, }; return { queryAnalysis, querySourceColumns: target.sourceColumns, queryEditabilityReason: undefined, tableMeta: target.tableMeta, resultColumnComments: multiSourceInfo?.comments, queryDisplaySourceColumns: multiSourceInfo?.mapping, }; } catch (err) { console.error("[DBX] ERROR fetching columns for query metadata:", err); return { queryAnalysis: undefined, querySourceColumns: undefined, queryEditabilityReason: "metadata-unavailable", tableMeta: undefined, }; } } function analyzeQueryMetadataInBackground(tabId: string, sql: string, result: QueryResult, executionDatabase: string, traceId: string, elapsed: () => string, databaseType: DatabaseType | undefined, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []) { void (async () => { const tab = tabs.value.find((t) => t.id === tabId); if (!tab || tab.result !== result) return; queryExecutionLog("info", "metadata:start", { traceId, elapsed: elapsed() }); const patch = await buildQueryMetadataPatch(tab, sql, executionDatabase, traceId, elapsed, hiddenPrimaryKeys); if (patch?.queryAnalysis && hasHiddenPhysicalRowKey(databaseType, hiddenPrimaryKeys)) { patch.queryAnalysis = { ...patch.queryAnalysis, allowInsert: false }; } const current = tabs.value.find((t) => t.id === tabId); if (patch && current?.result === result) { applyQueryMetadataPatch(current, patch); syncActiveResultRunFromDisplayed(current); queryExecutionLog("info", "metadata:done", { traceId, elapsed: elapsed() }); } else { queryExecutionLog("warn", "metadata:stale", { traceId, elapsed: elapsed() }); } })(); } function setQueryTotalRowCountIfCurrent(tabId: string, executionId: string, result: QueryResult, totalRowCount: number | undefined) { const current = tabs.value.find((t) => t.id === tabId); if (!current || (current.mode !== "query" && current.mode !== "data")) return; if (current.executionId !== executionId && current.result !== result) return; current.resultTotalRowCount = totalRowCount; current.resultTotalRowCountLoading = false; syncActiveResultRunFromDisplayed(current); } type TotalRowCountSqlTarget = { sql: string; schema?: string }; function countQueryTotalRowsInBackground(options: { tabId: string; connectionId: string; database: string; schema?: string; catalog?: string; countSql?: string; countSqlTarget?: () => Promise; result: QueryResult; pageLimit?: number; pageOffset?: number; useAgentResultSession?: boolean; executionId: string; traceId: string; elapsed: () => string; timeoutSecs: number; }) { const resultRowCount = options.result.rows.length; if (resultRowCount <= 0) { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } const exactIncompletePageTotal = exactTotalFromIncompletePage(options.result, options.pageLimit, options.pageOffset, options.useAgentResultSession); if (typeof exactIncompletePageTotal === "number") { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, exactIncompletePageTotal); return; } // A full page was returned, so more rows may exist and determining the true // total requires a potentially expensive COUNT(*) over the user's query. // Only run it automatically when the user opted in; otherwise leave the // total unknown and let them trigger it on demand from the result grid // (matches DBeaver's default of not counting large result sets). if (!useSettingsStore().editorSettings.autoCalculateTotalRows) { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } const clientSessionId = tabClientSessionId({ id: options.tabId }, "count"); const countExecutionId = `${options.executionId}:count`; void (async () => { try { const countTarget = options.countSql ? { sql: options.countSql, schema: options.schema } : await options.countSqlTarget?.(); if (!countTarget?.sql) { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } queryExecutionLog("info", "count:start", { traceId: options.traceId, elapsed: options.elapsed() }); const countResult = await api.executeQuery(options.connectionId, options.database, countTarget.sql, countTarget.schema, countExecutionId, { clientSessionId, catalog: options.catalog, timeoutSecs: options.timeoutSecs, }); const total = Number(countResult.rows?.[0]?.[0] ?? 0); if (!Number.isFinite(total) || total < 0) { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } // COUNT describes all matching rows; the configured result limit only // constrains how many of them pagination may load and retain. setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, total); queryExecutionLog("info", "count:done", { traceId: options.traceId, total, elapsed: options.elapsed(), }); } catch (error) { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); queryExecutionLog("warn", "count:error", { traceId: options.traceId, elapsed: options.elapsed(), error, }); } finally { void closeClientSessionId(options.connectionId, options.database, clientSessionId, options.catalog, { tabId: options.tabId }); } })(); } async function executeTabSql( id: string, sql: string, options?: { resultBaseSql?: string; resultSortedSql?: string | undefined; querySort?: { resultColumns: string[]; columnIndex: number; column: string; direction: "asc" | "desc"; }; pagination?: { limit: number; offset: number; sessionId?: string; clientSessionId?: string }; appendResult?: { maxRows: number }; mongoSafety?: MongoAggregateSafetyOptions; preserveResultDuringExecution?: boolean; preserveTotalRowCountDuringExecution?: boolean; preserveActiveResultIndex?: boolean; replaceActiveResultInGroup?: boolean; retainDisplayedResult?: boolean; skipRedisSafetyCheck?: boolean; sourceOffset?: number; sourceTraceId?: string; skipEnsureConnected?: boolean; openInNewResultTab?: boolean; targetContext?: SqlExecutionTargetContext; executionTarget?: MultiDbExecutionTarget; onExecutionStarted?: () => void; batchResume?: BatchSqlResumeOptions; }, ) { const tab = findExecutionTab(id); if (!tab || !sql.trim()) return; const openInNewResultTab = tab.mode === "query" && options?.openInNewResultTab === true; // Auto-saved results need two independent decisions: keep the currently // displayed run visible while the request is pending, then capture the new // response as another run. Previously `resultAutoSave` only made the latter // decision after clearing the displayed payload, which caused the result // toolbar and grid to briefly disappear before the next Run was added. const captureAutoSavedResultRun = tab.mode === "query" && tab.resultAutoSave === true && !!tab.activeResultRunId && !!tab.result; let captureResultRun = openInNewResultTab || captureAutoSavedResultRun; if (!captureResultRun && tab.mode === "query" && !tab.resultAutoSave && tab.activeResultRunId) { const activeRun = tab.resultRuns?.find((run) => run.id === tab.activeResultRunId); if (activeRun?.pinned) { const reusableRun = tab.resultRuns?.find((run) => !run.pinned); if (reusableRun) { // A stale disk snapshot must not make us fall back to overwriting // the pinned active run. Capture a fresh run instead. captureResultRun = !(await setActiveResultRun(id, reusableRun.id)); } else { captureResultRun = true; } } } if (captureResultRun && tab.activeResultRunId && !tab.result) { await setActiveResultRun(id, tab.activeResultRunId); if (findExecutionTab(id) !== tab) return false; } const executionId = uuid(); const executionEditorFingerprint = tab.mode === "query" ? sqlTextFingerprint(tab.sql) : undefined; const traceId = executionId.slice(0, 8); const startedAt = performance.now(); const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`; tab.isExecuting = true; options?.onExecutionStarted?.(); tab.isCancelling = false; if (!tab.queryExecutionStartedAt) { tab.queryExecutionStartedAt = Date.now(); } tab.executionId = executionId; const tableDataNativeSelectionBlockOwner = tab.mode === "data" ? {} : undefined; if (tableDataNativeSelectionBlockOwner) beginDataGridNativeSelectionBlock(tableDataNativeSelectionBlockOwner); const previousDisplayedSql = tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql; tab.lastExecutedSql = sql; tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; if (captureResultRun && tab.result && !tab.activeResultRunId) { captureDisplayedResultRun(tab, previousDisplayedSql); } if (captureResultRun && tab.activeResultRunId) { pendingResultRunRestores.set(executionId, tab.activeResultRunId); } const batchResume = options?.batchResume; const continueOnBatchError = batchResume?.continueOnError ?? settingsStore.editorSettings.continueOnErrorOnBatch; if (batchResume) { tab.batchSqlExecution = prepareBatchSqlRecovery(batchResume.batch, executionId, batchResume.startStatementIndex); liveBatchSqlExecutions.set(tab, tab.batchSqlExecution); } else { tab.batchSqlExecution = undefined; liveBatchSqlExecutions.delete(tab); } const preserveResultDuringExecution = batchResume !== undefined || options?.preserveResultDuringExecution === true || captureAutoSavedResultRun || (tab.mode === "query" && !!tab.activeResultRunId && !tab.resultAutoSave && !captureResultRun); const updateActiveResultRun = !!tab.activeResultRunId && preserveResultDuringExecution; if (!updateActiveResultRun) { tab.activeResultRunId = undefined; } if (!options?.preserveTotalRowCountDuringExecution) { tab.resultTotalRowCount = undefined; } tab.resultTotalRowCountLoading = false; const previousResultSessionClose = closeResultSession(tab, options?.pagination?.sessionId); if (!preserveResultDuringExecution || !tab.result) { clearResultPayload(tab, { preserveCacheSnapshot: captureResultRun && pendingResultRunRestores.has(executionId) }); } queryExecutionLog("info", "start", { traceId, tabId: id, mode: tab.mode, sourceTraceId: options?.sourceTraceId, sqlLength: sql.length, }); const queryBaseSql = options?.resultBaseSql ?? sql; let sqlToExecute = sql; let resultSortedSql = options?.resultSortedSql; let queryMetadataSql = queryBaseSql; let hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []; let useOracleLobPreview = false; let pageSql: string | undefined; let requestedPageLimit: number | undefined; let pageLimit: number | undefined; let pageOffset: number | undefined; let countSql: string | undefined; let exactQueryRowBound: number | undefined; let useAgentResultSession = false; let executionDispatched = false; let producedResult = false; const resumedExecutionTarget = batchResume?.batch.executionTarget; const executionConnectionId = resumedExecutionTarget?.connectionId ?? options?.executionTarget?.connectionId ?? tab.connectionId; try { await waitForTabSessionReset(id); const connStore = useConnectionStore(); const executionTarget = resumedExecutionTarget ?? options?.executionTarget; const usesExternalExecutionTarget = !!executionTarget; let conn = connStore.getConfig(executionConnectionId); const parsedMongoCommands = conn?.db_type === "mongodb" ? splitMongoCommandRanges(sql) : undefined; let mongoCommands = parsedMongoCommands ?? []; const mongoNeedsConnection = mongoCommands.some(({ command }) => command.kind !== "use"); if (options?.skipEnsureConnected) { queryExecutionLog("info", "ensure-connected:skip", { traceId, elapsed: elapsed(), reason: "caller" }); } else if (conn?.db_type === "mongodb" && mongoCommands.length > 0 && !mongoNeedsConnection) { queryExecutionLog("info", "ensure-connected:skip", { traceId, elapsed: elapsed(), reason: "mongo-use-only" }); } else { queryExecutionLog("info", "ensure-connected:start", { traceId, elapsed: elapsed() }); await connStore.ensureConnected(executionConnectionId); queryExecutionLog("info", "ensure-connected:done", { traceId, elapsed: elapsed() }); } conn = connStore.getConfig(executionConnectionId); if (parsedMongoCommands === undefined && conn?.db_type === "mongodb") { mongoCommands = splitMongoCommandRanges(sql); } const effectiveDbType = effectiveDatabaseTypeForConnection(conn); if (tab.autoCommit === false && !supportsTransaction(conn?.db_type)) { tab.autoCommit = true; } const targetContext = options?.targetContext; if (targetContext?.scope === "namespace") { throw new Error("Namespace execution targets require a registered execution adapter."); } const databaseTargetContext = targetContext?.scope === "catalog" || targetContext?.scope === "database" ? targetContext : undefined; const executionCatalog = resumedExecutionTarget ? resumedExecutionTarget.catalog : targetContext ? (targetContext.scope === "catalog" ? targetContext.catalog : undefined) : (executionTarget?.catalog ?? (tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog)); const contextDatabase = databaseTargetContext?.database; const targetDatabase = resumedExecutionTarget ? resumedExecutionTarget.database : targetContext?.scope === "connection" ? "" : (contextDatabase ?? executionTarget?.database ?? tab.database); const targetSchema = resumedExecutionTarget ? resumedExecutionTarget.schema : targetContext?.scope === "connection" ? undefined : (databaseTargetContext?.schema ?? executionTarget?.schema ?? tab.schema); const executionDatabase = dataTabExecutionDatabase(conn, targetDatabase, executionCatalog); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile); const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); if (!batchResume) { const statementExecution = tab.mode === "query" ? createBatchSqlExecution(executionId, tab.sql, sql, effectiveDbType, options?.sourceOffset, { connectionId: executionConnectionId, catalog: executionCatalog, database: targetDatabase, schema: targetSchema, }) : undefined; tab.batchSqlExecution = statementExecution && (tab.autoCommit !== false || statementExecution.total === 1) ? statementExecution : undefined; if (tab.batchSqlExecution) liveBatchSqlExecutions.set(tab, tab.batchSqlExecution); } queryExecutionLog("info", "previous-session-close:start", { traceId, elapsed: elapsed() }); await previousResultSessionClose; queryExecutionLog("info", "previous-session-close:done", { traceId, elapsed: elapsed() }); // Redis command execution — split multi-line input into individual commands if (conn?.db_type === "redis") { await connStore.ensureConnected(executionConnectionId); let currentDb = Number(executionDatabase) || Number(tab.database) || 0; const commands = sql .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); if (commands.length === 0) return false; queryExecutionLog("info", "redis:start", { traceId, db: currentDb, commandCount: commands.length, sqlLength: sql.length }); const allResults: QueryResult[] = []; const commandRanges = executableStatementRanges(sql, "redis"); const skipSafety = options?.skipRedisSafetyCheck; let hadMutatingCommand = false; for (const [commandIndex, command] of commands.entries()) { const commandRange = commandRanges[commandIndex]; const sourceRange = commandRange && options?.sourceOffset !== undefined ? { from: options.sourceOffset + commandRange.from, to: options.sourceOffset + commandRange.to } : undefined; try { const result = await api.redisExecuteCommand(executionConnectionId, currentDb, command, skipSafety); allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, command), command, undefined, undefined, sourceRange))); // Track db switches from SELECT N so later commands in the same batch run on the right db. currentDb = nextRedisCommandDb(currentDb, command, result.value); // Write commands (SET/DEL/...) mutate the key set — drop the cached key-name completion // for the db this command ran on so the next autocomplete fetch reflects the new keys. if (isRedisMutatingCommand(command)) { hadMutatingCommand = true; connStore.invalidateCompletionCache(executionConnectionId, String(currentDb)); } } catch (e: any) { allResults.push(annotateQueryResultSource(toErrorResult(e), command, undefined, undefined, sourceRange)); } } queryExecutionLog("info", "redis:done", { traceId, commandCount: commands.length, elapsed: elapsed() }); const current = findExecutionTab(id); if (current?.executionId === executionId) { if (captureResultRun && current.isCancelling && restorePendingResultRun(current, executionId)) return false; if (allResults.length > 1) { const activeResultIndex = allResults.findIndex((result) => !isQueryExecutionErrorResult(result)); const resultIndex = preservedResultIndex(allResults, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (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]; } producedResult = current.result !== undefined; touchResult(current); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.resultColumnComments = undefined; current.queryDisplaySourceColumns = undefined; current.queryEditabilityReason = undefined; current.mongoEditTarget = undefined; current.tableMeta = undefined; current.resultBaseSql = options?.resultBaseSql ?? sql; current.resultSortedSql = options?.resultSortedSql; syncDisplayedResultRun(current, options?.resultBaseSql ?? sql, captureResultRun); // Reflect db switches from SELECT N in the tab so the toolbar dropdown, tab title and // sidebar stay in sync with the command's effective db. if (!usesExternalExecutionTarget && current.database !== String(currentDb)) { current.database = String(currentDb); } } // Refresh the sidebar db key counts (INFO keyspace) when at least one command in // this batch mutated the key set, so `dbN (count)` stays accurate without a manual // refresh. Fire-and-forget: never block result display. if (hadMutatingCommand) { void connStore.refreshRedisDbKeyCounts(executionConnectionId); } return producedResult; } if (conn?.db_type === "mongodb" && mongoCommands.length === 0 && sql.trim()) { // Avoid falling through to the SQL executor, which only returns the generic // "Use MongoDB-specific commands" rejection and hides parse/syntax details. throw new Error(describeMongoCommandParseFailure(sql)); } if (mongoCommands.length > 0) { queryExecutionLog("info", "mongo:start", { traceId, commandCount: mongoCommands.length, sqlLength: sql.length }); const allResults: QueryResult[] = []; const mongoResultMaxRows = effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows); // Track the effective db as we walk the batch so later commands observe // earlier `use ...` statements in the same editor selection. let currentDatabase = executionTarget?.database ?? tab.database; let mongoEditTarget: QueryTab["mongoEditTarget"] | undefined; let mongoFindPageState: { pageLimit: number; pageOffset: number; total: number; totalIsExact: boolean } | undefined; for (const parsedCommand of mongoCommands) { let mongoCommand = parsedCommand.command; const sourceStatement = parsedCommand.text; const sourceRange = options?.sourceOffset === undefined ? undefined : { from: options.sourceOffset + parsedCommand.from, to: options.sourceOffset + parsedCommand.to }; const commandStartedAt = performance.now(); const annotateMongoResult = (result: QueryResult): QueryResult => { const annotated = annotateQueryResultSource(result, sourceStatement, undefined, undefined, sourceRange); if ("collection" in mongoCommand) { annotated.sourceLabel = currentDatabase ? `${currentDatabase}.${mongoCommand.collection}` : mongoCommand.collection; } return annotated; }; try { // The frontend parser remains responsible for editor ranges, while // dbx-core is authoritative for command semantics at execution time. mongoCommand = await api.mongoParseShellCommand(sourceStatement); switch (mongoCommand.kind) { case "find": { queryExecutionLog("info", "mongo-find:start", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const pagePlan = planMongoFindPagination(sourceStatement, mongoCommand, options?.pagination?.offset ?? 0, normalizeResultPageSize(options?.pagination?.limit ?? settingsStore.editorSettings.pageSize)); if (!pagePlan) throw new Error(describeMongoCommandParseFailure(sourceStatement)); // A stale request can point past an explicit .limit() bound. Keep // the backend call bounded so limit(0) cannot become unbounded. const result = await api.mongoFindDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, pagePlan.requestSkip, Math.max(1, pagePlan.requestLimit), mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, mongoCommand.collation, executionId); const documents = pagePlan.requestLimit === 0 ? [] : result.documents; const extendedDocuments = pagePlan.requestLimit === 0 ? [] : result.extended_documents; const totalIsExact = result.total_is_exact !== false; const reportedTotal = mongoFindLogicalTotal(result.total, pagePlan); const loadedLowerBound = pagePlan.pageOffset + documents.length; const total = totalIsExact ? reportedTotal : Math.max(reportedTotal, loadedLowerBound); const hasMore = totalIsExact ? loadedLowerBound < total : pagePlan.requestLimit > 0 && documents.length >= pagePlan.requestLimit && (pagePlan.logicalLimit === undefined || loadedLowerBound < pagePlan.logicalLimit); const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(documents, performance.now() - commandStartedAt, total, extendedDocuments, totalIsExact))); queryResult.truncated = hasMore; queryResult.has_more = hasMore; allResults.push(queryResult); if (mongoCommands.length === 1) { mongoFindPageState = { pageLimit: pagePlan.pageLimit, pageOffset: pagePlan.pageOffset, total, totalIsExact }; } mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined; queryExecutionLog("info", "mongo-find:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, rowCount: result.documents.length, total: result.total, elapsed: elapsed(), }); break; } case "findOne": { queryExecutionLog("info", "mongo-find-one:start", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const result = await api.mongoFindOne(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId); const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))); allResults.push(queryResult); mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined; queryExecutionLog("info", "mongo-find-one:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, rowCount: result.documents.length, elapsed: elapsed(), }); break; } case "version": { queryExecutionLog("info", "mongo-version:start", { traceId, database: currentDatabase }); const version = await api.mongoServerVersion(executionConnectionId, currentDatabase, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoVersionToQueryResult(version, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-version:done", { traceId, database: currentDatabase, version, elapsed: elapsed(), }); break; } case "showDatabases": { queryExecutionLog("info", "mongo-show-databases:start", { traceId }); const result = await api.mongoRunCommand(executionConnectionId, "admin", '{"listDatabases":1}', executionId); const queryResult = mongoDatabasesToQueryResult(result.documents, performance.now() - commandStartedAt, agentProtocolQueryResultMaxRows(mongoResultMaxRows)); allResults.push(markQueryResultRowsRaw(annotateMongoResult(queryResult))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-show-databases:done", { traceId, databaseCount: queryResult.affected_rows, elapsed: elapsed(), }); break; } case "countDocuments": { queryExecutionLog("info", "mongo-count:start", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const total = await api.mongoCountDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.mode, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCountToQueryResult(total, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-count:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, total, elapsed: elapsed(), }); break; } case "aggregate": { if (options?.mongoSafety) { const safety = evaluateMongoAggregateSafety(mongoCommand, options.mongoSafety); if (!safety.allowed) throw new Error(safety.reason); } queryExecutionLog("info", "mongo-aggregate:start", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize); const result = await api.mongoAggregateDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, mongoCommand.options, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-aggregate:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, rowCount: result.documents.length, total: result.total, elapsed: elapsed(), }); break; } case "distinct": { queryExecutionLog("info", "mongo-distinct:start", { traceId, collection: mongoCommand.collection, database: currentDatabase, field: mongoCommand.field }); const result = await api.mongoDistinct(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.field, mongoCommand.filter, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDistinctToQueryResult(mongoCommand.field, result.documents, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-distinct:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, field: mongoCommand.field, valueCount: result.documents.length, elapsed: elapsed(), }); break; } case "getIndexes": { queryExecutionLog("info", "mongo-indexes:start", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const indexes = await api.listIndexes(executionConnectionId, currentDatabase, "", mongoCommand.collection); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-indexes:done", { traceId, collection: mongoCommand.collection, database: currentDatabase, indexCount: indexes.length, elapsed: elapsed(), }); break; } case "collectionStats": { queryExecutionLog("info", "mongo-collection-stats:start", { traceId, collection: mongoCommand.collection, metric: mongoCommand.metric, database: currentDatabase, }); const stats = await api.mongoCollectionStats(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.scale, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCollectionStatsToQueryResult(mongoCommand.metric, stats as unknown as Record, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-collection-stats:done", { traceId, collection: mongoCommand.collection, metric: mongoCommand.metric, database: currentDatabase, elapsed: elapsed(), }); break; } case "findOneAndUpdate": case "findOneAndReplace": case "findOneAndDelete": { if (options?.mongoSafety) { const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety); if (!safety.allowed) throw new Error(safety.reason); } queryExecutionLog("info", "mongo-find-and-modify:start", { traceId, kind: mongoCommand.kind, collection: mongoCommand.collection, database: currentDatabase, }); const result = mongoCommand.kind === "findOneAndUpdate" ? await api.mongoFindOneAndUpdate(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.options) : mongoCommand.kind === "findOneAndReplace" ? await api.mongoFindOneAndReplace(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options) : await api.mongoFindOneAndDelete(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-find-and-modify:done", { traceId, kind: mongoCommand.kind, collection: mongoCommand.collection, database: currentDatabase, rowCount: result.documents.length, elapsed: elapsed(), }); break; } case "runCommand": { if (options?.mongoSafety) { const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety); if (!safety.allowed) throw new Error(safety.reason); } queryExecutionLog("info", "mongo-run-command:start", { traceId, database: currentDatabase, }); const result = await api.mongoRunCommand(executionConnectionId, currentDatabase, mongoCommand.commandJson, executionId); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)))); mongoEditTarget = undefined; queryExecutionLog("info", "mongo-run-command:done", { traceId, database: currentDatabase, rowCount: result.documents.length, elapsed: elapsed(), }); break; } case "insert": case "update": case "delete": case "createIndex": case "createUser": case "dropIndex": case "dropIndexes": case "dropCollection": { if (options?.mongoSafety) { const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety); if (!safety.allowed) throw new Error(safety.reason); } queryExecutionLog("info", "mongo-write:start", { traceId, database: currentDatabase, kind: mongoCommand.kind, ...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }), }); mongoEditTarget = undefined; if (mongoCommand.kind === "insert") { const result = await api.mongoInsertDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.docsJson); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)))); } else if (mongoCommand.kind === "update") { const result = await api.mongoUpdateDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.many, mongoCommand.options); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)))); } else if (mongoCommand.kind === "createIndex") { const result = await api.mongoCreateIndex(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt)))); } else if (mongoCommand.kind === "createUser") { const result = await api.mongoCreateUser(executionConnectionId, currentDatabase, mongoCommand.userJson, mongoCommand.writeConcernJson); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)))); } else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") { try { const result = await api.mongoDropIndexes(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex"); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt, result.failures)))); } finally { await refreshLoadedMongoIndexesAfterMutation(executionConnectionId, currentDatabase, mongoCommand.collection, traceId); } } else if (mongoCommand.kind === "dropCollection") { await api.mongoDropCollection(executionConnectionId, currentDatabase, mongoCommand.collection); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(1, performance.now() - commandStartedAt)))); } else { const result = await api.mongoDeleteDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)))); } queryExecutionLog("info", "mongo-write:done", { traceId, database: currentDatabase, kind: mongoCommand.kind, ...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }), elapsed: elapsed(), }); break; } case "use": { currentDatabase = mongoCommand.database; allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoUseToQueryResult(currentDatabase, performance.now() - commandStartedAt)))); mongoEditTarget = undefined; queryExecutionLog("info", "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(annotateMongoResult(toErrorResult(error))); mongoEditTarget = undefined; } } queryExecutionLog("info", "mongo:done", { traceId, database: currentDatabase, commandCount: mongoCommands.length, elapsed: elapsed(), }); const current = findExecutionTab(id); if (current?.executionId === executionId) { reconcileBatchSqlResults(current, executionId, allResults); if (captureResultRun && current.isCancelling && restorePendingResultRun(current, executionId)) return false; const activeGroupIndex = current.activeResultIndex; const activeGroupResults = current.results; const findPageState = mongoFindPageState; const shouldAppendResult = !!findPageState && !!options?.appendResult && !!current.result && allResults.length === 1; const shouldReplaceActiveResultInGroup = options?.replaceActiveResultInGroup === true && allResults.length === 1 && Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length; if (shouldAppendResult) { if (findPageState!.pageOffset !== current.result!.rows.length) { throw new Error("Ignoring a stale MongoDB result segment whose offset no longer matches the loaded rows"); } const appendedResult = appendQueryResultSegment(current.result!, allResults[0]!, options!.appendResult!.maxRows); if (Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length) { current.results = activeGroupResults.slice(); current.results[activeGroupIndex] = appendedResult; } current.result = appendedResult; } else if (shouldReplaceActiveResultInGroup) { current.results = activeGroupResults.slice(); current.results[activeGroupIndex] = allResults[0]; current.result = allResults[0]; } else 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) => !isQueryExecutionErrorResult(result)); const resultIndex = preservedResultIndex(allResults, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (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]; } producedResult = current.result !== undefined; touchResult(current); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.resultColumnComments = undefined; current.queryDisplaySourceColumns = undefined; current.queryEditabilityReason = undefined; current.mongoEditTarget = mongoCommands.length === 1 ? mongoEditTarget : undefined; current.tableMeta = undefined; current.resultBaseSql = shouldReplaceActiveResultInGroup ? (current.resultBaseSql ?? options?.resultBaseSql ?? sql) : (options?.resultBaseSql ?? sql); current.resultSortedSql = options?.resultSortedSql; current.resultPageSql = undefined; current.resultPageLimit = mongoFindPageState?.pageLimit; current.resultPageOffset = shouldAppendResult ? (current.resultPageOffset ?? 0) : mongoFindPageState?.pageOffset; current.resultCountSql = undefined; current.resultSessionId = undefined; current.resultClientSessionId = undefined; current.resultTotalRowCount = mongoFindPageState?.totalIsExact ? mongoFindPageState.total : undefined; current.resultTotalRowCountLoading = false; syncDisplayedResultRun(current, current.resultBaseSql ?? options?.resultBaseSql ?? sql, captureResultRun); if (!usesExternalExecutionTarget && current.database !== currentDatabase) current.database = currentDatabase; } return producedResult; } const elasticsearchRequests = elasticsearchRestRequestRanges(sqlToExecute, effectiveDbType); if (elasticsearchRequests.length > 0) { console.info("[DBX][executeTabSql:elasticsearch-rest-batch:start]", { traceId, requestCount: elasticsearchRequests.length, sql, }); const allResults: QueryResult[] = []; const continueOnError = continueOnBatchError; for (const request of elasticsearchRequests) { const current = findExecutionTab(id); if (current?.executionId !== executionId) break; const sourceRange = options?.sourceOffset === undefined ? undefined : { from: options.sourceOffset + request.from, to: options.sourceOffset + request.to }; try { const result = await api.executeQuery(executionConnectionId, executionDatabase, request.sql, undefined, executionId, { timeoutSecs: queryTimeoutSecs, }); allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(result, request.sql, targetDatabase || conn?.database, effectiveDbType, sourceRange))); if (elasticsearchHttpErrorStatus(result) !== undefined && !continueOnError) break; } catch (error) { const latest = findExecutionTab(id); if (latest?.executionId !== executionId) break; allResults.push(annotateQueryResultSource(toErrorResult(error), request.sql, targetDatabase || conn?.database, effectiveDbType, sourceRange)); if (!continueOnError) break; } } console.info("[DBX][executeTabSql:elasticsearch-rest-batch:done]", { traceId, requestCount: elasticsearchRequests.length, resultCount: allResults.length, elapsed: elapsed(), }); const current = findExecutionTab(id); if (current?.executionId === executionId && captureResultRun && current.isCancelling && restorePendingResultRun(current, executionId)) return false; if (current?.executionId === executionId && allResults.length > 0) { clearResultNavigationState(current); const errorResultIndex = allResults.findIndex((result) => isQueryExecutionErrorResult(result) || elasticsearchHttpErrorStatus(result) !== undefined); const resultIndex = errorResultIndex >= 0 ? errorResultIndex : 0; current.results = allResults.length > 1 ? allResults : undefined; current.activeResultIndex = allResults.length > 1 ? resultIndex : undefined; current.result = allResults[resultIndex]; producedResult = current.result !== undefined; touchResult(current); current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.resultColumnComments = undefined; current.queryDisplaySourceColumns = undefined; current.queryEditabilityReason = undefined; current.mongoEditTarget = undefined; current.tableMeta = undefined; current.resultBaseSql = options?.resultBaseSql ?? sql; current.resultSortedSql = undefined; syncDisplayedResultRun(current, current.resultBaseSql, captureResultRun); } return producedResult; } const queryResultMaxRows = tab.mode === "query" ? effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows) : undefined; if (tab.mode === "query") { const prepared = await prepareEditableQueryExecution(tab, sqlToExecute, conn, effectiveDbType, executionDatabase, traceId, elapsed); sqlToExecute = prepared.sql; // Database sorting executes a generated wrapper around the user's query. // Keep editability metadata anchored to the original query so the wrapper // does not turn an otherwise editable result into a complex read-only one. queryMetadataSql = options?.resultSortedSql && !options?.querySort ? queryBaseSql : prepared.metadataSql; hiddenPrimaryKeys = prepared.hiddenPrimaryKeys; useOracleLobPreview = prepared.oracleLobPreview; if (options?.querySort) { const sorted = await api.buildSortedQuerySql({ originalSql: sqlToExecute, databaseType: effectiveDbType, resultColumns: [...options.querySort.resultColumns, ...hiddenPrimaryKeys.map((projection) => projection.alias)], columnIndex: options.querySort.columnIndex, column: options.querySort.column, direction: options.querySort.direction, }); if (!sorted.ok || !sorted.sql) throw new Error("Unable to build sorted query SQL"); sqlToExecute = sorted.sql; resultSortedSql = sorted.sql; } const requestedPagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 }; requestedPageLimit = requestedPagination.limit; const pagination = limitQueryPagination(requestedPagination, queryResultMaxRows); const sqlBeforePagination = sqlToExecute; const sqlServerUseScript = effectiveDbType === "sqlserver" && sqlToExecute === queryBaseSql ? sqlServerLeadingUseScript(sqlToExecute) : undefined; const plan = await api.prepareQueryPaginationExecutionPlan({ sql: sqlServerUseScript?.querySql ?? sqlToExecute, queryBaseSql: sqlServerUseScript?.querySql ?? queryBaseSql, databaseType: effectiveDbType, pagination, useAgentCursor, firstPageUsesActualSql: hiddenPrimaryKeys.length > 0, }); const canPaginateSqlServerUseScript = !!sqlServerUseScript && !!plan.pageSql && typeof plan.pageLimit === "number" && typeof plan.pageOffset === "number"; if (sqlServerUseScript && !canPaginateSqlServerUseScript) { sqlToExecute = sqlBeforePagination; } else { sqlToExecute = sqlServerUseScript ? replaceSqlServerLeadingUseQuery(sqlBeforePagination, sqlServerUseScript, plan.sqlToExecute) : plan.sqlToExecute; pageSql = sqlServerUseScript && plan.pageSql ? replaceSqlServerLeadingUseQuery(sqlBeforePagination, sqlServerUseScript, plan.pageSql) : plan.pageSql; pageLimit = plan.pageLimit; pageOffset = plan.pageOffset; countSql = sqlServerUseScript && plan.countSql ? replaceSqlServerLeadingUseQuery(queryBaseSql, sqlServerUseScript, plan.countSql) : plan.countSql; exactQueryRowBound = plan.exactQueryRowBound; useAgentResultSession = plan.useAgentResultSession; } const hasBoundedPagination = typeof pageLimit === "number" && typeof pageOffset === "number"; if (options?.appendResult && !hasBoundedPagination && !useAgentResultSession) { const current = findExecutionTab(id); if (current?.executionId === executionId && current.result) { current.result.has_more = false; const activeResultIndex = current.activeResultIndex; if (Array.isArray(current.results) && typeof activeResultIndex === "number" && activeResultIndex >= 0 && activeResultIndex < current.results.length) { current.results[activeResultIndex]!.has_more = false; } touchResult(current); syncDisplayedResultRun(current, queryBaseSql, captureResultRun); } queryExecutionLog("info", "append-result:pagination-unsupported", { traceId, elapsed: elapsed() }); return false; } } else if (tab.mode === "data") { const requestedPagination = { limit: options?.pagination?.limit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize), offset: options?.pagination?.offset ?? 0, }; requestedPageLimit = requestedPagination.limit; const pagination = limitQueryPagination(requestedPagination, queryResultMaxRows); pageLimit = pagination.limit; pageOffset = pagination.offset; useAgentResultSession = conn?.db_type === "sqlserver" && conn?.driver_profile?.trim().toLowerCase() === "sqlserver-legacy"; } const executionSchema = connectionQueryExecutionSchema(conn, targetDatabase, targetSchema, tab.mode === "data"); const frontendTimeoutSecs = frontendQueryTimeoutSecsForSql(sqlToExecute, effectiveDbType, queryTimeoutSecs); const sourceLabelDatabase = targetDatabase || conn?.database; const executionClientSessionId = options?.pagination?.clientSessionId ?? (tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined); const currentBeforeDispatch = findExecutionTab(id); if (currentBeforeDispatch?.executionId !== executionId || currentBeforeDispatch.isCancelling) { queryExecutionLog("info", "dispatch:skipped-cancelled", { traceId, elapsed: elapsed() }); return false; } let executionPromise: Promise; if (tab.autoCommit === false) { if (!tab.txnSessionId) { queryExecutionLog("info", "begin-manual-txn:start", { traceId, elapsed: elapsed() }); tab.txnSessionId = await api.beginManualTransaction(executionConnectionId, executionDatabase, executionSchema, executionCatalog); queryExecutionLog("info", "begin-manual-txn:done", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() }); } queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() }); executionDispatched = true; // Only an initial manual execution classifies the user SQL (Oracle-only). // A later cursor-page fetch must neither set nor clear the sticky bit. const isInitialOracleManualExecution = effectiveDbType === "oracle" && !options?.pagination?.sessionId; const classificationSql = isInitialOracleManualExecution ? queryBaseSql : undefined; let manualTransactionRecoveryAttempted = false; executionPromise = (async () => { const txnSessionId = tab.txnSessionId; if (!txnSessionId) throw new Error("Manual transaction session was not initialized"); const executeInTransaction = (sessionId: string) => useAgentResultSession ? api.executeInManualTransaction(sessionId, sqlToExecute, executionDatabase, executionSchema, agentProtocolQueryResultMaxRows(queryResultMaxRows), useOracleLobPreview, pageLimit, options?.pagination?.sessionId, classificationSql) : api.executeInManualTransaction(sessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit ?? agentProtocolQueryResultMaxRows(queryResultMaxRows), useOracleLobPreview, undefined, undefined, classificationSql); try { return await executeInTransaction(txnSessionId); } catch (error) { if (options?.pagination?.sessionId || manualTransactionRecoveryAttempted || !isManualTransactionSessionExpired(error)) throw error; manualTransactionRecoveryAttempted = true; // The expired session was discarded by the backend; the replacement // session starts fresh, so the old sticky state resets with it. clearOracleTxnPossiblyDirty(tab); tab.txnSessionId = undefined; tab.txnAutoRolledBack = true; queryExecutionLog("info", "manual-txn:expired-recover", { traceId, elapsed: elapsed() }); const refreshedSessionId = await api.beginManualTransaction(executionConnectionId, executionDatabase, executionSchema, executionCatalog); tab.txnSessionId = refreshedSessionId; queryExecutionLog("info", "manual-txn:restarted", { traceId, txnSessionId: refreshedSessionId, elapsed: elapsed() }); return executeInTransaction(refreshedSessionId); } })(); } else { queryExecutionLog("info", "execute-multi:start", { traceId, elapsed: elapsed() }); // Query and data tabs use a tab-scoped pool so repeated executions keep // connection-local state and avoid MySQL pool resets on every refresh. const dataTabMeta = tab.mode === "data" ? tableMetaForDataTab(tab) : undefined; const useTableDataPreview = canUseTableDataLargeValuePreview(effectiveDbType, dataTabMeta?.columns ?? [], dataTabMeta?.primaryKeys ?? []); const useJdbcDriverRowOffset = tab.mode === "data" && conn?.db_type === "jdbc" && effectiveDbType === "iris"; const executionOptions = { ...(typeof pageLimit === "number" ? useAgentResultSession ? { // Agent cursors apply maxRows cumulatively across fetched pages. maxRows: agentProtocolQueryResultMaxRows(queryResultMaxRows), fetchSize: pageLimit, pageSize: pageLimit, resultSessionId: options?.pagination?.sessionId, } : { maxRows: pageLimit, fetchSize: pageLimit } : { maxRows: agentProtocolQueryResultMaxRows(queryResultMaxRows) }), ...(useJdbcDriverRowOffset && typeof pageOffset === "number" && pageOffset > 0 ? { rowOffset: pageOffset } : {}), ...(executionClientSessionId ? { clientSessionId: executionClientSessionId } : {}), ...(tab.mode === "data" && (effectiveDbType === "mysql" || effectiveDbType === "postgres") ? { maxResultBytes: TABLE_DATA_RESULT_MAX_BYTES, resultKeyColumns: dataTabMeta?.primaryKeys ?? [], tableDataPreview: useTableDataPreview, } : {}), ...(useOracleLobPreview ? { tableDataPreview: true } : {}), timeoutSecs: queryTimeoutSecs, catalog: executionCatalog, continueOnError: continueOnBatchError, }; queryExecutionLog("info", "execute-multi:invoke", { traceId, elapsed: elapsed(), executionSchema, optionKeys: Object.keys(executionOptions), clientSession: Boolean(executionClientSessionId), }); executionDispatched = true; executionPromise = tab.batchSqlExecution && tab.batchSqlExecution.total > 1 ? api.executeMultiWithProgress( executionConnectionId, executionDatabase, sqlToExecute, (progress) => { const current = findExecutionTab(id); if (current?.executionId === executionId) { applyBatchSqlProgress(current, progress, continueOnBatchError, batchResume?.startStatementIndex ?? 0); } }, executionSchema, { ...executionOptions, executionId }, ) : api.executeMulti(executionConnectionId, executionDatabase, sqlToExecute, executionSchema, executionId, executionOptions); } const annotatedResults = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset); const results = offsetBatchQueryResultIndexes(annotatedResults.results, batchResume?.startStatementIndex ?? 0); reconcileBatchSqlResults(tab, executionId, results); // Oracle-only sticky state aggregation. Only the initial manual execution // participates: a later cursor-page fetch (pagination.sessionId present) // must neither set nor clear the bit, and the Core no-op (empty script) // must neither set nor clear it. Otherwise any result that is not proven // read-only dirties the session monotonically. if (tab.autoCommit === false && effectiveDbType === "oracle" && !options?.pagination?.sessionId && tab.txnSessionId) { const rawResults = annotatedResults.results; const isCoreNoOp = rawResults.length > 0 && rawResults.every((result) => result.manual_transaction_no_statement === true); if (!isCoreNoOp && rawResults.some((result) => result.manual_transaction_proven_read_only !== true)) { tab.oracleTxnPossiblyDirty = true; } } const successfulOracleSchemaChanges = effectiveDbType === "oracle" ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0; const successfulSapHanaSchemaChanges = effectiveDbType === "saphana" ? results.filter((result) => result.execution_error !== true && isSapHanaSetSchemaStatement(result.sourceStatement)).length : 0; const sqlServerUseDatabase = effectiveDbType === "sqlserver" ? annotatedResults.sqlServerUseDatabase : undefined; if (hiddenPrimaryKeys.length > 0 && results.length === 1) { const hiddenIndexes = hiddenResultColumnIndexes(results[0]!.columns, hiddenPrimaryKeys); if (hiddenIndexes.length > 0) results[0]!.hidden_column_indexes = hiddenIndexes; if (hiddenIndexes.length !== hiddenPrimaryKeys.length) queryMetadataSql = queryBaseSql; } else if (hiddenPrimaryKeys.length > 0) { queryMetadataSql = queryBaseSql; } queryExecutionLog("info", "execute-multi:done", { traceId, resultCount: results.length, rowCounts: results.map((result) => result.rows.length), columnCounts: results.map((result) => result.columns.length), elapsed: elapsed(), }); let resolvedSapHanaSchema: string | undefined; if (successfulSapHanaSchemaChanges > 0 && findExecutionTab(id)?.executionId === executionId) { try { const schemaResult = await api.executeQuery(executionConnectionId, executionDatabase, "SELECT CURRENT_SCHEMA FROM DUMMY", undefined, executionId, { clientSessionId: tabClientSessionId(tab), timeoutSecs: queryTimeoutSecs, }); resolvedSapHanaSchema = sapHanaCurrentSchemaFromResult(schemaResult); } catch (error) { console.warn("[DBX] Failed to resolve SAP HANA CURRENT_SCHEMA", error); } } const current = findExecutionTab(id); if (current?.executionId === executionId) { if (captureResultRun && current.isCancelling && restorePendingResultRun(current, executionId)) return false; if (successfulOracleSchemaChanges > 0) { current.completionContextVersion = (current.completionContextVersion ?? 0) + successfulOracleSchemaChanges; } if (resolvedSapHanaSchema && !usesExternalExecutionTarget) { current.schema = resolvedSapHanaSchema; current.completionContextVersion = (current.completionContextVersion ?? 0) + successfulSapHanaSchemaChanges; } if (sqlServerUseDatabase && !usesExternalExecutionTarget && current.database !== sqlServerUseDatabase) { rollbackTabTransaction(current); void closeClientConnectionSession(current); current.database = sqlServerUseDatabase; current.schema = undefined; } const activeGroupIndex = current.activeResultIndex; const activeGroupResults = current.results; const shouldAppendResult = !!options?.appendResult && !!current.result; const shouldReplaceActiveResultInGroup = options?.replaceActiveResultInGroup === true && results.length === 1 && Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length; const retainedPaginationResult = options?.retainDisplayedResult === true && results.length === 1 && !isQueryExecutionErrorResult(results[0]!) ? results[0] : undefined; if (retainedPaginationResult) { // Cursor-only fetches advance the backend session without publishing // intermediate rows or page offsets to the visible result grid. current.resultSessionId = retainedPaginationResult.session_id ?? undefined; current.resultClientSessionId = current.resultSessionId ? executionClientSessionId : undefined; producedResult = true; return producedResult; } if (batchResume) { const mergedResults = mergeBatchQueryResults(batchResume.previousResults, results); const preferredResult = results.find((result) => isQueryExecutionErrorResult(result)) ?? results[results.length - 1] ?? mergedResults[mergedResults.length - 1]; const resultIndex = preferredResult ? mergedResults.indexOf(preferredResult) : 0; current.results = mergedResults.length > 1 ? mergedResults : undefined; current.activeResultIndex = mergedResults.length > 1 ? Math.max(0, resultIndex) : undefined; current.result = mergedResults[Math.max(0, resultIndex)]; } else if (shouldAppendResult) { if (results.length !== 1) throw new Error("Expected one result while loading the next segment"); if (options.pagination?.offset !== current.result!.rows.length) { throw new Error("Ignoring a stale result segment whose offset no longer matches the loaded rows"); } const appendedResult = appendQueryResultSegment(current.result!, results[0]!, options.appendResult!.maxRows); if (Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length) { current.results = activeGroupResults.slice(); current.results[activeGroupIndex] = appendedResult; } current.result = appendedResult; } else if (shouldReplaceActiveResultInGroup) { current.results = activeGroupResults.slice(); current.results[activeGroupIndex] = results[0]; current.result = results[0]; } else if (results.length > 1) { const errorResultIndex = results.findIndex((result) => isQueryExecutionErrorResult(result)); const activeResultIndex = results.findIndex((result) => result.columns.length > 0); const resultIndex = errorResultIndex >= 0 ? errorResultIndex : (preservedResultIndex(results, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0)); current.results = results; current.activeResultIndex = resultIndex; current.result = results[resultIndex]; } else { current.results = undefined; current.activeResultIndex = undefined; current.result = results[0]; } producedResult = current.result !== undefined; current.resultBaseSql = batchResume ? batchResume.batch.submittedSql : shouldReplaceActiveResultInGroup ? (current.resultBaseSql ?? queryBaseSql) : queryBaseSql; current.resultEditorFingerprint = batchResume ? batchResume.batch.editorFingerprint : shouldReplaceActiveResultInGroup ? (current.resultEditorFingerprint ?? executionEditorFingerprint) : executionEditorFingerprint; current.resultSortedSql = resultSortedSql; // Appended rows form one logical result starting at the original page. // Keep the base page state so later table refresh/cache recovery does // not re-execute only the most recently fetched tail segment. current.resultPageSql = shouldAppendResult ? (current.resultPageSql ?? pageSql) : pageSql; const displayPageLimit = typeof pageLimit === "number" ? (requestedPageLimit ?? pageLimit) : undefined; current.resultPageLimit = shouldAppendResult ? (current.resultPageLimit ?? displayPageLimit) : displayPageLimit; current.resultPageOffset = shouldAppendResult ? (current.resultPageOffset ?? 0) : pageOffset; current.resultCountSql = countSql; current.resultSessionId = current.result?.session_id ?? undefined; current.resultClientSessionId = current.resultSessionId ? executionClientSessionId : undefined; if (!options?.preserveTotalRowCountDuringExecution) { current.resultTotalRowCount = undefined; } const resultRowCount = current.result?.rows.length ?? 0; const resultLimitReached = !!current.result && queryResultLimitReached(pageOffset, resultRowCount, queryResultMaxRows); if (resultLimitReached && current.result) { current.result.has_more = false; current.result.truncated = true; current.resultTotalRowCount = queryResultMaxRows; } const paginationPageResult = shouldAppendResult ? results[0] : current.result; const exactIncompletePageTotal = paginationPageResult ? exactTotalFromIncompletePage(paginationPageResult, pageLimit, pageOffset, useAgentResultSession) : undefined; const totalKnownFromIncompletePage = typeof exactIncompletePageTotal === "number"; let totalRowCountResolved = false; if (current.mode === "query" && current.result && !isQueryExecutionErrorResult(current.result) && typeof exactQueryRowBound === "number") { const boundedTotal = capQueryResultTotal(exactQueryRowBound, queryResultMaxRows); current.resultTotalRowCount = Math.min(boundedTotal, exactIncompletePageTotal ?? boundedTotal); current.resultTotalRowCountLoading = false; totalRowCountResolved = true; } const dataCountTarget = current.mode === "data" ? (() => { const tableMeta = tableMetaForDataTab(current); if (!tableMeta?.tableName) return undefined; return { databaseType: effectiveDbType, identifierQuote: useConnectionStore().connectionIdentifierQuote?.(current.connectionId), catalog: tableMeta.catalog, database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, whereInput: current.whereInput?.trim() || undefined, countHint: effectiveDbType === "gaussdb" ? gaussdbCountQueryDopHint(useConnectionStore().getConfig(current.connectionId)) : undefined, }; })() : undefined; const canAutoCalculateTotalRows = !options?.appendResult && !!current.result && resultRowCount > 0 && !resultLimitReached && !totalKnownFromIncompletePage && !totalRowCountResolved && settingsStore.editorSettings.autoCalculateTotalRows && ((current.mode === "query" && !!countSql) || (current.mode === "data" && !!dataCountTarget)); current.resultTotalRowCountLoading = canAutoCalculateTotalRows; // Server-side pagination without a countSql: the backend (currently // the Elasticsearch driver) already reports the true match total via // affected_rows. Use it directly so the result-grid can compute the // page count without issuing a separate COUNT query. if (!totalRowCountResolved && current.result && current.result.total_is_exact !== false && current.mode === "query" && typeof pageLimit === "number" && !countSql && typeof current.result.affected_rows === "number" && current.result.affected_rows > current.result.rows.length) { current.resultTotalRowCount = current.result.affected_rows; current.resultTotalRowCountLoading = false; totalRowCountResolved = true; } touchResult(current); syncDisplayedResultRun(current, queryBaseSql, captureResultRun); if (!options?.appendResult && !resultLimitReached && !totalRowCountResolved && (current.mode === "query" || current.mode === "data") && current.result) { countQueryTotalRowsInBackground({ tabId: id, connectionId: executionConnectionId, database: executionDatabase, schema: executionTarget?.schema ?? current.schema, catalog: executionCatalog, countSql, countSqlTarget: dataCountTarget ? async () => ({ sql: await api.buildDataGridCountSql(dataCountTarget), schema: undefined, }) : undefined, result: current.result, pageLimit, pageOffset, useAgentResultSession, executionId, traceId, elapsed, timeoutSecs: queryTimeoutSecs, }); } queryExecutionLog("info", "result:assigned", { traceId, activeResultIndex: current.activeResultIndex, rowCount: current.result?.rows.length ?? 0, columnCount: current.result?.columns.length ?? 0, backendMs: current.result?.execution_time_ms, elapsed: elapsed(), }); if (current.mode === "query" && current.result) { analyzeQueryMetadataInBackground(id, displayedQueryMetadataSql(current, queryMetadataSql), current.result, executionDatabase, traceId, elapsed, effectiveDbType, hiddenPrimaryKeys); } } else { queryExecutionLog("warn", "stale-result", { traceId, currentExecutionId: current?.executionId, elapsed: elapsed(), }); } } catch (e: any) { queryExecutionLog("error", "error", { traceId, elapsed: elapsed(), error: e }); // Sync connection state if the error indicates a lost connection useConnectionStore().recordConnectionLostError(executionConnectionId ?? tab.connectionId, e); // Handle manual transaction auto-rollback (idle timeout only for the banner; // other statement failures still clear the session without the 5-minute notice). if (tab.autoCommit === false) { const errMsg: string = e?.message ?? String(e); const idleTimeout = /5 minutes of inactivity/i.test(errMsg) || errMsg.includes("5 分钟无操作") || errMsg.includes("已自动回滚"); if (idleTimeout) { // Backend session was removed and rolled back after idle expiry: clear // the sticky dirty state together with the session. clearOracleTxnPossiblyDirty(tab); tab.txnSessionId = undefined; tab.txnAutoRolledBack = true; } else if (/rolled.?back/i.test(errMsg) || /transaction session not found/i.test(errMsg) || /agent runtime terminated/i.test(errMsg)) { // Statement failure that disposed the manual session: the `rolled back` // message fragment is a frontend cleanup compatibility contract. clearOracleTxnPossiblyDirty(tab); tab.txnSessionId = undefined; tab.txnAutoRolledBack = false; } else if (tab.txnSessionId && executionDispatched && !options?.pagination?.sessionId && effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId)) === "oracle") { // Frontend timeout/cancel or mid-script failure: the statement may still // have executed server-side while the manual session survives, so keep // the sticky dirty state fail-closed instead of a clean toolbar on a // dirty session. Cursor-page fetches stay excluded like the aggregation. tab.oracleTxnPossiblyDirty = true; } } const current = findExecutionTab(id); if (current?.executionId === executionId) { failBatchSqlExecution(current, executionId, e, current.isCancelling === true); const restoredRetainedResult = captureResultRun && (current.isCancelling || !executionDispatched) && restorePendingResultRun(current, executionId); if (restoredRetainedResult) { queryExecutionLog("info", "retained-result:restored-after-abort", { traceId, elapsed: elapsed() }); return false; } else if (options?.appendResult && current.result) { // A failed background segment must not replace the visible result or // silently invalidate pending edits. The next explicit refresh can retry. queryExecutionLog("warn", "append-result:preserved-after-error", { traceId, elapsed: elapsed() }); return false; } const errorResult = toErrorResult(e); const activeGroupIndex = current.activeResultIndex; const activeGroupResults = current.results; const shouldReplaceActiveResultInGroup = options?.replaceActiveResultInGroup === true && Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length; if (shouldReplaceActiveResultInGroup) { current.results = activeGroupResults.slice(); current.results[activeGroupIndex] = errorResult; current.result = errorResult; } else { current.result = errorResult; current.results = undefined; current.activeResultIndex = undefined; } current.queryAnalysis = undefined; current.querySourceColumns = undefined; current.resultColumnComments = undefined; current.queryDisplaySourceColumns = undefined; current.queryEditabilityReason = undefined; current.mongoEditTarget = undefined; if (current.mode !== "data") current.tableMeta = undefined; current.resultBaseSql = shouldReplaceActiveResultInGroup ? (current.resultBaseSql ?? queryBaseSql) : queryBaseSql; current.resultSortedSql = resultSortedSql; current.resultPageSql = pageSql; current.resultPageLimit = typeof pageLimit === "number" ? (requestedPageLimit ?? pageLimit) : undefined; current.resultPageOffset = pageOffset; current.resultCountSql = countSql; current.resultSessionId = undefined; current.resultClientSessionId = undefined; current.resultTotalRowCount = undefined; current.resultTotalRowCountLoading = false; touchResult(current); producedResult = true; // When a pinned result requires a new run, errors must use that same // run instead of being replaced by the retained pinned result below. syncDisplayedResultRun(current, queryBaseSql, captureResultRun); } } finally { if (tableDataNativeSelectionBlockOwner) finishDataGridNativeSelectionBlock(tableDataNativeSelectionBlockOwner); const current = findExecutionTab(id); if (current?.executionId === executionId) { const liveBatch = liveBatchSqlExecutions.get(current); if (liveBatch?.executionId === executionId) current.batchSqlExecution = liveBatch; finishBatchSqlExecution(current, executionId, current.isCancelling === true); if (current.activeResultRunId && current.result) syncActiveResultRunFromDisplayed(current); if (captureResultRun && !current.activeResultRunId) { restorePendingResultRun(current, executionId); } else { pendingResultRunRestores.delete(executionId); } current.isExecuting = false; current.isCancelling = false; current.queryExecutionStartedAt = undefined; current.executionId = undefined; clearLiveBatchSqlExecution(current, executionId); queryExecutionLog("info", "finish", { traceId, elapsed: elapsed() }); } else { pendingResultRunRestores.delete(executionId); if (current) clearLiveBatchSqlExecution(current, executionId); queryExecutionLog("warn", "finish-stale", { traceId, currentExecutionId: current?.executionId, elapsed: elapsed(), }); } } scheduleResultCacheTrim(); return producedResult; } function dismissBatchSqlRecovery(id: string) { const tab = findExecutionTab(id); if (!tab?.batchSqlExecution || !batchSqlRecoveryState(tab)) return false; tab.batchSqlExecution.recoveryDismissed = true; return true; } async function resumeBatchSql(id: string, action: BatchSqlRecoveryAction) { const tab = findExecutionTab(id); const recovery = tab ? batchSqlRecoveryState(tab) : undefined; const batch = tab?.batchSqlExecution; if (!tab || !batch || !recovery) return false; const startStatementIndex = action === "retry" ? recovery.failedStatementIndex : recovery.failedStatementIndex + 1; const resumed = batchSqlRecoverySql(batch, startStatementIndex); if (!resumed) return false; const previousResults = tab.results?.slice() ?? (tab.result ? [tab.result] : []); return await executeTabSql(id, resumed.sql, { sourceOffset: resumed.sourceOffset, preserveResultDuringExecution: true, batchResume: { batch: cloneBatchSqlExecution(batch)!, previousResults, startStatementIndex, continueOnError: action === "skip-all", }, }); } async function explainTabSql(id: string, sql: string, databaseType?: DatabaseType, explainMode?: string) { 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 = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const executionId = uuid(); tab.isExplaining = true; tab.explainExecutionId = executionId; tab.explainPlan = undefined; tab.explainTableResult = undefined; tab.explainError = undefined; tab.explainTableError = undefined; tab.explainSql = undefined; tab.explainTableSql = undefined; tab.lastExplainedSql = sql; try { await waitForTabSessionReset(id); } catch (e: any) { // Do not start an explain with a session whose schema reset did not complete. tab.isExplaining = false; tab.explainExecutionId = undefined; tab.explainError = String(e?.message || e); return { ok: false as const, reason: tab.explainError }; } // DM and Oracle agents expose native text plans. DM also supports autotrace. if (databaseType === "dameng" || databaseType === "oracle") { let explainSql = sql; if (databaseType === "oracle") { const built = await buildExplainSql(databaseType, sql); if (!built.ok) { tab.isExplaining = false; tab.explainExecutionId = undefined; tab.explainPlan = undefined; tab.explainError = built.reason; return built; } explainSql = built.sql; } // Autotrace executes the SQL, so keep its stricter safety check. if (databaseType === "dameng" && explainMode === "autotrace") { const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; const cleaned = sql .replace(/\/\*[\s\S]*?\*\//g, " ") .replace(/--.*$/gm, " ") .replace(/#.*$/gm, " "); if (cleaned.split(";").some((stmt) => DANGER_RE.test(stmt))) { tab.isExplaining = false; tab.explainExecutionId = undefined; return { ok: false as const, reason: "unsafe" as const }; } } try { const mode = databaseType === "dameng" && explainMode === "autotrace" ? "autotrace" : "explain"; const planText = (await api.getExplainInfo(tab.connectionId, tab.database, tab.schema, sql, mode)) as string | undefined; const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { if (planText && planText.length > 0) { current.explainPlan = databaseType === "oracle" ? parseOracleExplainText(planText) : parseDamengExplainText(planText); current.explainSql = explainSql; current.explainError = undefined; } else { current.explainPlan = undefined; current.explainError = "No explain plan returned"; } } } catch (e: any) { const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.explainPlan = undefined; // Backend rejections contain the real ORA/Agent diagnostic; only successful empty responses use the generic empty-plan message. current.explainError = formatError(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: explainSql }; } if (databaseType === "mysql") { let tableBuilt: BuildExplainSqlResult; let jsonBuilt: BuildExplainSqlResult; try { [tableBuilt, jsonBuilt] = await Promise.all([buildExplainSql(databaseType, sql, "standard"), buildExplainSql(databaseType, sql, "json")]); } catch (e: any) { const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.isExplaining = false; current.explainExecutionId = undefined; current.explainError = String(e?.message || e); } return { ok: true as const, sql: "" }; } if (tabs.value.find((t) => t.id === id)?.explainExecutionId !== executionId) { return { ok: true as const, sql: jsonBuilt.ok ? jsonBuilt.sql : "" }; } if (!tableBuilt.ok || !jsonBuilt.ok) { const failed = !tableBuilt.ok ? tableBuilt : jsonBuilt; const reason = !tableBuilt.ok ? tableBuilt.reason : !jsonBuilt.ok ? jsonBuilt.reason : "unsupported"; const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.isExplaining = false; current.explainExecutionId = undefined; current.explainError = reason; } return failed; } let tableSql = tableBuilt.sql; let jsonSupportedByServer: boolean | undefined; tab.explainTableSql = tableSql; tab.explainSql = undefined; // Keep the two EXPLAIN statements on the same one-connection MySQL session. const clientSessionId = `${tabClientSessionId(tab, "explain")}:${executionId}`; tab.explainClientSessionId = clientSessionId; try { let tableResult: QueryResult | undefined; let tableError: unknown; try { tableResult = await api.executeQuery(tab.connectionId, tab.database, tableSql, tab.schema, executionId, { clientSessionId, catalog: tab.catalog, timeoutSecs: queryTimeoutSecs, }); } catch (error: unknown) { const compatibility = mysqlExplainCompatibilityHint(error, tableSql); jsonSupportedByServer = compatibility?.supportsJson; if (compatibility?.fallbackSql && tabs.value.find((t) => t.id === id)?.explainExecutionId === executionId) { tableSql = compatibility.fallbackSql; tab.explainTableSql = tableSql; try { tableResult = await api.executeQuery(tab.connectionId, tab.database, tableSql, tab.schema, executionId, { clientSessionId, catalog: tab.catalog, timeoutSecs: queryTimeoutSecs, }); } catch (fallbackError: unknown) { tableError = fallbackError; } } else { tableError = error; } } const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { if (tableResult) { current.explainTableResult = markQueryResultRowsRaw(tableResult); current.explainTableError = undefined; } else if (tableError !== undefined) { current.explainTableResult = undefined; current.explainTableError = formatError(tableError); } } // A canceled or superseded standard request must not start a fallback or JSON request. if (tabs.value.find((t) => t.id === id)?.explainExecutionId !== executionId) { return { ok: true as const, sql: tableSql }; } // ADB MySQL advertises its accepted formats in the first error; avoid a second known-invalid request. if (jsonSupportedByServer === false) { const latest = tabs.value.find((t) => t.id === id); if (latest?.explainExecutionId === executionId) { latest.explainPlan = undefined; latest.explainError = latest.explainTableResult ? undefined : latest.explainTableError; } return { ok: true as const, sql: tableSql }; } try { const latest = tabs.value.find((t) => t.id === id); if (latest?.explainExecutionId === executionId) latest.explainSql = jsonBuilt.sql; const jsonResult = await api.executeQuery(tab.connectionId, tab.database, jsonBuilt.sql, tab.schema, executionId, { clientSessionId, catalog: tab.catalog, timeoutSecs: queryTimeoutSecs, }); const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.explainPlan = parseExplainResult("mysql", jsonResult); current.explainError = undefined; } } catch (e: any) { const latest = tabs.value.find((t) => t.id === id); if (latest?.explainExecutionId === executionId) { latest.explainPlan = undefined; // Keep a usable tabular plan visible when the server explicitly rejects JSON. const compatibility = mysqlExplainCompatibilityHint(e, jsonBuilt.sql); latest.explainError = compatibility?.supportsJson === false && latest.explainTableResult ? undefined : formatError(e); } } } finally { const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.isExplaining = false; current.explainExecutionId = undefined; } if (current?.explainClientSessionId === clientSessionId) current.explainClientSessionId = undefined; void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id, explainExecutionId: executionId }); } return { ok: true as const, sql: tab.explainSql ?? tableSql }; } if (databaseType === "sqlserver") { // SQL Server reuses the autotrace toggle to ask for the actual execution plan: // STATISTICS XML runs the statement and adds runtime counters to the same // ShowPlanXML document, while SHOWPLAN_XML only estimates. const actualPlan = explainMode === "autotrace"; const planCaptureOn = actualPlan ? "SET STATISTICS XML ON;" : "SET SHOWPLAN_XML ON;"; const planCaptureOff = actualPlan ? "SET STATISTICS XML OFF;" : "SET SHOWPLAN_XML OFF;"; let built: BuildExplainSqlResult; try { built = actualPlan ? await buildExplainSql(databaseType, sql, "json", true) : await buildExplainSql(databaseType, sql); } catch (e: any) { tab.isExplaining = false; tab.explainExecutionId = undefined; tab.explainError = String(e?.message || e); return { ok: true as const, sql: "" }; } if (!built.ok) { tab.isExplaining = false; tab.explainExecutionId = undefined; tab.explainError = built.reason; return built; } tab.explainSql = built.sql; const clientSessionId = `${tabClientSessionId(tab, "explain")}:${executionId}`; tab.explainClientSessionId = clientSessionId; let planCaptureEnabled = false; try { await api.executeQuery(tab.connectionId, tab.database, planCaptureOn, tab.schema, executionId, { clientSessionId, timeoutSecs: queryTimeoutSecs, executionMode: "simple", }); planCaptureEnabled = true; if (tabs.value.find((t) => t.id === id)?.explainExecutionId !== executionId) { return { ok: true as const, sql: built.sql }; } const results = await api.executeMulti(tab.connectionId, tab.database, sql, tab.schema, executionId, { clientSessionId, timeoutSecs: queryTimeoutSecs, executionMode: "simple", }); const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { const outcome = sqlServerExplainResult(results); if (outcome.error !== undefined) { current.explainPlan = undefined; current.explainError = outcome.error; } else if (outcome.result) { current.explainPlan = parseExplainResult("sqlserver", outcome.result); current.explainError = undefined; } else { current.explainPlan = undefined; current.explainError = t("explain.empty"); } } } 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 { if (planCaptureEnabled) { try { await api.executeQuery(tab.connectionId, tab.database, planCaptureOff, tab.schema, undefined, { clientSessionId, timeoutSecs: queryTimeoutSecs > 0 ? Math.min(queryTimeoutSecs, 5) : 5, executionMode: "simple", }); } catch (error) { console.warn("[DBX][sqlserver-explain:cleanup:error]", { tabId: tab.id, error }); } } const current = tabs.value.find((t) => t.id === id); if (current?.explainExecutionId === executionId) { current.isExplaining = false; current.explainExecutionId = undefined; } if (current?.explainClientSessionId === clientSessionId) current.explainClientSessionId = undefined; await closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id, explainExecutionId: executionId }); } return { ok: true as const, sql: built.sql }; } const postgresAnalyze = databaseType === "postgres" && explainMode === "autotrace"; const built = postgresAnalyze ? await buildExplainSql(databaseType, sql, "json", true) : await buildExplainSql(databaseType, sql); if (!built.ok) { tab.explainPlan = undefined; tab.explainError = built.reason; tab.isExplaining = false; tab.explainExecutionId = undefined; return built; } tab.explainSql = built.sql; const clientSessionId = postgresAnalyze ? `${tabClientSessionId(tab, "explain")}:${executionId}` : tabClientSessionId(tab, "explain"); if (postgresAnalyze) tab.explainClientSessionId = clientSessionId; try { const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, tab.schema, executionId, { clientSessionId, catalog: tab.catalog, timeoutSecs: queryTimeoutSecs, executionMode: postgresAnalyze ? "postgres_read_only_transaction" : undefined, }); 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; } if (current?.explainClientSessionId === clientSessionId) current.explainClientSessionId = undefined; const closePromise = closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id, explainExecutionId: executionId }); if (postgresAnalyze) await closePromise; else void closePromise; } return { ok: true as const, sql: built.sql }; } async function cancelTabExecution(id: string) { const tab = findExecutionTab(id); if (!tab || !canCancelQueryExecution(tab)) return false; const executionId = tab.executionId; if (!executionId) return false; tab.isCancelling = true; // 单调递增、不随取消结果回退:导航流程据此判断"执行期间用户请求过停止" // (isCancelling 在取消失败或查询先完成时会被清掉,无法承担这个语义) tab.cancelRequestCount = (tab.cancelRequestCount ?? 0) + 1; const cancellationStartedAt = performance.now(); try { const canceled = await withCancelQueryTimeout(api.cancelQuery(executionId)); if (canceled) { clearAcknowledgedCancelIfStillRunning(id, executionId); } if (!canceled) { const current = findExecutionTab(id); if (current && current.executionId === executionId) { finishBatchSqlExecution(current, executionId, false); restorePendingResultRun(current, executionId); current.isExecuting = false; current.isCancelling = false; current.executionId = undefined; current.queryExecutionStartedAt = undefined; clearLiveBatchSqlExecution(current, executionId); } } return canceled; } catch (e: any) { // Sync connection state if the error indicates a lost connection if (tab) useConnectionStore().recordConnectionLostError(tab.connectionId, e); const current = findExecutionTab(id); if (current && current.executionId === executionId) { failBatchSqlExecution(current, executionId, e, false); finishBatchSqlExecution(current, executionId, false); if (restorePendingResultRun(current, executionId)) { current.isExecuting = false; current.isCancelling = false; current.queryExecutionStartedAt = undefined; current.executionId = undefined; } else { // 复用 setErrorResult 的完整清理:分组结果不清空的话,错误结果不会展示, // 估算值也会继续按旧的 results 计算 setErrorResult(id, e); } clearLiveBatchSqlExecution(current, executionId); } return false; } finally { recordQueryCancellationLatency(performance.now() - cancellationStartedAt); } } 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; // Invalidate locally before the remote cancellation call so no later stage can start. tab.isExplaining = false; tab.explainExecutionId = undefined; try { return await api.cancelQuery(executionId); } catch { return false; } } function setActiveResultIndex(id: string, index: number) { const tab = findExecutionTab(id); if (!tab?.results || index < 0 || index >= tab.results.length) return; tab.activeResultIndex = index; tab.result = tab.results[index]; tab.resultLocalSortOriginalRows = undefined; tab.resultLocalSortOriginalLargeValueCells = undefined; tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultLocalSortOriginalMongoCopyDocuments = undefined; tab.resultSortColumn = undefined; tab.resultSortColumnIndex = undefined; tab.resultSortDirection = undefined; tab.resultSortMode = undefined; tab.resultSortedSql = undefined; tab.resultTotalRowCount = undefined; tab.resultTotalRowCountLoading = false; // results 数组未变,估算值与当前激活的 result 无关,可直接复用 touchResult(tab, Date.now(), { reuseEstimatedBytes: true }); tab.queryAnalysis = undefined; tab.querySourceColumns = undefined; tab.resultColumnComments = undefined; tab.queryDisplaySourceColumns = undefined; tab.queryEditabilityReason = undefined; tab.mongoEditTarget = undefined; syncActiveResultRunFromDisplayed(tab); const sourceStatement = tab.result?.sourceStatement; if (tab.mode === "query" && sourceStatement && splitMongoCommandRanges(sourceStatement).length === 0) { const metadataStartedAt = performance.now(); const connection = useConnectionStore().getConfig(tab.connectionId); const executionDatabase = dataTabExecutionDatabase(connection, tab.database, tab.catalog); analyzeQueryMetadataInBackground(id, sourceStatement, tab.result, executionDatabase, uuid().slice(0, 8), () => `${Math.round(performance.now() - metadataStartedAt)}ms`, effectiveDatabaseTypeForConnection(connection)); } } function notifyConnectionMayBeLost() { const stuck = tabs.value.filter((t) => t.isExecuting); if (stuck.length > 0) { const connStore = useConnectionStore(); stuck.forEach((tab) => { const error = new Error(t("editor.connectionMayBeLost")); setErrorResult(tab.id, error); connStore.markConnectionLost(tab.connectionId, error); }); } } async function trimResultCache() { const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results)); const evictionIds = new Set( selectInactiveResultEvictions( inactive.map((tab) => ({ id: tab.id, estimatedBytes: tab.resultEstimatedBytes ?? estimateQueryResultsBytes(tab.result, tab.results), accessedAt: tab.resultAccessedAt ?? 0, })), MAX_CACHED_RESULT_BYTES, MAX_CACHED_RESULTS, ), ); const toEvict = inactive.filter((tab) => evictionIds.has(tab.id)); if (toEvict.length > 0) { await Promise.all(toEvict.map((t) => evictCachedResult(t))); } } function scheduleResultCacheTrim() { resultCacheTrimRequested = true; if (resultCacheTrimScheduled || resultCacheTrimRunning) return; resultCacheTrimScheduled = true; const run = () => { resultCacheTrimScheduled = false; void runRequestedResultCacheTrim(); }; // Eviction serializes large result payloads; schedule it after the result // assignment so the grid can paint before cache maintenance starts. if (typeof window !== "undefined" && "requestIdleCallback" in window) { window.requestIdleCallback(run, { timeout: 1500 }); } else { setTimeout(run, 0); } } async function runRequestedResultCacheTrim() { if (resultCacheTrimRunning) return; resultCacheTrimRunning = true; try { while (resultCacheTrimRequested) { resultCacheTrimRequested = false; await trimResultCache(); } } finally { resultCacheTrimRunning = false; if (resultCacheTrimRequested) scheduleResultCacheTrim(); } } function rememberActiveTab(id: string | null) { if (!id || !tabs.value.some((tab) => tab.id === id)) return; activeTabHistory.value = [...activeTabHistory.value.filter((tabId) => tabId !== id), id]; } function fallbackActiveTabAfterClose(closedId: string, closedIndex: number): string | null { const remainingIds = new Set(tabs.value.map((tab) => tab.id)); // Prefer the most recently focused remaining tab. This preserves the // source query tab when a transient table-info/data tab is closed. const history = activeTabHistory.value.filter((tabId) => tabId !== closedId && remainingIds.has(tabId)); activeTabHistory.value = history; return [...history].reverse().find((tabId) => remainingIds.has(tabId)) ?? tabs.value[Math.min(closedIndex, tabs.value.length - 1)]?.id ?? null; } watch( activeTabId, (id) => { rememberActiveTab(id); touchResult( tabs.value.find((tab) => tab.id === id), Date.now(), { reuseEstimatedBytes: true }, ); }, { flush: "sync" }, ); function restoreCachedResultPayload(tab: QueryTab, snapshot: Awaited>) { if (!snapshot) return false; const results = snapshot.results ? markQueryResultsRowsRaw(snapshot.results) : undefined; const activeIndex = snapshot.activeResultIndex ?? 0; tab.results = results; tab.activeResultIndex = snapshot.activeResultIndex; tab.resultEditorFingerprint = snapshot.resultEditorFingerprint; tab.result = snapshot.result ? markQueryResultRowsRaw(snapshot.result) : results?.[activeIndex] ? markQueryResultRowsRaw(results[activeIndex]) : undefined; tab.resultLocalSortOriginalRows = snapshot.resultLocalSortOriginalRows ? markRaw(snapshot.resultLocalSortOriginalRows) : undefined; tab.resultLocalSortOriginalLargeValueCells = snapshot.resultLocalSortOriginalLargeValueCells?.map((cell) => ({ ...cell })); tab.resultLocalSortOriginalMongoDocuments = snapshot.resultLocalSortOriginalMongoDocuments ? markRaw(snapshot.resultLocalSortOriginalMongoDocuments) : undefined; tab.resultLocalSortOriginalMongoCopyDocuments = snapshot.resultLocalSortOriginalMongoCopyDocuments ? markRaw(snapshot.resultLocalSortOriginalMongoCopyDocuments) : undefined; // 快照编解码会重建负载,落盘前的各 run 估算值不再对应恢复后的对象, // 置空让 projectResultRun 按需重算 tab.resultRuns = snapshot.resultRuns ? markQueryResultRunsRowsRaw(snapshot.resultRuns).map((run) => ({ ...run, resultEstimatedBytes: undefined })) : tab.resultRuns; tab.activeResultRunId = snapshot.activeResultRunId ?? tab.activeResultRunId; if (!tab.result && !tab.results && !tab.resultRuns) return false; tab.queryAnalysis = snapshot.queryAnalysis; tab.querySourceColumns = snapshot.querySourceColumns; tab.resultColumnComments = snapshot.resultColumnComments; tab.queryDisplaySourceColumns = snapshot.queryDisplaySourceColumns; tab.queryEditabilityReason = snapshot.queryEditabilityReason; tab.mongoEditTarget = snapshot.mongoEditTarget; // Data tab 的结果快照可能早于最近一次结构变更。已持有真实元数据时, // 不允许旧快照回滚列名或主键;若恢复后仍没有真实列,重新挂起编辑门控。 if (tab.mode === "data" && tab.tableMeta?.columns.length) { // 保留当前真实元数据 } else { tab.tableMeta = snapshot.tableMeta; } if (tab.mode === "data" && !tab.tableMeta?.columns.length) { tab.tableMetaPending = true; } tab.resultPageSql = snapshot.resultPageSql; tab.resultPageLimit = snapshot.resultPageLimit; tab.resultPageOffset = snapshot.resultPageOffset; tab.resultCountSql = snapshot.resultCountSql; tab.resultTotalRowCount = snapshot.resultTotalRowCount; tab.resultTotalRowCountLoading = false; tab.resultSessionId = undefined; tab.resultClientSessionId = undefined; tab.resultEvicted = undefined; tab.resultCacheState = "memory"; touchResult(tab); return true; } async function hydrateResultRunsForArchive(tab: QueryTab, snapshot: NonNullable>) { if (!snapshot.resultRuns?.length) return snapshot; const resultRuns = await Promise.all( snapshot.resultRuns.map(async (run) => { if (resultRunHasPayload(run)) return run; const cacheKey = run.resultCacheKey ?? tab.resultRuns?.find((item) => item.id === run.id)?.resultCacheKey; if (!cacheKey) return run; const cached = await readTabResultSnapshot(cacheKey); return cached?.resultRuns?.find((item) => item.id === run.id) ?? run; }), ); return { ...snapshot, resultRuns }; } async function resultArchiveSnapshotForTab(tab: QueryTab) { let snapshot = buildTabResultSnapshot(tab); if (tab.resultCacheKey && (!snapshot || tab.resultEvicted || !resultSnapshotHasPayload(snapshot))) { snapshot = (await readTabResultSnapshot(tab.resultCacheKey)) ?? snapshot; } if (snapshot) snapshot = await hydrateResultRunsForArchive(tab, snapshot); return snapshot && resultSnapshotHasPayload(snapshot) ? snapshot : undefined; } async function exportResultArchive(id: string): Promise { const tab = tabs.value.find((t) => t.id === id); if (!tab || tab.mode !== "query") return undefined; const snapshot = await resultArchiveSnapshotForTab(tab); if (!snapshot) return undefined; return encodeQueryResultArchive(tab, snapshot); } function openResultArchiveTab(archive: DecodedQueryResultArchive): string | undefined { const id = uuid(); const title = archive.tab.title.trim() || t("tabs.importedResultArchive"); const dbType = useConnectionStore().getConfig(archive.tab.connectionId)?.db_type; const tab: QueryTab = { id, title, customTitle: true, connectionId: archive.tab.connectionId, database: archive.tab.database, schema: archive.tab.schema, sql: archive.tab.sql, originalSql: archive.tab.sql, lastExecutedSql: archive.tab.lastExecutedSql, resultBaseSql: archive.tab.resultBaseSql, resultSortedSql: archive.tab.resultSortedSql, isExecuting: false, isCancelling: false, isExplaining: false, mode: "query", autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), }; if (!restoreCachedResultPayload(tab, archive.snapshot)) return undefined; const activeRun = tab.resultRuns?.find((run) => run.id === tab.activeResultRunId) ?? tab.resultRuns?.[0]; if (activeRun) projectResultRun(tab, activeRun); tabs.value.push(tab); activeTabId.value = id; return id; } async function importResultArchive(bytes: Uint8Array | ArrayBuffer): Promise { const archive = await decodeQueryResultArchive(bytes); if (!archive) return undefined; return openResultArchiveTab(archive); } async function reloadEvictedTab(id: string, { reexecuteOnMissing = false }: { reexecuteOnMissing?: boolean } = {}) { const tab = tabs.value.find((t) => t.id === id); if (!tab || !tab.resultEvicted) return; if (tab.resultCacheKey) { const restored = restoreCachedResultPayload(tab, await readTabResultSnapshot(tab.resultCacheKey)); if (restored) return; tab.resultCacheState = "missing"; if (!reexecuteOnMissing) 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, pagination: tab.mode === "data" ? { limit: tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize), offset: tab.resultPageOffset ?? 0, } : undefined, }); } async function fetchTabResultForExport(id: string, onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void): Promise { const tab = tabs.value.find((t) => t.id === id); if (!tab?.result) return undefined; if (tab.mode === "data") { const connStore = useConnectionStore(); await connStore.ensureConnected(tab.connectionId); const conn = connStore.getConfig(tab.connectionId); const tableMeta = tableMetaForDataTab(tab); if (!tableMeta?.tableName) return tab.result; // Use the already-computed total row count as a progress estimate so the // export dialog shows a moving bar instead of a stuck 0 while paginating. const totalRows = typeof tab.resultTotalRowCount === "number" ? tab.resultTotalRowCount : null; const pageLimit = TABLE_DATA_EXPORT_PAGE_SIZE; const effectiveDbType = effectiveDatabaseTypeForConnection(conn); const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId); const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys; const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined; const orderBy = tab.orderByInput?.trim() || sortOrder; const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tableMeta.catalog); const rows: QueryResult["rows"] = []; let columns: string[] = []; let executionTimeMs = 0; let offset = 0; const clientSessionId = tabClientSessionId(tab, "export"); const exportExecutionId = uuid(); const useAgentCursor = conn?.db_type === "sqlserver" && conn?.driver_profile?.trim().toLowerCase() === "sqlserver-legacy"; let sessionId: string | undefined; try { while (true) { const sql = await api.buildTableSelectSql({ databaseType: effectiveDbType, driverProfile: conn?.driver_profile, identifierQuote, database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, catalog: tableMeta.catalog, columns: tableMeta.columns.map((column) => column.name), primaryKeys, whereInput: tab.whereInput, orderBy, limit: pageLimit, offset, }); const results = await api.executeMulti( tab.connectionId, executionDatabase, sql, undefined, exportExecutionId, useAgentCursor ? { maxRows: 2_147_483_647, fetchSize: pageLimit, pageSize: pageLimit, resultSessionId: sessionId, clientSessionId, catalog: tableMeta.catalog, timeoutSecs: queryTimeoutSecs, } : { maxRows: pageLimit, fetchSize: pageLimit, clientSessionId, catalog: tableMeta.catalog, timeoutSecs: queryTimeoutSecs, }, ); const result = results[0]; if (!result) break; if (columns.length === 0) columns = result.columns; rows.push(...result.rows); executionTimeMs += result.execution_time_ms ?? 0; onProgress?.({ rowsExported: rows.length, totalRows }); sessionId = result.session_id ?? undefined; const shouldFetchNextPage = useAgentCursor ? result.has_more === true : result.rows.length >= pageLimit; if (!shouldFetchNextPage) break; offset += result.rows.length; } } finally { if (sessionId) void api.closeQuerySession(tab.connectionId, executionDatabase, sessionId, clientSessionId, tableMeta.catalog); void closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, tableMeta.catalog, { tabId: tab.id }); } return { columns: columns.length ? columns : tab.result.columns, rows, affected_rows: 0, execution_time_ms: executionTimeMs, truncated: false, has_more: false, }; } if (tab.mode !== "query") return tab.result; const sql = queryResultExecutionSql(tab); if (!sql.trim()) return tab.result; const location = queryResultExecutionLocation(tab); const connStore = useConnectionStore(); await connStore.ensureConnected(location.connectionId); const conn = connStore.getConfig(location.connectionId); const effectiveDbType = effectiveDatabaseTypeForConnection(conn); const executionDatabase = dataTabExecutionDatabase(conn, location.database, location.catalog); // main 引入全局查询超时:queryTimeoutSecsForConnection 现需传入全局默认值; // settingsStore 取 defineStore 顶层声明的实例(本函数无局部覆盖)。 const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile); const queryBaseSql = queryResultBaseSql(tab); const exportSettings = useSettingsStore().editorSettings; const exportRowLimit = exportSettings.exportRowLimitEnabled ? exportSettings.exportRowLimit : Number.POSITIVE_INFINITY; if (effectiveDbType === "mongodb") { let mongoCommand; try { mongoCommand = await api.mongoParseShellCommand(sql); } catch { throw new Error(QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR); } if (mongoCommand.kind !== "find") throw new Error(QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR); const pageLimit = Math.max(1, Math.trunc(exportSettings.exportBatchSize)); const documents: unknown[] = []; let copyDocuments: unknown[] | undefined = []; let pageOffset = 0; let totalRows = typeof tab.resultTotalRowCount === "number" ? Math.min(tab.resultTotalRowCount, exportRowLimit) : null; const exportStartedAt = performance.now(); const exportExecutionId = uuid(); while (documents.length < exportRowLimit) { const remaining = exportRowLimit - documents.length; const plan = planMongoFindPagination(sql, mongoCommand, pageOffset, Math.min(pageLimit, remaining)); if (!plan) throw new Error(QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR); if (plan.requestLimit === 0) break; const result = await api.mongoFindDocuments(location.connectionId, location.database, mongoCommand.collection, plan.requestSkip, plan.requestLimit, mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, mongoCommand.collation, exportExecutionId); const pageDocuments = result.documents.slice(0, plan.requestLimit); documents.push(...pageDocuments); if (copyDocuments) { if (result.extended_documents?.length === result.documents.length) { copyDocuments.push(...result.extended_documents.slice(0, pageDocuments.length)); } else { copyDocuments = undefined; } } if (result.total_is_exact !== false) { totalRows = Math.min(mongoFindLogicalTotal(result.total, plan), exportRowLimit); } onProgress?.({ rowsExported: documents.length, totalRows }); pageOffset += pageDocuments.length; const reachedLogicalLimit = plan.logicalLimit !== undefined && pageOffset >= plan.logicalLimit; const reachedExactTotal = result.total_is_exact !== false && pageOffset >= mongoFindLogicalTotal(result.total, plan); if (pageDocuments.length === 0 || pageDocuments.length < plan.requestLimit || reachedLogicalLimit || reachedExactTotal) break; } const result = mongoDocumentsToQueryResult(documents, performance.now() - exportStartedAt, totalRows ?? documents.length, copyDocuments, totalRows !== null); if (result.columns.length === 0) { result.columns = tab.result.columns; result.column_types = tab.result.column_types; } result.affected_rows = documents.length; result.truncated = false; result.has_more = false; return result; } const agentExportMaxRows = exportSettings.exportRowLimitEnabled ? exportSettings.exportRowLimit : 2_147_483_647; // Use the already-computed total row count as a progress estimate so the // export dialog shows a moving bar instead of a stuck 0 while paginating. const totalRows = typeof tab.resultTotalRowCount === "number" ? Math.min(tab.resultTotalRowCount, exportRowLimit) : null; const pageLimit = Math.max(tab.resultPageLimit ?? 0, TABLE_DATA_EXPORT_PAGE_SIZE); const rows: QueryResult["rows"] = []; let columns: string[] = []; let executionTimeMs = 0; let offset = 0; let sessionId: string | undefined; const clientSessionId = tabClientSessionId(tab, "export"); const exportExecutionId = uuid(); try { while (rows.length < exportRowLimit) { const remaining = exportRowLimit - rows.length; const effectivePageLimit = Math.min(pageLimit, remaining); const plan = await api.prepareQueryPaginationExecutionPlan({ sql, queryBaseSql, databaseType: effectiveDbType, pagination: { limit: effectivePageLimit, offset, sessionId }, useAgentCursor, firstPageUsesActualSql: true, }); if (typeof plan.pageLimit !== "number" || typeof plan.pageOffset !== "number") return tab.result; const executionOptions = plan.useAgentResultSession ? { maxRows: agentExportMaxRows, fetchSize: plan.pageLimit, pageSize: plan.pageLimit, resultSessionId: sessionId, clientSessionId, catalog: location.catalog, timeoutSecs: queryTimeoutSecs, } : { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, catalog: location.catalog, timeoutSecs: queryTimeoutSecs }; const results = await api.executeMulti(location.connectionId, executionDatabase, plan.sqlToExecute, location.schema, exportExecutionId, executionOptions); const result = results[0]; if (!result) break; if (columns.length === 0) columns = result.columns; rows.push(...result.rows); executionTimeMs += result.execution_time_ms ?? 0; onProgress?.({ rowsExported: rows.length, totalRows }); sessionId = result.session_id ?? undefined; const shouldFetchNextPage = plan.useAgentResultSession ? result.has_more === true : result.rows.length >= plan.pageLimit; if (!shouldFetchNextPage || rows.length >= exportRowLimit) break; offset += result.rows.length; } } finally { if (sessionId) void api.closeQuerySession(location.connectionId, executionDatabase, sessionId, clientSessionId, location.catalog); void closeClientSessionId(location.connectionId, executionDatabase, clientSessionId, location.catalog, { tabId: tab.id }); } return { columns: columns.length ? columns : tab.result.columns, rows, affected_rows: 0, execution_time_ms: executionTimeMs, truncated: false, has_more: false, }; } async function buildQueryResultExportRequest(id: string, options: BuildQueryResultExportRequestOptions) { const tab = tabs.value.find((t) => t.id === id); if (!tab?.result || tab.mode !== "query") return undefined; const sql = queryResultExecutionSql(tab); if (!sql.trim()) return undefined; const location = queryResultExecutionLocation(tab); const connStore = useConnectionStore(); await connStore.ensureConnected(location.connectionId); const conn = connStore.getConfig(location.connectionId); const settings = useSettingsStore().editorSettings; const effectiveDbType = effectiveDatabaseTypeForConnection(conn); if (!effectiveDbType) return undefined; if (effectiveDbType === "mongodb") return undefined; const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile); const queryBaseSql = queryResultBaseSql(tab); const resultStatementIndex = tab.result.statement_index; const batchSql = tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql; const batchStatements = effectiveDbType === "postgres" && tab.result.truncated === true && Number.isInteger(resultStatementIndex) && resultStatementIndex! > 0 ? splitSqlStatementRanges(batchSql, effectiveDbType) : []; const setupSql = batchStatements[resultStatementIndex!]?.sql === tab.result.sourceStatement ? batchStatements.slice(0, resultStatementIndex).map((statement) => statement.sql) : undefined; const rowLimit = settings.exportRowLimitEnabled ? settings.exportRowLimit : null; const totalRows = typeof tab.resultTotalRowCount === "number" ? (rowLimit === null ? tab.resultTotalRowCount : Math.min(tab.resultTotalRowCount, rowLimit)) : null; const clientSessionId = `${tabClientSessionId(tab, "export")}:${options.exportId}`; return { exportId: options.exportId, connectionId: location.connectionId, database: dataTabExecutionDatabase(conn, location.database, location.catalog), schema: location.schema, catalog: location.catalog, sql, queryBaseSql, setupSql, databaseType: effectiveDbType, useAgentCursor, filePath: options.filePath, format: options.format, includeSqlSheet: options.format === "xlsx" && options.includeSqlSheet === true, pageSize: settings.exportBatchSize, rowLimit, totalRows, timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs), keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled, clientSessionId, executionId: uuid(), exportTableName: options.exportTableName, exportColumnTypes: options.exportColumnTypes, numericColumnRightAlign: settings.numericColumnRightAlign, identifierQuote: connStore.connectionIdentifierQuote(location.connectionId), }; } async function exportQuerySqlDirect(id: string, sql: string, format: "csv" | "xlsx" | "txt", filePath: string, columnComments?: (string | null)[]) { const tab = tabs.value.find((item) => item.id === id); if (!tab || tab.mode !== "query" || !sql.trim()) return; const connStore = useConnectionStore(); await connStore.ensureConnected(tab.connectionId); const conn = connStore.getConfig(tab.connectionId); const settings = useSettingsStore().editorSettings; const effectiveDbType = effectiveDatabaseTypeForConnection(conn); if (!effectiveDbType) return; const exportId = uuid(); const request: api.QueryResultExportRequest = { exportId, connectionId: tab.connectionId, database: tab.database, schema: tab.schema, sql, queryBaseSql: sql, databaseType: effectiveDbType, useAgentCursor: usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile), filePath, format, pageSize: settings.exportBatchSize, rowLimit: settings.exportRowLimitEnabled ? settings.exportRowLimit : null, totalRows: null, timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs), keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled, clientSessionId: `${tabClientSessionId(tab, "export")}:${exportId}`, executionId: uuid(), numericColumnRightAlign: settings.numericColumnRightAlign, columnComments, identifierQuote: connStore.connectionIdentifierQuote(tab.connectionId), }; const tracker = useExportTracker(); tracker.addTask("Query Result", format, filePath, request.exportId); tracker.registerTaskCancelHandler(request.exportId, () => api.cancelQueryResultExport(request.exportId, request.executionId)); void (async () => { try { await api.startQueryResultExport(request, (progress) => tracker.updateTableExportTask(request.exportId, progress)); } catch (error: any) { const task = tracker.tasks.value.find((item) => item.exportId === request.exportId); if (task) { task.status = "Error"; task.errorMessage = error?.message || String(error); } } finally { tracker.unregisterTaskCancelHandler(request.exportId); } })(); } return { tabs, activeTabId, isOpenTabsLoaded, recentTabIds, initOpenTabs, showCloseConfirm, pendingCloseTabId, closeConfirmContext, closeConfirmDirtyTabIds, hasDirtyTabs, requiresAppCloseDraftPersist, isConfirmingAppClose, createTab, openObjectSourceTab, showExecutedQueryResults, switchTab, closeTab, forceClosePendingTab, forceCloseAllPendingTabs, cancelClosePendingTab, flushPendingPersist, saveAndClosePendingTab, suspendCloseConfirm, resumeCloseConfirm, completePendingCloseAfterSaveAll, isTabDirty, markTabClean, applyExternalSqlFileSnapshot, markExternalSqlFileSaved, updateExternalSqlFileVersion, ignoreExternalSqlFileVersion, acknowledgeExternalSqlFileMissing, relocateExternalSqlFilePath, markExternalSqlFileMissingForPath, discardTabChanges, requestAppCloseConfirmation, closeOtherTabs, closeTabsByIds, closeRightTabs, closeOtherRegularTabs, closeRegularTabs, closeOtherFixedTabs, closeFixedTabs, closeAllTabs, duplicateTab, closeConnectionTabs, closeDatabaseTabs, closeDroppedTableObjectTabs, refreshDataTab, refreshDataTabsForTable, releaseConnectionTabs, releaseDatabaseTabs, staleConnectionDataTabMetadata, isDatabaseOpen, openDatabaseKeys, rollbackConnectionTransactions, rollbackDatabaseTransactions, updateSql, updateDataGridLocalColumnFilters, updateDataGridHiddenColumnKeys, updateEditorViewport, updateEditorSelection, updateObjectBrowserViewport, setAutoCommit, commitTransaction, rollbackTransaction, renameTab, openDatabaseBrowser, openDriverProfileWorkspace, openObjectBrowser, openMongoGridFs, openMongoBucket, openUserAdmin, openProcessList, openSqlServerActivityTrace, openMysqlDashboard, openPostgresDashboard, openNacosDashboard, openDamengUsers, openDamengRoles, openDamengJobAdmin, openMqAdmin, openMqttAdmin, openNacosAdmin, clearNacosNavigationTarget, openTableStructure, linkSavedSql, linkExternalSqlPath, openExternalSqlFile, openSavedSql, hydrateSavedSqlTabs, togglePinnedTab, reorderTab, updateDatabase, updateCatalog, updateSchema, updateConnection, setTableMeta, clearInvalidDataTabSort, invalidateTableStructure, tableStructureRefreshVersion, setObjectSource, setExecuting, setExecutingWithId, setErrorResult, invalidateResultEstimateForPayload, toggleResultAutoSave, setActiveResultRun, toggleResultRunPinned, unpinAllResultRuns, closeOtherResultRuns, closeResultRunsToLeft, closeResultRunsToRight, removeResultRun, closeQueryResult, clearQueryResults, setActiveResultIndex, executeCurrentTab, executeCurrentSql, executeTabSql, dismissBatchSqlRecovery, resumeBatchSql, activeResultExecutionTarget, getExecutionTab, createMultiDbExecutionWorker, captureMultiDbExecutionWorkerResult, removeMultiDbExecutionWorker, cancelMultiDbExecutionScope, sortTabResultLocally, explainTabSql, cancelTabExecution, cancelTabExplain, reloadEvictedTab, exportResultArchive, importResultArchive, fetchTabResultForExport, buildQueryResultExportRequest, exportQuerySqlDirect, getResourceLifecycleDiagnostics: () => resourceLifecycleDiagnostics(tabs.value), notifyConnectionMayBeLost, }; });