From 5325c4263720f82eaf53fe78218b88ab5f86d4d7 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 3 Aug 2026 14:49:35 +0800 Subject: [PATCH] fix(structure): load indexes when tab becomes visible --- .../TableStructureEditor.primaryKey.spec.ts | 48 +++++++++++ .../structure/TableStructureEditor.vue | 81 ++++++++----------- .../tableStructureMetadataLoading.spec.ts | 30 +++---- .../table/tableStructureMetadataLoading.ts | 22 ++--- apps/desktop/src/types/database.ts | 1 + 5 files changed, 106 insertions(+), 76 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts index 8f518a5a43..f878b5fb2d 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts +++ b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts @@ -383,4 +383,52 @@ describe("TableStructureEditor metadata loading", () => { expect(mocks.loadObjectMetadataFacet.mock.calls.map((call) => call[1])).toEqual(expectedFacets); expect(mocks.loadObjectDdl).not.toHaveBeenCalled(); }); + + it("loads index metadata when an initialized column draft opens the indexes tab", async () => { + mocks.connection.db_type = "postgres"; + mocks.connection.name = "postgres"; + mocks.connection.driver_label = "postgres"; + mocks.ensureConnected.mockResolvedValue(undefined); + mocks.listDataTypes.mockResolvedValue([]); + mocks.buildTableStructureChangeSql.mockResolvedValue({ statements: [], warnings: [] }); + mocks.loadObjectMetadataFacet.mockImplementation(async (_request, facet: string) => ({ + value: + facet === "indexes" + ? [ + { + name: "idx_users_id", + columns: ["id"], + is_unique: false, + is_primary: false, + }, + ] + : facet === "comment" + ? "" + : [], + cacheStatus: "remote", + })); + + const root = document.createElement("div"); + document.body.append(root); + const app = createApp(TableStructureEditor, { + connectionId: mocks.connection.id, + database: "test", + schema: "public", + tableName: "users", + initialTab: "indexes", + draft: { + ...draft(), + loadedMetadataFacets: ["columns", "comment"], + }, + }); + mountedApps.push(app); + app.mount(root); + await nextTick(); + await Promise.resolve(); + await nextTick(); + + await vi.waitFor(() => expect(mocks.loadObjectMetadataFacet).toHaveBeenCalledTimes(1)); + expect(mocks.loadObjectMetadataFacet.mock.calls.map((call) => call[1])).toEqual(["indexes"]); + await vi.waitFor(() => expect(root.querySelector('[data-index-row-index="0"]')).not.toBeNull()); + }); }); diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index d5e6278ee0..df7a701692 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -35,7 +35,7 @@ import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp"; import { getPostgresDataTypeHelp } from "@/lib/table/postgresDataTypeHelp"; import { getSqliteDataTypeHelp } from "@/lib/table/sqliteDataTypeHelp"; import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities"; -import { shouldLoadTableStructureTriggers, TRIGGERS_ONLY_REFRESH_SCOPE, visibleTableStructureRefreshScope, type TableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; +import { hasTableStructureRefreshWork, unloadedTableStructureRefreshScope, visibleTableStructureRefreshScope, type TableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; import { orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; import { loadTableDataGridColumnOrder, notifyTableDataGridColumnOrderChanged, removeTableDataGridColumnOrder, saveTableDataGridColumnOrder, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage"; @@ -925,6 +925,7 @@ function createCurrentDraft(initialized = true): TableStructureEditorDraft { foreignKeys: cloneDraftValue(foreignKeys.value), triggers: cloneDraftValue(triggers.value), triggersLoaded: triggersLoaded.value, + loadedMetadataFacets: [...loadedMetadataFacets], scrollPositions: cloneDraftValue(structureScrollPositions.value), initialized, }; @@ -951,6 +952,17 @@ function restoreDraft(draft: TableStructureEditorDraft) { triggers.value = cloneDraftValue(draft.triggers || []); // Drafts created before lazy trigger loading always contained live trigger metadata. triggersLoaded.value = draft.triggersLoaded ?? true; + loadedMetadataFacets.clear(); + if (draft.loadedMetadataFacets) { + for (const facet of draft.loadedMetadataFacets) loadedMetadataFacets.add(facet); + } else { + const activeScope = visibleTableStructureRefreshScope(draft.activeTab || "columns"); + if (activeScope.columns) loadedMetadataFacets.add("columns"); + if (activeScope.indexes || draft.indexes?.length) loadedMetadataFacets.add("indexes"); + if (activeScope.foreignKeys || draft.foreignKeys?.length) loadedMetadataFacets.add("foreign-keys"); + if (activeScope.triggers || triggersLoaded.value) loadedMetadataFacets.add("triggers"); + if (activeScope.tableComment) loadedMetadataFacets.add("comment"); + } structureScrollPositions.value = cloneDraftValue(draft.scrollPositions || {}); restoringDraft = false; draftHydrated = !needsColumnDraftMetadataHydration(); @@ -1244,15 +1256,6 @@ function setSecondaryMetadataLoading(scope: TableStructureRefreshScope, value: b if (scope.triggers && tableMetadataCapabilities.value.triggers) triggersLoading.value = value; } -function hasLoadedMetadataScope(scope: TableStructureRefreshScope): boolean { - if (scope.columns && !loadedMetadataFacets.has("columns")) return false; - if (scope.indexes && !loadedMetadataFacets.has("indexes")) return false; - if (scope.foreignKeys && !loadedMetadataFacets.has("foreign-keys")) return false; - if (scope.triggers && !loadedMetadataFacets.has("triggers")) return false; - if (scope.tableComment && !loadedMetadataFacets.has("comment")) return false; - return true; -} - async function fetchTableCommentValue(connectionId: string, database: string, schema: string, tableName: string, catalog?: string): Promise { try { return (await api.getTableComment(connectionId, database, schema, tableName, catalog)) || ""; @@ -2420,7 +2423,10 @@ onMounted(() => { } structureEditorReady = true; if (props.draft?.initialized) { - void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget()); + void hydrateRestoredDraftFromDatabase().then(() => { + applyInitialStructureTarget(); + void loadActiveTableStructureMetadataIfNeeded(); + }); } else if (isCreateMode.value) { markDraftHydratedAndSync(); } else if (activeTab.value === "ddl") { @@ -2436,7 +2442,10 @@ onActivated(() => { if (props.draft?.initialized && !draftHydrated) { restoreDraft(props.draft); applyInitialStructureTarget(); - void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget()); + void hydrateRestoredDraftFromDatabase().then(() => { + applyInitialStructureTarget(); + void loadActiveTableStructureMetadataIfNeeded(); + }); } restoreStructureScrollPosition(); }); @@ -2542,25 +2551,6 @@ watch(activeTab, () => { syncDraftToParent(); }); -async function loadTriggersIfNeeded() { - if ( - !shouldLoadTableStructureTriggers({ - activeTab: activeTab.value, - isCreateMode: isCreateMode.value, - supported: tableMetadataCapabilities.value.triggers, - loaded: triggersLoaded.value, - loading: triggersLoading.value, - structureLoading: loading.value, - }) - ) - return; - await loadStructure(true, TRIGGERS_ONLY_REFRESH_SCOPE, true, { blockSecondaryMetadata: true, preserveDraft: true }); -} - -watch([activeTab, loading], () => { - void loadTriggersIfNeeded(); -}); - watch( columns, (items) => { @@ -2595,21 +2585,20 @@ watch(refreshVersion, (version, previous) => { void loadStructure(true, visibleTableStructureRefreshScope(activeTab.value)); }); -watch( - activeTab, - (tab) => { - if (!structureEditorReady) return; - if (tab === "ddl") { - void fetchDdl(); - } else if (!isCreateMode.value && !props.draft?.initialized && !loading.value) { - const scope = visibleTableStructureRefreshScope(tab); - if (!hasLoadedMetadataScope(scope)) { - void loadStructure(false, scope, true, { blockSecondaryMetadata: true }).then(() => applyInitialStructureTarget()); - } - } - }, - { flush: "sync" }, -); +async function loadActiveTableStructureMetadataIfNeeded() { + if (!structureEditorReady || isCreateMode.value) return; + if (activeTab.value === "ddl") { + await fetchDdl(); + return; + } + if (loading.value || secondaryMetadataLoading.value) return; + const scope = unloadedTableStructureRefreshScope(activeTab.value, loadedMetadataFacets); + if (!hasTableStructureRefreshWork(scope)) return; + await loadStructure(true, scope, true, { blockSecondaryMetadata: true, preserveDraft: true }); + applyInitialStructureTarget(); +} + +watch([activeTab, loading, secondaryMetadataLoading], () => void loadActiveTableStructureMetadataIfNeeded(), { flush: "sync" }); watch([activeTab, ddlLoading], ([tab, loading]) => { if (tab === "ddl" && !loading) { diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts index 1c6c6b03bd..57d0a9cab0 100644 --- a/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { shouldLoadTableStructureTriggers, visibleTableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; +import { hasTableStructureRefreshWork, unloadedTableStructureRefreshScope, visibleTableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; describe("table structure metadata loading", () => { it.each([ @@ -12,28 +12,16 @@ describe("table structure metadata loading", () => { expect(visibleTableStructureRefreshScope(tab)).toEqual(expected); }); - it("loads trigger metadata once when the trigger tab becomes visible", () => { - const base = { - activeTab: "triggers" as const, - isCreateMode: false, - supported: true, - loading: false, - structureLoading: false, - }; + it("requests only index metadata after columns and comments are already loaded", () => { + const scope = unloadedTableStructureRefreshScope("indexes", new Set(["columns", "comment"])); - expect(shouldLoadTableStructureTriggers({ ...base, loaded: false })).toBe(true); - expect(shouldLoadTableStructureTriggers({ ...base, loaded: true })).toBe(false); + expect(scope).toEqual({ columns: false, indexes: true, foreignKeys: false, triggers: false, tableComment: false }); + expect(hasTableStructureRefreshWork(scope)).toBe(true); + expect(hasTableStructureRefreshWork(unloadedTableStructureRefreshScope("indexes", new Set(["columns", "indexes", "comment"])))).toBe(false); }); - it("waits for the initial structure load and skips create mode", () => { - const base = { - activeTab: "triggers" as const, - supported: true, - loaded: false, - loading: false, - }; - - expect(shouldLoadTableStructureTriggers({ ...base, isCreateMode: false, structureLoading: true })).toBe(false); - expect(shouldLoadTableStructureTriggers({ ...base, isCreateMode: true, structureLoading: false })).toBe(false); + it("requests trigger metadata only until that facet is loaded", () => { + expect(unloadedTableStructureRefreshScope("triggers", new Set(["comment"]))).toEqual({ columns: false, indexes: false, foreignKeys: false, triggers: true, tableComment: false }); + expect(hasTableStructureRefreshWork(unloadedTableStructureRefreshScope("triggers", new Set(["triggers", "comment"])))).toBe(false); }); }); diff --git a/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts b/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts index 2b8aa99a94..7dae7b7355 100644 --- a/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts +++ b/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts @@ -1,4 +1,5 @@ import type { TableInfoTab } from "@/types/database"; +import type { ObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache"; export interface TableStructureRefreshScope { columns: boolean; @@ -23,14 +24,17 @@ export function visibleTableStructureRefreshScope(activeTab: TableInfoTab): Tabl } } -export const TRIGGERS_ONLY_REFRESH_SCOPE: TableStructureRefreshScope = { - columns: false, - indexes: false, - foreignKeys: false, - triggers: true, - tableComment: false, -}; +export function unloadedTableStructureRefreshScope(activeTab: TableInfoTab, loadedFacets: ReadonlySet): TableStructureRefreshScope { + const visibleScope = visibleTableStructureRefreshScope(activeTab); + return { + columns: visibleScope.columns && !loadedFacets.has("columns"), + indexes: visibleScope.indexes && !loadedFacets.has("indexes"), + foreignKeys: visibleScope.foreignKeys && !loadedFacets.has("foreign-keys"), + triggers: visibleScope.triggers && !loadedFacets.has("triggers"), + tableComment: visibleScope.tableComment && !loadedFacets.has("comment"), + }; +} -export function shouldLoadTableStructureTriggers(options: { activeTab: TableInfoTab; isCreateMode: boolean; supported: boolean; loaded: boolean; loading: boolean; structureLoading: boolean }): boolean { - return options.activeTab === "triggers" && !options.isCreateMode && options.supported && !options.loaded && !options.loading && !options.structureLoading; +export function hasTableStructureRefreshWork(scope: TableStructureRefreshScope): boolean { + return scope.columns || scope.indexes || scope.foreignKeys || scope.triggers || scope.tableComment; } diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 399bd34d4f..8a4ced9dc7 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -900,6 +900,7 @@ export interface TableStructureEditorDraft { foreignKeys: import("@/lib/table/tableStructureEditorSql").EditableStructureForeignKey[]; triggers: import("@/lib/table/tableStructureEditorSql").EditableStructureTrigger[]; triggersLoaded?: boolean; + loadedMetadataFacets?: import("@/lib/metadata/objectMetadataCache").ObjectMetadataFacet[]; scrollPositions?: Partial>; initialized: boolean; }