fix(metadata): bound cache maintenance and pool recovery
This commit is contained in:
1 parent
21af5cf600
commit
8c2b232243
41 files changed
+2747
-350
No files matched your search
@@ -129,6 +129,8 @@ import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPr
|
||||
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
|
||||
import { cacheObjectBrowserRows, createObjectBrowserRowsCacheWriteToken, getCachedObjectBrowserRows, type ObjectBrowserRowsCacheScope, type ObjectBrowserRowsCacheWriteToken } from "@/lib/table/objectBrowserRowsCache";
|
||||
import { createObjectBrowserRowsLoadGuard, type ObjectBrowserRowsLoadHandle } from "@/lib/table/objectBrowserRowsLoadGuard";
|
||||
import { loadObjectDdl, type ObjectDdlRequest } from "@/lib/metadata/objectDdlCache";
|
||||
import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
|
||||
|
||||
type ObjectFilter = ObjectBrowserFilter;
|
||||
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
|
||||
@@ -183,14 +185,19 @@ const sidePanelMode = ref<"table-info" | "source" | "type-info">("source");
|
||||
const tableInfoTab = ref<TableInfoTab>("ddl");
|
||||
const tableColumns = ref<ColumnInfo[]>([]);
|
||||
const tableColumnsLoading = ref(false);
|
||||
const tableColumnsLoaded = ref(false);
|
||||
const tableDdlContent = ref("");
|
||||
const tableDdlLoading = ref(false);
|
||||
const tableDdlLoaded = ref(false);
|
||||
const tableIndexes = ref<IndexInfo[]>([]);
|
||||
const tableIndexesLoading = ref(false);
|
||||
const tableIndexesLoaded = ref(false);
|
||||
const tableForeignKeys = ref<ForeignKeyInfo[]>([]);
|
||||
const tableForeignKeysLoading = ref(false);
|
||||
const tableForeignKeysLoaded = ref(false);
|
||||
const tableTriggers = ref<TriggerInfo[]>([]);
|
||||
const tableTriggersLoading = ref(false);
|
||||
const tableTriggersLoaded = ref(false);
|
||||
const tableInfoSearchQuery = ref("");
|
||||
const tableInfoDdlPreRef = ref<HTMLPreElement | null>(null);
|
||||
const SIDE_PANEL_MIN_WIDTH = 280;
|
||||
@@ -973,6 +980,11 @@ async function openTableInfo(row: ObjectBrowserRow, initialTab?: TableInfoTab) {
|
||||
tableIndexes.value = [];
|
||||
tableForeignKeys.value = [];
|
||||
tableTriggers.value = [];
|
||||
tableColumnsLoaded.value = false;
|
||||
tableDdlLoaded.value = false;
|
||||
tableIndexesLoaded.value = false;
|
||||
tableForeignKeysLoaded.value = false;
|
||||
tableTriggersLoaded.value = false;
|
||||
tableInfoSearchQuery.value = "";
|
||||
// Determine initial tab: explicit request > previously activated
|
||||
const firstTab = initialTab ?? tableInfoTab.value;
|
||||
@@ -991,93 +1003,132 @@ async function selectTableInfoTab(tab: TableInfoTab) {
|
||||
else if (nextTab === "triggers") await fetchTableTriggers();
|
||||
}
|
||||
|
||||
async function fetchTableDdl() {
|
||||
function tableMetadataRequest(row: ObjectBrowserRow): ObjectDdlRequest {
|
||||
return {
|
||||
connectionId: props.connection.id,
|
||||
database: props.database || "",
|
||||
schema: row.schema || selectedSchema.value || props.database,
|
||||
tableName: row.name,
|
||||
objectType: tableDdlObjectType(row.type),
|
||||
catalog: props.catalog,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchTableDdl(force = false) {
|
||||
const row = sidePanelRow.value;
|
||||
if (!row) return;
|
||||
if (!row || (tableDdlLoaded.value && !force)) return;
|
||||
const epoch = sidePanelGuard.capture();
|
||||
tableDdlLoading.value = true;
|
||||
let loadedSuccessfully = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const ddl = await api.getTableDisplayDdl(props.connection.id, props.database || "", schema, row.name, tableDdlObjectType(row.type), props.catalog);
|
||||
const { ddl } = await loadObjectDdl(tableMetadataRequest(row), { force });
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableDdlContent.value = ddl;
|
||||
loadedSuccessfully = true;
|
||||
} catch (e: any) {
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableDdlContent.value = `-- Error: ${e?.message || e}`;
|
||||
} finally {
|
||||
if (sidePanelGuard.isFresh(epoch)) tableDdlLoading.value = false;
|
||||
if (sidePanelGuard.isFresh(epoch)) {
|
||||
tableDdlLoaded.value = loadedSuccessfully;
|
||||
tableDdlLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTableColumns() {
|
||||
async function fetchTableColumns(force = false) {
|
||||
const row = sidePanelRow.value;
|
||||
if (!row || tableColumns.value.length > 0) return;
|
||||
if (!row || (tableColumnsLoaded.value && !force)) return;
|
||||
const epoch = sidePanelGuard.capture();
|
||||
tableColumnsLoading.value = true;
|
||||
let loadedSuccessfully = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const columns = await api.getColumns(props.connection.id, props.database || "", schema, row.name, props.catalog);
|
||||
const request = tableMetadataRequest(row);
|
||||
const { value: columns } = await loadObjectMetadataFacet(request, "columns", () => api.getColumns(request.connectionId, request.database, request.schema || request.database, request.tableName, request.catalog), { force });
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableColumns.value = columns;
|
||||
} catch {
|
||||
loadedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableColumns.value = [];
|
||||
toast(translateBackendError(t, error), 5000);
|
||||
} finally {
|
||||
if (sidePanelGuard.isFresh(epoch)) tableColumnsLoading.value = false;
|
||||
if (sidePanelGuard.isFresh(epoch)) {
|
||||
tableColumnsLoaded.value = loadedSuccessfully;
|
||||
tableColumnsLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTableIndexes() {
|
||||
async function fetchTableIndexes(force = false) {
|
||||
const row = sidePanelRow.value;
|
||||
if (!row || tableIndexes.value.length > 0) return;
|
||||
if (!row || (tableIndexesLoaded.value && !force)) return;
|
||||
const epoch = sidePanelGuard.capture();
|
||||
tableIndexesLoading.value = true;
|
||||
let loadedSuccessfully = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const indexes = await api.listIndexes(props.connection.id, props.database || "", schema, row.name, props.catalog);
|
||||
const request = tableMetadataRequest(row);
|
||||
const { value: indexes } = await loadObjectMetadataFacet(request, "indexes", () => api.listIndexes(request.connectionId, request.database, request.schema || request.database, request.tableName, request.catalog), { force });
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableIndexes.value = indexes;
|
||||
} catch {
|
||||
loadedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableIndexes.value = [];
|
||||
toast(translateBackendError(t, error), 5000);
|
||||
} finally {
|
||||
if (sidePanelGuard.isFresh(epoch)) tableIndexesLoading.value = false;
|
||||
if (sidePanelGuard.isFresh(epoch)) {
|
||||
tableIndexesLoaded.value = loadedSuccessfully;
|
||||
tableIndexesLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTableForeignKeys() {
|
||||
async function fetchTableForeignKeys(force = false) {
|
||||
const row = sidePanelRow.value;
|
||||
if (!row || tableForeignKeys.value.length > 0) return;
|
||||
if (!row || (tableForeignKeysLoaded.value && !force)) return;
|
||||
const epoch = sidePanelGuard.capture();
|
||||
tableForeignKeysLoading.value = true;
|
||||
let loadedSuccessfully = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const fks = await api.listForeignKeys(props.connection.id, props.database || "", schema, row.name, props.catalog);
|
||||
const request = tableMetadataRequest(row);
|
||||
const { value: fks } = await loadObjectMetadataFacet(request, "foreign-keys", () => api.listForeignKeys(request.connectionId, request.database, request.schema || request.database, request.tableName, request.catalog), { force });
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableForeignKeys.value = fks;
|
||||
} catch {
|
||||
loadedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableForeignKeys.value = [];
|
||||
toast(translateBackendError(t, error), 5000);
|
||||
} finally {
|
||||
if (sidePanelGuard.isFresh(epoch)) tableForeignKeysLoading.value = false;
|
||||
if (sidePanelGuard.isFresh(epoch)) {
|
||||
tableForeignKeysLoaded.value = loadedSuccessfully;
|
||||
tableForeignKeysLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTableTriggers() {
|
||||
async function fetchTableTriggers(force = false) {
|
||||
const row = sidePanelRow.value;
|
||||
if (!row || tableTriggers.value.length > 0) return;
|
||||
if (!row || (tableTriggersLoaded.value && !force)) return;
|
||||
const epoch = sidePanelGuard.capture();
|
||||
tableTriggersLoading.value = true;
|
||||
let loadedSuccessfully = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const triggers = await api.listTriggers(props.connection.id, props.database || "", schema, row.name, props.catalog);
|
||||
const request = tableMetadataRequest(row);
|
||||
const { value: triggers } = await loadObjectMetadataFacet(request, "triggers", () => api.listTriggers(request.connectionId, request.database, request.schema || request.database, request.tableName, request.catalog), { force });
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableTriggers.value = triggers;
|
||||
} catch {
|
||||
loadedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
tableTriggers.value = [];
|
||||
toast(translateBackendError(t, error), 5000);
|
||||
} finally {
|
||||
if (sidePanelGuard.isFresh(epoch)) tableTriggersLoading.value = false;
|
||||
if (sidePanelGuard.isFresh(epoch)) {
|
||||
tableTriggersLoaded.value = loadedSuccessfully;
|
||||
tableTriggersLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,19 +1138,24 @@ async function refreshActiveTableInfo() {
|
||||
|
||||
if (tableInfoTab.value === "ddl") {
|
||||
tableDdlContent.value = "";
|
||||
await fetchTableDdl();
|
||||
tableDdlLoaded.value = false;
|
||||
await fetchTableDdl(true);
|
||||
} else if (tableInfoTab.value === "columns") {
|
||||
tableColumns.value = [];
|
||||
await fetchTableColumns();
|
||||
tableColumnsLoaded.value = false;
|
||||
await fetchTableColumns(true);
|
||||
} else if (tableInfoTab.value === "indexes") {
|
||||
tableIndexes.value = [];
|
||||
await fetchTableIndexes();
|
||||
tableIndexesLoaded.value = false;
|
||||
await fetchTableIndexes(true);
|
||||
} else if (tableInfoTab.value === "foreignKeys") {
|
||||
tableForeignKeys.value = [];
|
||||
await fetchTableForeignKeys();
|
||||
tableForeignKeysLoaded.value = false;
|
||||
await fetchTableForeignKeys(true);
|
||||
} else if (tableInfoTab.value === "triggers") {
|
||||
tableTriggers.value = [];
|
||||
await fetchTableTriggers();
|
||||
tableTriggersLoaded.value = false;
|
||||
await fetchTableTriggers(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-5
@@ -30,14 +30,33 @@ describe("ObjectBrowser table metadata refresh", () => {
|
||||
|
||||
expect(refreshTableInfo).toContain('sidePanelMode.value !== "table-info" || !sidePanelRow.value');
|
||||
expect(refreshTableInfo).toContain("sidePanelGuard.bump();");
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "ddl"[\s\S]*?tableDdlContent\.value = "";[\s\S]*?await fetchTableDdl\(\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "columns"[\s\S]*?tableColumns\.value = \[\];[\s\S]*?await fetchTableColumns\(\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "indexes"[\s\S]*?tableIndexes\.value = \[\];[\s\S]*?await fetchTableIndexes\(\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "foreignKeys"[\s\S]*?tableForeignKeys\.value = \[\];[\s\S]*?await fetchTableForeignKeys\(\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "triggers"[\s\S]*?tableTriggers\.value = \[\];[\s\S]*?await fetchTableTriggers\(\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "ddl"[\s\S]*?tableDdlContent\.value = "";[\s\S]*?await fetchTableDdl\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "columns"[\s\S]*?tableColumns\.value = \[\];[\s\S]*?await fetchTableColumns\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "indexes"[\s\S]*?tableIndexes\.value = \[\];[\s\S]*?await fetchTableIndexes\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "foreignKeys"[\s\S]*?tableForeignKeys\.value = \[\];[\s\S]*?await fetchTableForeignKeys\(true\);/);
|
||||
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "triggers"[\s\S]*?tableTriggers\.value = \[\];[\s\S]*?await fetchTableTriggers\(true\);/);
|
||||
});
|
||||
|
||||
it("routes table-info metadata through the object caches", () => {
|
||||
expect(source).toContain('from "@/lib/metadata/objectDdlCache"');
|
||||
expect(source).toContain('from "@/lib/metadata/objectMetadataCache"');
|
||||
expect(functionBody("fetchTableDdl")).toContain("loadObjectDdl(");
|
||||
for (const name of ["fetchTableColumns", "fetchTableIndexes", "fetchTableForeignKeys", "fetchTableTriggers"]) {
|
||||
expect(functionBody(name)).toContain("loadObjectMetadataFacet(");
|
||||
expect(functionBody(name)).not.toContain(".value.length > 0");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps automatic object reloads free of extra metadata requests", () => {
|
||||
expect(functionBody("reload")).not.toContain("refreshActiveTableInfo");
|
||||
});
|
||||
|
||||
it("leaves a failed facet eligible for retry", () => {
|
||||
for (const name of ["fetchTableDdl", "fetchTableColumns", "fetchTableIndexes", "fetchTableForeignKeys", "fetchTableTriggers"]) {
|
||||
const body = functionBody(name);
|
||||
expect(body).toContain("let loadedSuccessfully = false;");
|
||||
expect(body).toMatch(/loadedSuccessfully = true;/);
|
||||
expect(body).toMatch(/Loaded\.value = loadedSuccessfully;/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user