From 71d07348ad2e9b28b2578c9090a7fd0bc7974c80 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 27 Aug 2026 22:25:39 +0800 Subject: [PATCH] perf(oracle): stop slow index metadata from blocking first query --- .../queryStore.hiddenPrimaryKey.spec.ts | 64 ++++++++++++++++++- apps/desktop/src/stores/queryStore.ts | 32 +++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts index 4241ab388a..8ffed77375 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts @@ -157,7 +157,7 @@ describe("queryStore hidden primary key editing", () => { expect(tab.queryAnalysis).toBeDefined(); expect(tab.queryAnalysis?.allowInsert).toBe(false); expect(tab.queryEditabilityReason).toBeUndefined(); - }); + }, 10_000); it("keeps MySQL expression columns read-only without disabling direct columns", async () => { const sql = "SELECT id, status, extra->>'$.mode' mode, extra->>'$.template' tmpl FROM items"; @@ -801,6 +801,68 @@ describe("queryStore hidden primary key editing", () => { await execution; }); + it("starts an Oracle star query when index metadata exceeds the preflight budget", async () => { + vi.useFakeTimers(); + const columnsGate = deferred>>(); + const indexesGate = deferred>>(); + try { + getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 }); + getColumns.mockReturnValue(columnsGate.promise); + listIndexes.mockReturnValue(indexesGate.promise); + analyzeEditableQueryEditability.mockImplementation(async () => ({ + editable: true, + analysis: { + schema: "APP", + tableName: "SLOW_METADATA_TABLE", + selectStar: true, + columns: [], + }, + })); + executeMulti.mockResolvedValue([ + { + columns: ["ID", "NAME"], + rows: [[1, "Alice"]], + affected_rows: 0, + execution_time_ms: 12, + }, + ]); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oracle-1", "ORCL", "Query"); + store.setAutoCommit(tabId, true); + + const execution = store.executeTabSql(tabId, "SELECT * FROM APP.SLOW_METADATA_TABLE"); + await vi.waitFor(() => expect(listIndexes).toHaveBeenCalledOnce()); + expect(executeMulti).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1_000); + await vi.waitFor(() => expect(executeMulti).toHaveBeenCalledOnce()); + expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT * FROM APP.SLOW_METADATA_TABLE", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 })); + await execution; + + indexesGate.resolve([{ name: "PK_SLOW_METADATA_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]); + columnsGate.resolve([ + { name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null }, + { name: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + ]); + await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.tableMeta?.primaryKeys).toEqual(["ID"])); + + await store.executeTabSql(tabId, "SELECT * FROM APP.SLOW_METADATA_TABLE"); + expect(executeMulti).toHaveBeenCalledTimes(2); + expect(listIndexes).toHaveBeenCalledOnce(); + expect(getColumns).toHaveBeenCalledOnce(); + } finally { + indexesGate.resolve([{ name: "PK_SLOW_METADATA_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]); + columnsGate.resolve([ + { name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null }, + { name: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + ]); + await vi.runAllTimersAsync(); + vi.useRealTimers(); + } + }); + it("waits for first-run Oracle XMLTYPE metadata before manual transaction execution", async () => { const columnsGate = deferred>>(); getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index f982bd0459..e925692dac 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -104,6 +104,8 @@ const QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR = "Streaming export is unsupported f 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"; @@ -529,6 +531,20 @@ async function withCancelQueryTimeout(promise: Promise): Promise { } } +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(); @@ -4051,9 +4067,19 @@ export const useQueryStore = defineStore("query", () => { 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 indexes = await loadTableIndexes(target.request); - if (primaryKeyIndex(indexes) && projectsAllColumnsForSource(target.analysis, target.source.key) && tab.autoCommit !== false) { - return unchanged; + 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); }