From a8592af97a64736655fbbf6a4fc8915bd3db0e48 Mon Sep 17 00:00:00 2001 From: miracle Date: Wed, 29 Jul 2026 00:44:01 +0800 Subject: [PATCH] fix(editor): avoid metadata wait for wildcard queries --- .../queryStore.hiddenPrimaryKey.spec.ts | 77 +++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 10 ++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts index e96378eb9..35d1c539b 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts @@ -131,6 +131,44 @@ describe("queryStore hidden primary key editing", () => { expect(tab.queryEditabilityReason).toBeUndefined(); }); + it("starts a qualified MySQL star query before slow column metadata finishes", async () => { + const columnsGate = deferred>>(); + getColumns.mockReturnValue(columnsGate.promise); + analyzeEditableQueryEditability.mockResolvedValue({ + editable: true, + analysis: { + schema: undefined, + tableName: "sys_dept", + selectStar: true, + columns: [], + }, + }); + executeMulti.mockResolvedValue([ + { + columns: ["dept_id", "dept_name"], + rows: [[1, "Headquarters"]], + affected_rows: 0, + execution_time_ms: 12, + }, + ]); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("mysql-1", "app", "Query"); + + const execution = store.executeTabSql(tabId, "SELECT sys_dept.* FROM sys_dept"); + await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled()); + expect(executeMulti).toHaveBeenCalledWith("mysql-1", "app", "SELECT sys_dept.* FROM sys_dept", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 })); + + columnsGate.resolve([ + { name: "dept_id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }, + { name: "dept_name", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + ]); + await execution; + const tab = store.tabs.find((item) => item.id === tabId)!; + await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("sys_dept")); + }); + it("loads metadata from the connection default database when the query tab database is empty", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); @@ -208,6 +246,45 @@ describe("queryStore hidden primary key editing", () => { expect(tab.queryEditabilityReason).toBeUndefined(); }); + it("starts a MySQL JDBC star query before slow column metadata finishes", async () => { + const columnsGate = deferred>>(); + getConnectionConfig.mockReturnValue({ id: "jdbc-1", name: "JDBC MySQL", db_type: "jdbc", connection_string: "jdbc:mysql://localhost:3306/app", database: "app", query_timeout_secs: 30 }); + getColumns.mockReturnValue(columnsGate.promise); + analyzeEditableQueryEditability.mockResolvedValue({ + editable: true, + analysis: { + schema: undefined, + tableName: "sys_dept", + selectStar: true, + columns: [], + }, + }); + executeMulti.mockResolvedValue([ + { + columns: ["dept_id", "dept_name"], + rows: [[1, "Headquarters"]], + affected_rows: 0, + execution_time_ms: 12, + }, + ]); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("jdbc-1", "", "Query"); + + const execution = store.executeTabSql(tabId, "SELECT * FROM sys_dept"); + await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled()); + expect(executeMulti).toHaveBeenCalledWith("jdbc-1", "", "SELECT * FROM sys_dept", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 })); + + columnsGate.resolve([ + { name: "dept_id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }, + { name: "dept_name", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + ]); + await execution; + const tab = store.tabs.find((item) => item.id === tabId)!; + await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("sys_dept")); + }); + it("keeps an explicitly selected database instead of falling back to the connection default", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index a6dc3cb72..80829ace3 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -339,6 +339,10 @@ function editableQuerySources(analysis: EditableQueryInfo): EditableQuerySource[ ]; } +function projectsAllColumnsForSource(analysis: EditableQueryInfo, sourceKey: string): boolean { + return analysis.selectStar || analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === sourceKey)); +} + function cloneAnalysisForSource(analysis: EditableQueryInfo, source: EditableQuerySource): EditableQueryInfo { return { ...analysis, @@ -2729,6 +2733,9 @@ export const useQueryStore = defineStore("query", () => { const analysis = editability.analysis; const sources = editableQuerySources(analysis); if (sources.length !== 1 || analysis.distinct) 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" && projectsAllColumnsForSource(analysis, sources[0]!.key)) return unchanged; const target = resolveEditableSourceMetadataTarget(tab, analysis, sources[0]!, conn, databaseType, executionDatabase); const cached = getCachedTableMetadata(target.request); @@ -2743,8 +2750,7 @@ export const useQueryStore = defineStore("query", () => { }); void fullMetadataPromise.catch((error) => queryExecutionLog("warn", "metadata:table-prefetch:failed", { traceId, error, elapsed: elapsed() })); const indexes = await loadTableIndexes(target.request); - const projectsAllColumns = target.analysis.selectStar || target.analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === target.source.key)); - if (primaryKeyIndex(indexes) && projectsAllColumns) { + if (primaryKeyIndex(indexes) && projectsAllColumnsForSource(target.analysis, target.source.key)) { return unchanged; } loaded = loadedEditableSourceFromMetadata(target, (await fullMetadataPromise).metadata);