From 3bb54625f3efc985fa1b895625bde80159640288 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 8 Sep 2026 22:25:20 +0800 Subject: [PATCH] refactor(grid): extract table metadata loaders --- apps/desktop/src/components/grid/DataGrid.vue | 371 +++--------------- .../useDataGridTableMetadataLoaders.ts | 313 +++++++++++++++ 2 files changed, 376 insertions(+), 308 deletions(-) create mode 100644 apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index b4cea4e0f9..7cc820fa0a 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -84,16 +84,11 @@ import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTex import GridSnapshotDialog from "@/components/grid/GridSnapshotDialog.vue"; import type { QueryResult, ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo, TableInfoTab, QueryResultSourceColumnRef, QueryPageJumpProgress } from "@/types/database"; import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError"; -import { tableObjectSourceKind } from "@/lib/table/tableObjectSourceKind"; import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation"; import { shouldNavigateFromTableInfoColumnClick } from "@/lib/table/tableInfoColumnNavigation"; import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference"; -import { loadObjectDdl } from "@/lib/metadata/objectDdlCache"; -import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache"; import * as api from "@/lib/backend/api"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; -import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; -import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; import type { SqlInsertMode } from "@/lib/export/sqlInsertMode"; import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion"; import { createColumnDrafts } from "@/lib/table/tableStructureEditorState"; @@ -283,7 +278,7 @@ import { dataGridSelectedSortMenuValue, type DataGridColumnSortState, } from "@/lib/dataGrid/dataGridContextMenu"; -import { buildColumnForeignKeyMap, combineForeignKeyConditions, foreignKeyAssociationCells, foreignKeyMetadataRequestCurrent, foreignKeyNavigationTarget, foreignKeySourceColumnName, foreignKeyTableIdentity, type ForeignKeyAssociation } from "@/lib/dataGrid/dataGridForeignKeyNavigation"; +import { buildColumnForeignKeyMap, combineForeignKeyConditions, foreignKeyAssociationCells, foreignKeyNavigationTarget, foreignKeySourceColumnName, type ForeignKeyAssociation } from "@/lib/dataGrid/dataGridForeignKeyNavigation"; import { collectForeignKeyDisplayValues, createForeignKeyDisplayRequestCoordinator, @@ -354,13 +349,12 @@ import { constraintsForConstraintsTab } from "@/lib/table/constraintPresentation import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo"; import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp"; import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout"; -import { buildColumnIndexMap, columnIndexColorClass, columnIndexMetadataRequestCurrent, columnIndexNameKey, columnIndexTableIdentity, type ColumnIndexKind } from "@/lib/dataGrid/dataGridColumnIndexIcon"; +import { buildColumnIndexMap, columnIndexColorClass, columnIndexNameKey, type ColumnIndexKind } from "@/lib/dataGrid/dataGridColumnIndexIcon"; import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, dataGridLocalFilterLabel, restoreDataGridLocalColumnFilters, rowMatchesDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; import { effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint } from "@/lib/database/jdbcDialect"; import { mongoCollectionSupportsIndexes, supportsMongoIndexMutations } from "@/lib/mongo/mongoCapabilities"; -import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata"; import { isProtectedMongoIndex, mongoDropAllIndexesPreview, mongoDropIndexFailureCount, mongoDropIndexPreview } from "@/lib/sidebar/mongoCollectionMutation"; import { runMongoMutation } from "@/lib/sidebar/runMongoSidebarMutation"; import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote, dataGridFilterColumns } from "@/lib/dataGrid/dataGridConditionCompletion"; @@ -370,6 +364,7 @@ import { formatShortcut } from "@/lib/editor/shortcutRegistry"; import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; import { SearchableSelect } from "@/components/ui/searchable-select"; import { useDataGridColumnFormatter } from "@/composables/useDataGridColumnFormatter"; +import { useDataGridTableMetadataLoaders } from "@/composables/useDataGridTableMetadataLoaders"; const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue")); const ImagePreviewDialog = defineAsyncComponent(() => import("@/components/grid/ImagePreviewDialog.vue")); @@ -401,6 +396,11 @@ const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridB const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes); const { isDark, themePalette } = useTheme(); const { toast } = useToast(); +let tableMetadataLoaderFetchForeignKeys: () => Promise = async () => {}; + +function fetchForeignKeys() { + return tableMetadataLoaderFetchForeignKeys(); +} // 外键单元格跳转复用导航入口;对话框引用传 stub(同 AiAssistant 模式) const { openTableTarget } = useNavigationTargets({ showFieldLineageDialog: ref(false), @@ -10544,42 +10544,16 @@ const tableInfoDrawerPinned = computed(() => settingsStore.editorSettings.tableI const ddlContent = ref(""); const tableInfoColumns = ref(props.tableMeta?.columns ?? []); const tableInfoColumnsLoading = ref(false); -let tableInfoColumnsRequestGeneration = 0; +const tableInfoColumnsRequestGeneration = ref(0); const ddlPreRef = ref(null); const ddlSearchMatchCount = ref(0); const ddlSearchMatchIndex = ref(0); const tableOwner = ref(null); const tableOwnerLoading = ref(false); const tableOwnerError = ref(""); -let tableOwnerRequestGeneration = 0; +const tableOwnerRequestGeneration = ref(0); const canShowTableOwner = computed(() => resolvedDatabaseType.value === "postgres" && !!props.connectionId && !!props.database && !!props.tableMeta?.schema && !!props.tableMeta?.tableName); -async function fetchTableOwner(force = false) { - if (!canShowTableOwner.value || !props.connectionId || !props.database || !props.tableMeta?.schema || !props.tableMeta.tableName) return; - const requestGeneration = ++tableOwnerRequestGeneration; - const request = { - connectionId: props.connectionId, - database: props.database, - schema: props.tableMeta.schema, - tableName: props.tableMeta.tableName, - catalog: props.tableMeta.catalog, - objectType: tableObjectSourceKind(props.tableMeta.tableType), - }; - tableOwnerLoading.value = true; - tableOwnerError.value = ""; - try { - const result = await loadObjectMetadataFacet(request, "owner", () => api.getTableOwner(request.connectionId, request.database, request.schema, request.tableName), { force }); - if (requestGeneration !== tableOwnerRequestGeneration) return; - tableOwner.value = result.value; - } catch (error: any) { - if (requestGeneration !== tableOwnerRequestGeneration) return; - tableOwner.value = null; - tableOwnerError.value = error?.message || String(error); - } finally { - if (requestGeneration === tableOwnerRequestGeneration) tableOwnerLoading.value = false; - } -} - function scrollDdlSearchMatchIntoView(match: HTMLElement) { const pre = ddlPreRef.value; if (!pre) return; @@ -10652,16 +10626,7 @@ let detailResizeStartY = 0; let detailResizeStartHeight = 0; let mongoJsonPreviewResizeStartX = 0; let mongoJsonPreviewResizeStartWidth = 0; -const currentIndexTableIdentity = computed(() => - columnIndexTableIdentity({ - connectionId: props.connectionId, - database: props.database, - catalog: props.tableMeta?.catalog, - schema: props.tableMeta?.schema, - tableName: props.tableMeta?.tableName, - }), -); -let indexesRequestGeneration = 0; +const indexesRequestGeneration = ref(0); const showDropMongoIndexConfirm = ref(false); const dropMongoIndexLoading = ref(false); const pendingDropMongoIndex = ref(null); @@ -10671,16 +10636,7 @@ const foreignKeys = ref([]); const foreignKeysLoaded = ref(false); const foreignKeysLoading = ref(false); const foreignKeysError = ref(""); -const currentForeignKeyTableIdentity = computed(() => - foreignKeyTableIdentity({ - connectionId: props.connectionId, - database: props.database, - catalog: props.tableMeta?.catalog, - schema: props.tableMeta?.schema, - tableName: props.tableMeta?.tableName, - }), -); -let foreignKeysRequestGeneration = 0; +const foreignKeysRequestGeneration = ref(0); const triggers = ref([]); const triggersLoaded = ref(false); const triggersLoading = ref(false); @@ -10689,16 +10645,7 @@ const constraints = ref([]); const constraintsLoaded = ref(false); const constraintsLoading = ref(false); const constraintsError = ref(""); -const currentConstraintTableIdentity = computed(() => - columnIndexTableIdentity({ - connectionId: props.connectionId, - database: props.database, - catalog: props.tableMeta?.catalog, - schema: props.tableMeta?.schema, - tableName: props.tableMeta?.tableName, - }), -); -let constraintsRequestGeneration = 0; +const constraintsRequestGeneration = ref(0); // The Constraints tab hides foreign keys when a dedicated Foreign Keys tab is // also shown (each constraint appears once; FK navigation stays in that tab). const constraintsForTab = computed(() => constraintsForConstraintsTab(constraints.value, tableMetadataCapabilities.value.foreignKeys)); @@ -10810,6 +10757,51 @@ const canOpenTableStructureEditor = computed(() => !!props.connectionId && !!pro const mongoConnectionConfig = resolvedConnectionConfig; const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && supportsMongoIndexMutations(mongoConnectionConfig.value, props.tableMeta?.tableType)); const canShowTableIndexes = computed(() => tableMetadataCapabilities.value.indexes && (resolvedDatabaseType.value !== "mongodb" || mongoCollectionSupportsIndexes(props.tableMeta?.tableType))); + +const metadataLoaders = useDataGridTableMetadataLoaders({ + props, + state: { + ddlContent, + ddlLoading, + tableInfoColumns, + tableInfoColumnsLoading, + tableOwner, + tableOwnerLoading, + tableOwnerError, + indexes, + indexesLoaded, + indexesLoading, + indexesError, + foreignKeys, + foreignKeysLoaded, + foreignKeysLoading, + foreignKeysError, + triggers, + triggersLoaded, + triggersLoading, + triggersError, + constraints, + constraintsLoaded, + constraintsLoading, + constraintsError, + tableInfoColumnsRequestGeneration, + tableOwnerRequestGeneration, + indexesRequestGeneration, + foreignKeysRequestGeneration, + constraintsRequestGeneration, + }, + settingsStore, + connectionStore, + resolvedDatabaseType, + canShowTableIndexes, + showTableInfo, + toast, + formatBackendError: (error) => translateBackendError(t, error), + toastMongoIndexRefreshError: (message) => toast(t("contextMenu.mongoIndexRefreshFailed", { message }), 5000), +}); + +const { fetchDdl, fetchTableInfoColumns, fetchTableOwner, currentIndexTableIdentity, fetchIndexes, refreshMongoIndexMetadataAfterMutation, currentForeignKeyTableIdentity, fetchForeignKeys: fetchForeignKeysMetadata, fetchTriggers, fetchConstraints } = metadataLoaders; +tableMetadataLoaderFetchForeignKeys = fetchForeignKeysMetadata; const tableInfoTabs = computed(() => { const tabs: TableInfoTabItem[] = []; if (tableMetadataCapabilities.value.ddl) { @@ -10908,58 +10900,6 @@ watch( { immediate: true }, ); -async function fetchDdl(force = settingsStore.editorSettings.refreshDdlOnOpen) { - if (!props.connectionId || !props.tableMeta) return; - showTableInfo.value = true; - ddlLoading.value = true; - try { - // Preserve view identity so the backend loads the stored view source instead of synthesizing table DDL. - const { ddl } = await loadObjectDdl( - { - connectionId: props.connectionId, - database: props.database || "", - schema: props.tableMeta.schema || props.database || "", - tableName: props.tableMeta.tableName, - objectType: tableObjectSourceKind(props.tableMeta.tableType), - catalog: props.tableMeta.catalog, - }, - { force }, - ); - const formatDialect = sqlFormatDialectForDbType(resolvedDatabaseType.value); - ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect); - } catch (e: any) { - ddlContent.value = `-- Error: ${e}`; - } finally { - ddlLoading.value = false; - } -} - -async function fetchTableInfoColumns(force = false) { - if (!props.connectionId || !props.tableMeta) return; - const requestGeneration = ++tableInfoColumnsRequestGeneration; - tableInfoColumnsLoading.value = true; - try { - // Route through the shared metadata facet cache so a grid-side refresh - // invalidates/populates the same cache the sidebar and hover views read. - const request = { - connectionId: props.connectionId, - database: props.database || "", - schema: props.tableMeta.schema || props.database || "", - tableName: props.tableMeta.tableName, - objectType: tableObjectSourceKind(props.tableMeta.tableType), - catalog: props.tableMeta.catalog, - }; - const { value: columns } = await loadObjectMetadataFacet(request, "columns", () => api.getColumns(request.connectionId, request.database, request.schema, request.tableName, request.catalog), { force }); - if (requestGeneration !== tableInfoColumnsRequestGeneration) return; - tableInfoColumns.value = columns; - } catch (error) { - if (requestGeneration !== tableInfoColumnsRequestGeneration) return; - toast(translateBackendError(t, error), 5000); - } finally { - if (requestGeneration === tableInfoColumnsRequestGeneration) tableInfoColumnsLoading.value = false; - } -} - async function refreshActiveTableInfo() { if (!showTableInfo.value || !props.tableMeta) return; if (canShowTableOwner.value) void fetchTableOwner(true); @@ -10981,212 +10921,27 @@ async function refreshActiveTableInfo() { } } -async function fetchIndexes() { - const requestIdentity = currentIndexTableIdentity.value; - if (!props.connectionId || !props.tableMeta || !canShowTableIndexes.value || !requestIdentity || indexesLoaded.value || indexesLoading.value) return; - const connectionId = props.connectionId; - const database = props.database || ""; - const schema = props.tableMeta.schema || props.database || ""; - const tableName = props.tableMeta.tableName; - const catalog = props.tableMeta.catalog; - const requestGeneration = ++indexesRequestGeneration; - indexesLoading.value = true; - indexesError.value = ""; - try { - const nextIndexes = await api.listIndexes(connectionId, database, schema, tableName, catalog); - if ( - !columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: indexesRequestGeneration, - requestIdentity, - currentIdentity: currentIndexTableIdentity.value, - }) - ) - return; - indexes.value = nextIndexes; - indexesLoaded.value = true; - } catch (e: any) { - if ( - !columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: indexesRequestGeneration, - requestIdentity, - currentIdentity: currentIndexTableIdentity.value, - }) - ) - return; - indexesError.value = String(e?.message || e); - } finally { - if ( - columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: indexesRequestGeneration, - requestIdentity, - currentIdentity: currentIndexTableIdentity.value, - }) - ) - indexesLoading.value = false; - } -} - -async function reloadIndexes() { - indexesLoaded.value = false; - await fetchIndexes(); -} - -async function refreshMongoIndexMetadataAfterMutation() { - await reloadIndexes(); - const connectionId = props.connectionId; - const database = props.database; - const collection = props.tableMeta?.tableName; - if (!connectionId || !database || !collection) return; - try { - await refreshLoadedMongoIndexes(connectionStore, { - connectionId, - database, - collection, - }); - } catch (e: any) { - toast( - t("contextMenu.mongoIndexRefreshFailed", { - message: String(e?.message || e), - }), - 5000, - ); - } -} - -async function fetchForeignKeys() { - const requestIdentity = currentForeignKeyTableIdentity.value; - if (!props.connectionId || !props.tableMeta || !requestIdentity || foreignKeysLoaded.value || foreignKeysLoading.value) return; - const connectionId = props.connectionId; - const database = props.database || ""; - const schema = props.tableMeta.schema || props.database || ""; - const tableName = props.tableMeta.tableName; - const catalog = props.tableMeta.catalog; - const requestGeneration = ++foreignKeysRequestGeneration; - foreignKeysLoading.value = true; - foreignKeysError.value = ""; - try { - const nextForeignKeys = await api.listForeignKeys(connectionId, database, schema, tableName, catalog); - if ( - !foreignKeyMetadataRequestCurrent({ - requestGeneration, - currentGeneration: foreignKeysRequestGeneration, - requestIdentity, - currentIdentity: currentForeignKeyTableIdentity.value, - }) - ) - return; - foreignKeys.value = nextForeignKeys; - foreignKeysLoaded.value = true; - } catch (e: any) { - if ( - !foreignKeyMetadataRequestCurrent({ - requestGeneration, - currentGeneration: foreignKeysRequestGeneration, - requestIdentity, - currentIdentity: currentForeignKeyTableIdentity.value, - }) - ) - return; - foreignKeysError.value = String(e?.message || e); - } finally { - if ( - foreignKeyMetadataRequestCurrent({ - requestGeneration, - currentGeneration: foreignKeysRequestGeneration, - requestIdentity, - currentIdentity: currentForeignKeyTableIdentity.value, - }) - ) - foreignKeysLoading.value = false; - } -} - -async function fetchTriggers() { - if (!props.connectionId || !props.tableMeta || triggersLoaded.value || triggersLoading.value) return; - triggersLoading.value = true; - triggersError.value = ""; - try { - triggers.value = await api.listTriggers(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, props.tableMeta.catalog); - triggersLoaded.value = true; - } catch (e: any) { - triggersError.value = String(e?.message || e); - } finally { - triggersLoading.value = false; - } -} - -async function fetchConstraints() { - const requestIdentity = currentConstraintTableIdentity.value; - if (!props.connectionId || !props.tableMeta || !requestIdentity || constraintsLoaded.value || constraintsLoading.value) return; - const connectionId = props.connectionId; - const database = props.database || ""; - const schema = props.tableMeta.schema || props.database || ""; - const tableName = props.tableMeta.tableName; - const catalog = props.tableMeta.catalog; - const requestGeneration = ++constraintsRequestGeneration; - constraintsLoading.value = true; - constraintsError.value = ""; - try { - const nextConstraints = await api.listConstraints(connectionId, database, schema, tableName, catalog); - if ( - !columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: constraintsRequestGeneration, - requestIdentity, - currentIdentity: currentConstraintTableIdentity.value, - }) - ) - return; - constraints.value = nextConstraints; - constraintsLoaded.value = true; - } catch (e: any) { - if ( - !columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: constraintsRequestGeneration, - requestIdentity, - currentIdentity: currentConstraintTableIdentity.value, - }) - ) - return; - constraintsError.value = String(e?.message || e); - } finally { - if ( - columnIndexMetadataRequestCurrent({ - requestGeneration, - currentGeneration: constraintsRequestGeneration, - requestIdentity, - currentIdentity: currentConstraintTableIdentity.value, - }) - ) - constraintsLoading.value = false; - } -} - watch( () => [props.connectionId, props.database, props.tableMeta?.catalog, props.tableMeta?.schema, props.tableMeta?.tableName], () => { tableInfoColumns.value = props.tableMeta?.columns ?? []; tableInfoColumnsLoading.value = false; - tableInfoColumnsRequestGeneration += 1; + tableInfoColumnsRequestGeneration.value += 1; tableOwner.value = null; tableOwnerLoading.value = false; tableOwnerError.value = ""; - tableOwnerRequestGeneration += 1; + tableOwnerRequestGeneration.value += 1; ddlContent.value = ""; indexes.value = []; indexesLoaded.value = false; indexesLoading.value = false; indexesError.value = ""; - indexesRequestGeneration += 1; + indexesRequestGeneration.value += 1; foreignKeys.value = []; foreignKeysLoaded.value = false; foreignKeysLoading.value = false; foreignKeysError.value = ""; - foreignKeysRequestGeneration += 1; + foreignKeysRequestGeneration.value += 1; triggers.value = []; triggersLoaded.value = false; triggersError.value = ""; @@ -11194,7 +10949,7 @@ watch( constraintsLoaded.value = false; constraintsLoading.value = false; constraintsError.value = ""; - constraintsRequestGeneration += 1; + constraintsRequestGeneration.value += 1; // 表身份变更后,主动触发索引加载,确保索引指示器在切换表后立即可见 if (showIndexIndicatorsInHeader.value && canShowTableIndexes.value && currentIndexTableIdentity.value) { void fetchIndexes(); diff --git a/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts b/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts new file mode 100644 index 0000000000..f62674dbe6 --- /dev/null +++ b/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts @@ -0,0 +1,313 @@ +import { computed, type ComputedRef, type Ref } from "vue"; +import * as api from "@/lib/backend/api"; +import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; +import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; +import { loadObjectDdl } from "@/lib/metadata/objectDdlCache"; +import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache"; +import { tableObjectSourceKind } from "@/lib/table/tableObjectSourceKind"; +import { columnIndexMetadataRequestCurrent, columnIndexTableIdentity } from "@/lib/dataGrid/dataGridColumnIndexIcon"; +import { foreignKeyMetadataRequestCurrent, foreignKeyTableIdentity } from "@/lib/dataGrid/dataGridForeignKeyNavigation"; +import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata"; +import type { ColumnInfo, ConstraintInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database"; +import { useConnectionStore } from "@/stores/connectionStore"; + +type SettingsStore = ReturnType; +type ConnectionStore = ReturnType; + +interface TableMetadataProps { + connectionId?: string; + database?: string; + context?: "results" | "table-data"; + tableInfoTab?: string; + autoShowTableInfo?: boolean; + tableMeta?: { + catalog?: string; + database?: string; + schema?: string; + tableName: string; + tableType?: string; + columns: ColumnInfo[]; + primaryKeys: string[]; + }; +} + +interface TableMetadataState { + ddlContent: Ref; + ddlLoading: Ref; + tableInfoColumns: Ref; + tableInfoColumnsLoading: Ref; + tableOwner: Ref; + tableOwnerLoading: Ref; + tableOwnerError: Ref; + indexes: Ref; + indexesLoaded: Ref; + indexesLoading: Ref; + indexesError: Ref; + foreignKeys: Ref; + foreignKeysLoaded: Ref; + foreignKeysLoading: Ref; + foreignKeysError: Ref; + triggers: Ref; + triggersLoaded: Ref; + triggersLoading: Ref; + triggersError: Ref; + constraints: Ref; + constraintsLoaded: Ref; + constraintsLoading: Ref; + constraintsError: Ref; + tableInfoColumnsRequestGeneration: Ref; + tableOwnerRequestGeneration: Ref; + indexesRequestGeneration: Ref; + foreignKeysRequestGeneration: Ref; + constraintsRequestGeneration: Ref; +} + +export interface DataGridTableMetadataLoaderOptions { + props: TableMetadataProps; + state: TableMetadataState; + settingsStore: SettingsStore; + connectionStore: ConnectionStore; + resolvedDatabaseType: ComputedRef; + canShowTableIndexes: ComputedRef; + showTableInfo: Ref; + toast: (message: string, duration?: number) => void; + formatBackendError: (error: unknown) => string; + toastMongoIndexRefreshError: (message: string) => void; +} + +export function useDataGridTableMetadataLoaders(options: DataGridTableMetadataLoaderOptions) { + const { props, state } = options; + + const currentTableIdentity = (kind: "columns" | "owner" | "indexes" | "foreignKeys" | "constraints") => + kind === "foreignKeys" + ? foreignKeyTableIdentity({ + connectionId: props.connectionId, + database: props.database, + catalog: props.tableMeta?.catalog, + schema: props.tableMeta?.schema, + tableName: props.tableMeta?.tableName, + }) + : columnIndexTableIdentity({ + connectionId: props.connectionId, + database: props.database, + catalog: props.tableMeta?.catalog, + schema: props.tableMeta?.schema, + tableName: props.tableMeta?.tableName, + }); + + const tableRequest = () => { + if (!props.connectionId || !props.tableMeta) return undefined; + return { + connectionId: props.connectionId, + database: props.database || "", + schema: props.tableMeta.schema || props.database || "", + tableName: props.tableMeta.tableName, + catalog: props.tableMeta.catalog, + objectType: tableObjectSourceKind(props.tableMeta.tableType), + }; + }; + + const currentIndexTableIdentity = computed(() => currentTableIdentity("indexes")); + const currentForeignKeyTableIdentity = computed(() => currentTableIdentity("foreignKeys")); + const currentConstraintTableIdentity = computed(() => currentTableIdentity("constraints")); + + async function fetchDdl(force = options.settingsStore.editorSettings.refreshDdlOnOpen) { + const request = tableRequest(); + if (!request) return; + options.showTableInfo.value = true; + state.ddlLoading.value = true; + try { + const { ddl } = await loadObjectDdl(request, { force }); + const formatDialect = sqlFormatDialectForDbType(options.resolvedDatabaseType.value); + state.ddlContent.value = options.settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect); + } catch (error: any) { + state.ddlContent.value = `-- Error: ${error}`; + } finally { + state.ddlLoading.value = false; + } + } + + async function fetchTableInfoColumns(force = false) { + const request = tableRequest(); + if (!request) return; + const requestGeneration = ++state.tableInfoColumnsRequestGeneration.value; + state.tableInfoColumnsLoading.value = true; + try { + const { value: columns } = await loadObjectMetadataFacet(request, "columns", () => api.getColumns(request.connectionId, request.database, request.schema, request.tableName, request.catalog), { force }); + if (requestGeneration !== state.tableInfoColumnsRequestGeneration.value) return; + state.tableInfoColumns.value = columns; + } catch (error) { + if (requestGeneration !== state.tableInfoColumnsRequestGeneration.value) return; + options.toast(options.formatBackendError(error), 5000); + } finally { + if (requestGeneration === state.tableInfoColumnsRequestGeneration.value) state.tableInfoColumnsLoading.value = false; + } + } + + async function fetchTableOwner(force = false) { + if (options.resolvedDatabaseType.value !== "postgres" || !props.connectionId || !props.database || !props.tableMeta?.schema || !props.tableMeta.tableName) return; + const request = tableRequest(); + if (!request) return; + const requestGeneration = ++state.tableOwnerRequestGeneration.value; + state.tableOwnerLoading.value = true; + state.tableOwnerError.value = ""; + try { + const result = await loadObjectMetadataFacet(request, "owner", () => api.getTableOwner(request.connectionId, request.database, request.schema, request.tableName), { force }); + if (requestGeneration !== state.tableOwnerRequestGeneration.value) return; + state.tableOwner.value = result.value; + } catch (error: any) { + if (requestGeneration !== state.tableOwnerRequestGeneration.value) return; + state.tableOwner.value = null; + state.tableOwnerError.value = error?.message || String(error); + } finally { + if (requestGeneration === state.tableOwnerRequestGeneration.value) state.tableOwnerLoading.value = false; + } + } + + async function fetchIndexes() { + const request = tableRequest(); + const requestIdentity = currentTableIdentity("indexes"); + if (!request || !requestIdentity || !options.canShowTableIndexes.value || state.indexesLoaded.value || state.indexesLoading.value) return; + const requestGeneration = ++state.indexesRequestGeneration.value; + await runMetadataRequest({ + loading: state.indexesLoading, + error: state.indexesError, + requestGeneration, + currentGeneration: () => state.indexesRequestGeneration.value, + isCurrent: () => + columnIndexMetadataRequestCurrent({ + requestGeneration, + currentGeneration: state.indexesRequestGeneration.value, + requestIdentity, + currentIdentity: currentTableIdentity("indexes"), + }), + load: () => api.listIndexes(request.connectionId, request.database, request.schema, request.tableName, request.catalog), + onSuccess: (value) => { + state.indexes.value = value; + state.indexesLoaded.value = true; + }, + }); + } + + async function reloadIndexes() { + state.indexesLoaded.value = false; + await fetchIndexes(); + } + + async function refreshMongoIndexMetadataAfterMutation() { + await reloadIndexes(); + const connectionId = props.connectionId; + const database = props.database; + const collection = props.tableMeta?.tableName; + if (!connectionId || !database || !collection) return; + try { + await refreshLoadedMongoIndexes(options.connectionStore, { connectionId, database, collection }); + } catch (error: any) { + options.toastMongoIndexRefreshError(String(error?.message || error)); + } + } + + async function fetchForeignKeys() { + const request = tableRequest(); + const requestIdentity = currentTableIdentity("foreignKeys"); + if (!request || !requestIdentity || state.foreignKeysLoaded.value || state.foreignKeysLoading.value) return; + const requestGeneration = ++state.foreignKeysRequestGeneration.value; + await runMetadataRequest({ + loading: state.foreignKeysLoading, + error: state.foreignKeysError, + requestGeneration, + currentGeneration: () => state.foreignKeysRequestGeneration.value, + isCurrent: () => + foreignKeyMetadataRequestCurrent({ + requestGeneration, + currentGeneration: state.foreignKeysRequestGeneration.value, + requestIdentity, + currentIdentity: currentTableIdentity("foreignKeys"), + }), + load: () => api.listForeignKeys(request.connectionId, request.database, request.schema, request.tableName, request.catalog), + onSuccess: (value) => { + state.foreignKeys.value = value; + state.foreignKeysLoaded.value = true; + }, + }); + } + + async function fetchTriggers() { + const request = tableRequest(); + if (!request || state.triggersLoaded.value || state.triggersLoading.value) return; + state.triggersLoading.value = true; + state.triggersError.value = ""; + try { + state.triggers.value = await api.listTriggers(request.connectionId, request.database, request.schema, request.tableName, request.catalog); + state.triggersLoaded.value = true; + } catch (error: any) { + state.triggersError.value = String(error?.message || error); + } finally { + state.triggersLoading.value = false; + } + } + + async function fetchConstraints() { + const request = tableRequest(); + const requestIdentity = currentTableIdentity("constraints"); + if (!request || !requestIdentity || state.constraintsLoaded.value || state.constraintsLoading.value) return; + const requestGeneration = ++state.constraintsRequestGeneration.value; + await runMetadataRequest({ + loading: state.constraintsLoading, + error: state.constraintsError, + requestGeneration, + currentGeneration: () => state.constraintsRequestGeneration.value, + isCurrent: () => + columnIndexMetadataRequestCurrent({ + requestGeneration, + currentGeneration: state.constraintsRequestGeneration.value, + requestIdentity, + currentIdentity: currentTableIdentity("constraints"), + }), + load: () => api.listConstraints(request.connectionId, request.database, request.schema, request.tableName, request.catalog), + onSuccess: (value) => { + state.constraints.value = value; + state.constraintsLoaded.value = true; + }, + }); + } + + return { + currentIndexTableIdentity, + currentForeignKeyTableIdentity, + currentConstraintTableIdentity, + fetchDdl, + fetchTableInfoColumns, + fetchTableOwner, + fetchIndexes, + reloadIndexes, + refreshMongoIndexMetadataAfterMutation, + fetchForeignKeys, + fetchTriggers, + fetchConstraints, + }; +} + +interface MetadataRequest { + loading: Ref; + error: Ref; + requestGeneration: number; + currentGeneration: () => number; + isCurrent: () => boolean; + load: () => Promise; + onSuccess: (value: T) => void; +} + +async function runMetadataRequest(request: MetadataRequest) { + request.loading.value = true; + request.error.value = ""; + try { + const value = await request.load(); + if (!request.isCurrent()) return; + request.onSuccess(value); + } catch (error: any) { + if (request.isCurrent()) request.error.value = String(error?.message || error); + } finally { + if (request.isCurrent() && request.currentGeneration() === request.requestGeneration) request.loading.value = false; + } +}