From 56008725655a592524ffadfa8d6bf099d284c517 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 24 Jul 2026 00:51:26 +0800 Subject: [PATCH] fix(oracle): lazy load table triggers --- .../structure/TableStructureEditor.vue | 70 ++++++++++++------- .../tableStructureMetadataLoading.spec.ts | 37 ++++++++++ .../table/tableStructureMetadataLoading.ts | 33 +++++++++ apps/desktop/src/types/database.ts | 1 + 4 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts create mode 100644 apps/desktop/src/lib/table/tableStructureMetadataLoading.ts diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 1c6aa1f17e..39de4fbf0d 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -32,6 +32,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 { 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"; @@ -172,24 +173,9 @@ const warnings = ref([]); const sqliteSchemaRevision = ref(); const foreignKeys = ref([]); const triggers = ref([]); +const triggersLoaded = ref(false); const secondaryMetadataLoading = computed(() => indexesLoading.value || foreignKeysLoading.value || triggersLoading.value); -interface StructureRefreshScope { - columns: boolean; - indexes: boolean; - foreignKeys: boolean; - triggers: boolean; - tableComment: boolean; -} - -const FULL_STRUCTURE_REFRESH_SCOPE: StructureRefreshScope = { - columns: true, - indexes: true, - foreignKeys: true, - triggers: true, - tableComment: true, -}; - function sameList(left: string[] | null | undefined, right: string[] | null | undefined): boolean { const a = left ?? []; const b = right ?? []; @@ -251,7 +237,7 @@ function triggerChanged(trigger: EditableStructureTrigger): boolean { return trigger.name !== original.name || trigger.timing !== original.timing || trigger.event !== original.event || !sameText(trigger.statement, original.statement); } -function captureStructureRefreshScope(): StructureRefreshScope { +function captureStructureRefreshScope(): TableStructureRefreshScope { return { columns: columns.value.some(columnChanged), indexes: indexes.value.some(indexChanged), @@ -921,6 +907,7 @@ function createCurrentDraft(initialized = true): TableStructureEditorDraft { indexes: cloneDraftValue(indexes.value), foreignKeys: cloneDraftValue(foreignKeys.value), triggers: cloneDraftValue(triggers.value), + triggersLoaded: triggersLoaded.value, scrollPositions: cloneDraftValue(structureScrollPositions.value), initialized, }; @@ -945,6 +932,8 @@ function restoreDraft(draft: TableStructureEditorDraft) { indexes.value = cloneDraftValue(draft.indexes || []); foreignKeys.value = cloneDraftValue(draft.foreignKeys || []); triggers.value = cloneDraftValue(draft.triggers || []); + // Drafts created before lazy trigger loading always contained live trigger metadata. + triggersLoaded.value = draft.triggersLoaded ?? true; structureScrollPositions.value = cloneDraftValue(draft.scrollPositions || {}); restoringDraft = false; draftHydrated = !needsColumnDraftMetadataHydration(); @@ -1195,6 +1184,7 @@ function resetState() { sqliteSchemaRevision.value = undefined; foreignKeys.value = []; triggers.value = []; + triggersLoaded.value = false; selectedColumnId.value = null; ddlContent.value = ""; ddlFetched.value = false; @@ -1213,10 +1203,14 @@ function resetState() { async function reloadStructureFromDatabase() { if (isCreateMode.value) return; draftHydrated = false; - await loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }); + if (activeTab.value !== "triggers") { + triggers.value = []; + triggersLoaded.value = false; + } + await loadStructure(false, visibleTableStructureRefreshScope(activeTab.value), true, { blockSecondaryMetadata: true }); } -function setSecondaryMetadataLoading(scope: StructureRefreshScope, value: boolean) { +function setSecondaryMetadataLoading(scope: TableStructureRefreshScope, value: boolean) { if (scope.indexes && tableMetadataCapabilities.value.indexes) indexesLoading.value = value; if (scope.foreignKeys && tableMetadataCapabilities.value.foreignKeys) foreignKeysLoading.value = value; if (scope.triggers && tableMetadataCapabilities.value.triggers) triggersLoading.value = value; @@ -1236,7 +1230,7 @@ async function fetchTableCommentValue(connectionId: string, database: string, sc } } -async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; damengLengthUnitsAfterSave?: ReadonlyMap } = {}) { +async function loadStructure(silent = false, scope: TableStructureRefreshScope = visibleTableStructureRefreshScope(activeTab.value), showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; damengLengthUnitsAfterSave?: ReadonlyMap } = {}) { const connectionId = props.connectionId; const database = props.database; const catalog = props.catalog; @@ -1289,7 +1283,10 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL if (requestId !== structureLoadRequestId) return; if (nextIndexes) indexes.value = createIndexDrafts(nextIndexes); if (nextForeignKeys) foreignKeys.value = createForeignKeyDrafts(nextForeignKeys); - if (nextTriggers) triggers.value = createTriggerDrafts(nextTriggers); + if (nextTriggers) { + triggers.value = createTriggerDrafts(nextTriggers); + triggersLoaded.value = true; + } }; secondaryMetadataScheduled = true; @@ -1321,7 +1318,7 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL } } -async function refreshStructureAfterSave(scope: StructureRefreshScope, damengLengthUnitsAfterSave: ReadonlyMap) { +async function refreshStructureAfterSave(scope: TableStructureRefreshScope, damengLengthUnitsAfterSave: ReadonlyMap) { try { await loadStructure(true, scope, false, { blockSecondaryMetadata: true, damengLengthUnitsAfterSave }); } catch (e) { @@ -2294,7 +2291,7 @@ function addItemForActiveTab(): boolean { addForeignKey(); return true; } - if (activeTab.value === "triggers" && canEditTriggers.value) { + if (activeTab.value === "triggers" && canEditTriggers.value && !triggersLoading.value) { addTrigger(); return true; } @@ -2350,7 +2347,7 @@ onMounted(() => { } else if (isCreateMode.value) { markDraftHydratedAndSync(); } else { - void loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }).then(() => applyInitialStructureTarget()); + void loadStructure(false, visibleTableStructureRefreshScope(activeTab.value), true, { blockSecondaryMetadata: true }).then(() => applyInitialStructureTarget()); } }); @@ -2466,6 +2463,25 @@ 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) => { @@ -2493,7 +2509,11 @@ watch(refreshVersion, (version, previous) => { skipNextRefreshVersion = false; return; } - void loadStructure(true); + if (activeTab.value !== "triggers") { + triggers.value = []; + triggersLoaded.value = false; + } + void loadStructure(true, visibleTableStructureRefreshScope(activeTab.value)); }); watch( diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts new file mode 100644 index 0000000000..ebd349ab4b --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/tableStructureMetadataLoading.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { shouldLoadTableStructureTriggers, visibleTableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; + +describe("table structure metadata loading", () => { + it("does not request triggers while opening the default columns tab", () => { + expect(visibleTableStructureRefreshScope("columns").triggers).toBe(false); + }); + + it("requests triggers when the structure editor opens on the trigger tab", () => { + expect(visibleTableStructureRefreshScope("triggers").triggers).toBe(true); + }); + + 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, + }; + + expect(shouldLoadTableStructureTriggers({ ...base, loaded: false })).toBe(true); + expect(shouldLoadTableStructureTriggers({ ...base, loaded: true })).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); + }); +}); diff --git a/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts b/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts new file mode 100644 index 0000000000..894ca37084 --- /dev/null +++ b/apps/desktop/src/lib/table/tableStructureMetadataLoading.ts @@ -0,0 +1,33 @@ +import type { TableInfoTab } from "@/types/database"; + +export interface TableStructureRefreshScope { + columns: boolean; + indexes: boolean; + foreignKeys: boolean; + triggers: boolean; + tableComment: boolean; +} + +export function visibleTableStructureRefreshScope(activeTab: TableInfoTab): TableStructureRefreshScope { + return { + columns: true, + indexes: true, + foreignKeys: true, + // Trigger definitions can contain large source bodies, so defer them until + // the trigger editor is actually visible. + triggers: activeTab === "triggers", + tableComment: true, + }; +} + +export const TRIGGERS_ONLY_REFRESH_SCOPE: TableStructureRefreshScope = { + columns: false, + indexes: false, + foreignKeys: false, + triggers: true, + tableComment: false, +}; + +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; +} diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index c7166d5ec9..b7aa1630fd 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -754,6 +754,7 @@ export interface TableStructureEditorDraft { indexes: import("@/lib/table/tableStructureEditorSql").EditableStructureIndex[]; foreignKeys: import("@/lib/table/tableStructureEditorSql").EditableStructureForeignKey[]; triggers: import("@/lib/table/tableStructureEditorSql").EditableStructureTrigger[]; + triggersLoaded?: boolean; scrollPositions?: Partial>; initialized: boolean; }