fix(structure): load indexes when tab becomes visible

This commit is contained in:
t8y2 committed 2026-08-03 14:49:35 +08:00
1 parent 3e3e6d04b8
commit 5325c42637
5 files changed
+106 -76

No files matched your search

@@ -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());
});
});
@@ -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<string | undefined> {
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) {
@@ -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);
});
});
@@ -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<ObjectMetadataFacet>): 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;
}
+1
View File
@@ -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<Record<TableInfoTab, TableStructureEditorViewport>>;
initialized: boolean;
}