455 lines
23 KiB
TypeScript
455 lines
23 KiB
TypeScript
import * as api from "@/lib/backend/api";
|
||
import { connectionObjectTreeNodeSchema, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||
import { invalidateTableMetadataCache, loadTableMetadata } from "@/lib/metadata/tableMetadataCache";
|
||
import { canApplyDataTabMetadata, canReuseActiveMongoTab, type DataTabReuseMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
||
import { isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||
import { resolveTableDefaultSort, applyTableDefaultSortResult } from "@/lib/table/tableDefaultSort";
|
||
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
|
||
import { editableRowIdentifierColumns, physicalTablePrimaryKeys, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
|
||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||
import { uuid } from "@/lib/common/utils";
|
||
import { beginDataTabNavigation, endDataTabNavigation, isCurrentDataTabNavigation } from "@/lib/tabs/dataTabNavigationGeneration";
|
||
import { useSidebarDataOpenRuntime } from "@/composables/useSidebarDataOpenRuntime";
|
||
import { useConnectionStore } from "@/stores/connectionStore";
|
||
import { useQueryStore } from "@/stores/queryStore";
|
||
import { useSettingsStore } from "@/stores/settingsStore";
|
||
import type { ColumnInfo, TableInfoTab } from "@/types/database";
|
||
|
||
export type NavigationTarget = {
|
||
connectionId: string;
|
||
database: string;
|
||
catalog?: string;
|
||
schema?: string;
|
||
tableName: string;
|
||
tableType?: string;
|
||
comment?: string | null;
|
||
columnName?: string;
|
||
whereInput?: string;
|
||
};
|
||
|
||
function openMongoCollectionTarget(target: NavigationTarget, reuseMode: DataTabReuseMode) {
|
||
const connectionStore = useConnectionStore();
|
||
const queryStore = useQueryStore();
|
||
connectionStore.activeConnectionId = target.connectionId;
|
||
const tabTitle = `${target.database}.${target.tableName}`;
|
||
const sameCollectionTab = reuseMode === "always-new" ? undefined : queryStore.tabs.find((tab) => tab.mode === "mongo" && tab.connectionId === target.connectionId && tab.database === target.database && tab.sql === target.tableName);
|
||
const activeTab = queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId);
|
||
const reusableActiveTab = reuseMode === "active-tab" && !sameCollectionTab && canReuseActiveMongoTab(activeTab, target) ? activeTab : undefined;
|
||
const reusableTab = sameCollectionTab ?? reusableActiveTab;
|
||
const tabId = reusableTab?.id ?? queryStore.createTab(target.connectionId, target.database, tabTitle, "mongo", undefined, undefined, undefined, { forceNew: true });
|
||
if (reusableTab) {
|
||
queryStore.switchTab(tabId);
|
||
reusableTab.title = tabTitle;
|
||
}
|
||
queryStore.updateSql(tabId, target.tableName);
|
||
queryStore.setTableMeta(tabId, {
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
tableType: target.tableType || "TABLE",
|
||
columns: [],
|
||
primaryKeys: [],
|
||
});
|
||
}
|
||
|
||
async function openTableTarget(target: NavigationTarget, options: { tableInfoTab?: TableInfoTab } = {}) {
|
||
const connectionStore = useConnectionStore();
|
||
const queryStore = useQueryStore();
|
||
const settingsStore = useSettingsStore();
|
||
const pageLimit = tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
|
||
|
||
connectionStore.activeConnectionId = target.connectionId;
|
||
const config = connectionStore.getConfig(target.connectionId);
|
||
if (config?.db_type === "mongodb") {
|
||
openMongoCollectionTarget(target, settingsStore.editorSettings.dataTabReuseMode);
|
||
return;
|
||
}
|
||
const tableSchema = connectionObjectTreeNodeSchema(config, target.database, target.schema);
|
||
const tabTitle = target.catalog ? `${target.catalog}.${tableSchema || target.database}.${target.tableName}` : tableSchema ? `${tableSchema}.${target.tableName}` : target.tableName;
|
||
if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
||
await connectionStore.ensureConnected(target.connectionId);
|
||
const tabId = queryStore.createTab(target.connectionId, target.database || "default", tabTitle, "vector");
|
||
queryStore.updateSql(tabId, target.tableName);
|
||
return;
|
||
}
|
||
const tabId = queryStore.createTab(target.connectionId, target.database, tabTitle, "data", tableSchema, undefined, undefined, { forceNew: true });
|
||
const targetTab = queryStore.tabs.find((tab) => tab.id === tabId);
|
||
if (targetTab) {
|
||
targetTab.tableInfoTab = options.tableInfoTab;
|
||
targetTab.tableComment = target.comment;
|
||
}
|
||
// Stamp the new table identity synchronously so SQL rebuilds (refresh,
|
||
// filters, row count) never read a stale tableMeta from a reused tab or
|
||
// fall back to parsing the schema-qualified tab title (issue #3613).
|
||
queryStore.setTableMeta(tabId, {
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
tableType: target.tableType ?? "TABLE",
|
||
columns: [],
|
||
primaryKeys: [],
|
||
});
|
||
// 占位身份的行标识未知:真实元数据落地(下方 getColumns 成功后的 setTableMeta)
|
||
// 前编辑/保存保持禁用,防止数据查询先返回时整行 WHERE 保存路径短暂可用(#3727)
|
||
if (targetTab) targetTab.tableMetaPending = true;
|
||
// 立即作废旧执行代次:旧导航在途的 executeTabSql 结果按 executionId 比对
|
||
// 落地,仅置 isExecuting 不会替换 executionId,旧结果仍可能写入本 tab。
|
||
// preparationId 同时参与 isCurrentTarget:用户在准备期点击停止会清掉
|
||
// executionId,后续异步返回后不得再启动查询(同 refreshPreparationId 模式)
|
||
const preparationId = uuid();
|
||
queryStore.setExecutingWithId(tabId, preparationId);
|
||
// 侧边栏 openData 可能接管同一 tab:每次异步返回后必须校验(1)本次
|
||
// 导航仍是该 tab 的最新代次,(2)tab 未被其他流程改指别的
|
||
// 目标,(3)准备期 executionId 未被停止/接管清除。否则旧请求晚返回会用
|
||
// A 的元数据覆盖新目标 B 的占位并解除 pending,造成结果属于 B、写入目标
|
||
// 却是 A 的错位
|
||
const navigationToken = beginDataTabNavigation(tabId);
|
||
const isCurrentTarget = () =>
|
||
isCurrentDataTabNavigation(tabId, navigationToken) &&
|
||
canApplyDataTabMetadata(
|
||
queryStore.tabs.find((tab) => tab.id === tabId),
|
||
{
|
||
connectionId: target.connectionId,
|
||
database: target.database,
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
tableName: target.tableName,
|
||
},
|
||
);
|
||
// 仅首次 executeTabSql 前有效:executeTabSql 会替换 executionId、完成后清空,
|
||
// 之后的落地点(元数据/fallback)回到 isCurrentTarget 判定。
|
||
// isCancelling 必须同查:停止请求先同步置 isCancelling,等 cancelQuery 返回
|
||
// 才清 executionId——这个窗口内准备流程不得启动查询
|
||
const isPreparationCurrent = () => {
|
||
const currentTab = queryStore.tabs.find((tab) => tab.id === tabId);
|
||
return isCurrentTarget() && currentTab?.executionId === preparationId && !currentTab.isCancelling;
|
||
};
|
||
// 区分外层 catch 的判定:准备期(首次 executeTabSql 前)reject 用
|
||
// isPreparationCurrent——停止/接管后的迟到错误不得写入;首次执行之后
|
||
// preparationId 正常失效,回到 isCurrentTarget,真实错误仍要展示
|
||
let firstExecuteStarted = false;
|
||
|
||
try {
|
||
await connectionStore.ensureConnected(target.connectionId);
|
||
if (!isPreparationCurrent()) return;
|
||
if (!config) throw new Error("Connection config not found");
|
||
const metadataGenerationAtStart = connectionStore.metadataGenerationFor(target.connectionId, target.database);
|
||
const isCurrentGeneration = () => connectionStore.metadataGenerationFor(target.connectionId, target.database) === metadataGenerationAtStart;
|
||
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
||
const identifierQuote = connectionStore.connectionIdentifierQuote?.(target.connectionId);
|
||
const querySchema = metadataSchemaForConnection(config, target.database, tableSchema);
|
||
const targetTableType = target.tableType ?? "TABLE";
|
||
if (config.db_type === "neo4j") {
|
||
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
|
||
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, undefined, targetTableType);
|
||
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, physicalTablePrimaryKeys(columns), identifierQuote);
|
||
const sql = await buildTableSelectSql({
|
||
databaseType: effectiveDbType,
|
||
driverProfile: config.driver_profile,
|
||
identifierQuote,
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||
injectDefaultTimeSeriesWhere: true,
|
||
tableType: targetTableType,
|
||
columns: columns.map((column) => column.name),
|
||
primaryKeys,
|
||
orderBy: defaultSort.orderBy,
|
||
whereInput: target.whereInput,
|
||
limit: pageLimit,
|
||
});
|
||
if (!isPreparationCurrent() || !isCurrentGeneration()) return;
|
||
queryStore.updateSql(tabId, sql);
|
||
queryStore.setTableMeta(tabId, {
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
schema: tableSchema,
|
||
tableName: target.tableName,
|
||
tableType: targetTableType,
|
||
columns,
|
||
primaryKeys,
|
||
});
|
||
firstExecuteStarted = true;
|
||
if (targetTab) targetTab.orderByInput = defaultSort.orderBy;
|
||
await queryStore.executeTabSql(tabId, sql, { pagination: { limit: pageLimit, offset: 0 } });
|
||
if (isCurrentTarget() && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
|
||
return;
|
||
}
|
||
let eagerMetadata: Awaited<ReturnType<typeof loadTableMetadata>> | undefined;
|
||
if (effectiveDbType === "mysql" || effectiveDbType === "postgres" || (settingsStore.editorSettings.tableOpenSortMode ?? "none") !== "none") {
|
||
try {
|
||
eagerMetadata = await loadTableMetadata({
|
||
connectionId: target.connectionId,
|
||
database: target.database,
|
||
schema: querySchema,
|
||
tableName: target.tableName,
|
||
tableType: targetTableType,
|
||
databaseType: effectiveDbType ?? config.db_type,
|
||
driverProfile: config.driver_profile || config.db_type,
|
||
catalog: target.catalog,
|
||
});
|
||
} catch (error) {
|
||
// Metadata is needed for optional default sorting, but it must not
|
||
// prevent the data preview from opening. The later metadata refresh
|
||
// keeps the tab pending for edits and can retry independently.
|
||
console.warn("[DBX] unable to preload table metadata for default sort", error);
|
||
}
|
||
}
|
||
const eagerColumns = eagerMetadata?.metadata.columns ?? [];
|
||
const eagerPrimaryKeys = eagerMetadata?.metadata.primaryKeys ?? [];
|
||
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, physicalTablePrimaryKeys(eagerColumns, eagerMetadata?.metadata.indexes), identifierQuote);
|
||
const sql = await buildTableSelectSql({
|
||
databaseType: effectiveDbType,
|
||
driverProfile: config.driver_profile,
|
||
identifierQuote,
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||
tableType: targetTableType,
|
||
columns: eagerColumns.map((column) => column.name),
|
||
primaryKeys: eagerPrimaryKeys,
|
||
orderBy: defaultSort.orderBy,
|
||
...tableDataLargeValuePreviewOptions(effectiveDbType, eagerColumns, eagerPrimaryKeys, pageLimit),
|
||
whereInput: target.whereInput,
|
||
limit: pageLimit,
|
||
});
|
||
if (!isPreparationCurrent() || !isCurrentGeneration()) return;
|
||
queryStore.updateSql(tabId, sql);
|
||
queryStore.setTableMeta(tabId, {
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
tableType: targetTableType,
|
||
columns: eagerColumns,
|
||
primaryKeys: eagerPrimaryKeys,
|
||
});
|
||
firstExecuteStarted = true;
|
||
// 取消计数快照:isCancelling 是瞬态的(取消失败/查询先完成会被清掉),
|
||
// 比对计数才能跨越 executeTabSql 生命周期识别"执行期间用户请求过停止"
|
||
const cancelCountBeforeExecute = queryStore.tabs.find((tab) => tab.id === tabId)?.cancelRequestCount ?? 0;
|
||
if (targetTab) targetTab.orderByInput = defaultSort.orderBy;
|
||
await queryStore.executeTabSql(tabId, sql, { pagination: { limit: pageLimit, offset: 0 } });
|
||
if (!isCurrentTarget()) return;
|
||
// 首次查询被停止/失败(executeTabSql 以 Error 结果表达,不抛出)时,
|
||
// 后续不得自动启动第二次查询(synthetic row-id/TDengine 重查)——用户
|
||
// 停止了查询,流程不能替他再跑一次;元数据落地不受影响
|
||
const tabAfterFirstExecute = queryStore.tabs.find((tab) => tab.id === tabId);
|
||
const firstResult = tabAfterFirstExecute?.result;
|
||
const cancelRequestedDuringExecute = (tabAfterFirstExecute?.cancelRequestCount ?? 0) > cancelCountBeforeExecute;
|
||
const firstQueryFailed = cancelRequestedDuringExecute || tabAfterFirstExecute?.isCancelling === true || (firstResult !== undefined && isQueryExecutionErrorResult(firstResult));
|
||
if (!firstQueryFailed && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
|
||
// executeTabSql surfaces query failures as an "Error" result instead of throwing.
|
||
// A snapshot-less lake table fails the data preview above but its metadata still
|
||
// reads fine — retry with LIMIT 0 so the user sees the table structure (columns +
|
||
// empty grid) rather than a cryptic server error. The flag also skips the
|
||
// synthetic-row-id re-query below, which is another data read that would fail
|
||
// the same way on a snapshot-less table.
|
||
const fellBackToLimitZero = isNoSnapshotErrorResult(queryStore.tabs.find((tab) => tab.id === tabId)?.result);
|
||
if (fellBackToLimitZero) {
|
||
const emptySql = await buildTableSelectSql({
|
||
databaseType: effectiveDbType,
|
||
driverProfile: config.driver_profile,
|
||
identifierQuote,
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||
injectDefaultTimeSeriesWhere: true,
|
||
tableType: targetTableType,
|
||
whereInput: target.whereInput,
|
||
limit: 0,
|
||
});
|
||
if (!isCurrentTarget()) return;
|
||
queryStore.updateSql(tabId, emptySql);
|
||
await queryStore.executeTabSql(tabId, emptySql, { pagination: { limit: pageLimit, offset: 0 } });
|
||
if (!isCurrentTarget()) return;
|
||
}
|
||
try {
|
||
// 复用共享表元数据缓存(30s TTL + in-flight 去重)
|
||
const { metadata } = await loadTableMetadata({
|
||
connectionId: target.connectionId,
|
||
database: target.database,
|
||
schema: querySchema,
|
||
tableName: target.tableName,
|
||
tableType: targetTableType,
|
||
databaseType: effectiveDbType ?? config.db_type,
|
||
driverProfile: config.driver_profile || config.db_type,
|
||
catalog: target.catalog,
|
||
});
|
||
const columns = metadata.columns;
|
||
const primaryKeys = metadata.primaryKeys;
|
||
// 异步窗口内 tab 可能已被复用为其他目标:旧请求的元数据不得落地、
|
||
// 不得解除新目标的 pending
|
||
if (!isCurrentTarget() || !isCurrentGeneration()) return;
|
||
const useRowId = shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, targetTableType);
|
||
queryStore.setTableMeta(tabId, {
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
tableType: targetTableType,
|
||
columns,
|
||
primaryKeys,
|
||
});
|
||
if (!fellBackToLimitZero && !firstQueryFailed && (useRowId || config.db_type === "tdengine")) {
|
||
const newSql = await buildTableSelectSql({
|
||
databaseType: effectiveDbType,
|
||
driverProfile: config.driver_profile,
|
||
identifierQuote,
|
||
schema: tableSchema,
|
||
catalog: target.catalog,
|
||
database: target.database,
|
||
tableName: target.tableName,
|
||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||
injectDefaultTimeSeriesWhere: true,
|
||
tableType: targetTableType,
|
||
whereInput: target.whereInput,
|
||
primaryKeys,
|
||
columns: columns.map((column) => column.name),
|
||
includeRowId: true,
|
||
orderBy: defaultSort.orderBy,
|
||
limit: pageLimit,
|
||
});
|
||
if (!isCurrentTarget()) return;
|
||
queryStore.updateSql(tabId, newSql);
|
||
await queryStore.executeTabSql(tabId, newSql, { pagination: { limit: pageLimit, offset: 0 } });
|
||
if (isCurrentTarget() && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
|
||
}
|
||
} catch (reason) {
|
||
console.error("[DBX] ERROR fetching table metadata:", reason);
|
||
}
|
||
} catch (e: any) {
|
||
if (firstExecuteStarted ? !isCurrentTarget() : !isPreparationCurrent()) return;
|
||
queryStore.setErrorResult(tabId, e);
|
||
} finally {
|
||
endDataTabNavigation(tabId, navigationToken);
|
||
}
|
||
}
|
||
|
||
export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value: boolean }; showDatabaseSearchDialog: { value: boolean }; showDiagramDialog: { value: boolean } }) {
|
||
const connectionStore = useConnectionStore();
|
||
const queryStore = useQueryStore();
|
||
const settingsStore = useSettingsStore();
|
||
const { openData } = useSidebarDataOpenRuntime();
|
||
|
||
async function openObjectBrowserTableTarget(target: NavigationTarget) {
|
||
if (connectionStore.getConfig(target.connectionId)?.db_type === "mongodb") {
|
||
openMongoCollectionTarget(target, settingsStore.editorSettings.dataTabReuseMode);
|
||
return;
|
||
}
|
||
if (settingsStore.editorSettings.dataTabReuseMode === "always-new") {
|
||
await openTableTarget(target);
|
||
return;
|
||
}
|
||
connectionStore.activeConnectionId = target.connectionId;
|
||
const normalizedTableType = target.tableType?.trim().toUpperCase().replaceAll(" ", "_");
|
||
const nodeType = normalizedTableType === "VIEW" ? "view" : normalizedTableType === "MATERIALIZED_VIEW" ? "materialized_view" : "table";
|
||
await openData(
|
||
{
|
||
id: uuid(),
|
||
label: target.tableName,
|
||
type: nodeType,
|
||
connectionId: target.connectionId,
|
||
database: target.database,
|
||
schema: target.schema,
|
||
catalog: target.catalog,
|
||
tableType: target.tableType,
|
||
comment: target.comment,
|
||
},
|
||
undefined,
|
||
"default",
|
||
{ reuseMode: settingsStore.editorSettings.dataTabReuseMode },
|
||
);
|
||
}
|
||
|
||
async function openLineageTarget(target: NavigationTarget) {
|
||
dialogs.showFieldLineageDialog.value = false;
|
||
await openTableTarget(target);
|
||
}
|
||
|
||
async function openDatabaseSearchTarget(target: NavigationTarget) {
|
||
dialogs.showDatabaseSearchDialog.value = false;
|
||
await openTableTarget(target);
|
||
}
|
||
|
||
async function openDiagramTarget(target: NavigationTarget) {
|
||
dialogs.showDiagramDialog.value = false;
|
||
await openTableTarget(target);
|
||
}
|
||
|
||
async function onStructureEditorSaved(reloadData: () => Promise<void>, toast: (msg: string, duration?: number) => void, context: { connectionId: string; database: string; schema?: string; catalog?: string; tableName: string }, commentChanged?: boolean) {
|
||
if (!context.tableName) {
|
||
try {
|
||
await connectionStore.refreshObjectListTreeNode(context.connectionId, context.database, context.schema || undefined);
|
||
} catch {}
|
||
return;
|
||
}
|
||
if (commentChanged) {
|
||
try {
|
||
await connectionStore.refreshObjectListTreeNode(context.connectionId, context.database, context.schema || undefined);
|
||
} catch {}
|
||
}
|
||
connectionStore.invalidateCompletionTableCache(context.connectionId, context.database, context.tableName, context.schema, context.catalog);
|
||
queryStore.invalidateTableStructure(context.connectionId, context.database, context.schema, context.tableName);
|
||
// 结构已变更:无论是否有打开的 data tab 都必须作废共享元数据缓存,否则
|
||
// 其它 loadTableMetadata 消费者最长 30 秒拿到旧列。不带 schema/catalog
|
||
// 维度(宁可多废,schema 形态在各消费点可能不同)
|
||
invalidateTableMetadataCache({ connectionId: context.connectionId, database: context.database, tableName: context.tableName });
|
||
const matchingDataTabs = queryStore.tabs.filter((tab) => tab.mode === "data" && tab.connectionId === context.connectionId && tab.database === context.database && tab.tableMeta?.tableName === context.tableName && (tab.schema || tab.tableMeta.schema || "") === (context.schema || ""));
|
||
// 同一 catalog 只强制加载一次,结果分发给全部匹配 tab
|
||
const loadedByCatalog = new Map<string, { columns: ColumnInfo[]; primaryKeys: string[] }>();
|
||
for (const tab of matchingDataTabs) {
|
||
try {
|
||
const connection = connectionStore.getConfig(tab.connectionId);
|
||
// 捕获不可变目标身份:await 期间 tab 可能被导航复用改指其他表,
|
||
// 共享缓存结果落地前仍必须复核,不能解除新目标的 pending
|
||
const capturedMeta = tab.tableMeta!;
|
||
const capturedSchema = tab.schema || capturedMeta.schema;
|
||
const capturedTarget = { connectionId: tab.connectionId, database: tab.database, schema: capturedSchema, catalog: capturedMeta.catalog, tableName: capturedMeta.tableName };
|
||
const metadataGenerationAtStart = connectionStore.metadataGenerationFor(tab.connectionId, tab.database);
|
||
const metadataSchema = metadataSchemaForConnection(connection, tab.database, capturedSchema);
|
||
// 分组含 tableType:主键计算依赖它,不同 tableType 不能共享加载结果
|
||
const catalogKey = `${capturedMeta.catalog ?? ""}\u0000${capturedMeta.tableType ?? ""}`;
|
||
let metadata = loadedByCatalog.get(catalogKey);
|
||
if (!metadata) {
|
||
metadata = (
|
||
await loadTableMetadata({
|
||
connectionId: tab.connectionId,
|
||
database: tab.database,
|
||
schema: metadataSchema,
|
||
tableName: capturedMeta.tableName,
|
||
tableType: capturedMeta.tableType,
|
||
databaseType: effectiveDatabaseTypeForConnection(connection) ?? connection?.db_type ?? "",
|
||
driverProfile: connection?.driver_profile || connection?.db_type,
|
||
catalog: capturedMeta.catalog,
|
||
force: true,
|
||
})
|
||
).metadata;
|
||
loadedByCatalog.set(catalogKey, metadata);
|
||
}
|
||
const currentTab = queryStore.tabs.find((item) => item.id === tab.id);
|
||
if (connectionStore.metadataGenerationFor(tab.connectionId, tab.database) !== metadataGenerationAtStart) continue;
|
||
if (!canApplyDataTabMetadata(currentTab, capturedTarget)) continue;
|
||
queryStore.setTableMeta(tab.id, {
|
||
...capturedMeta,
|
||
schema: capturedSchema,
|
||
columns: metadata.columns,
|
||
primaryKeys: metadata.primaryKeys,
|
||
});
|
||
if (tab.id === queryStore.activeTabId) await reloadData();
|
||
} catch (e: any) {
|
||
toast(e?.message || String(e), 5000);
|
||
}
|
||
}
|
||
}
|
||
|
||
return { openLineageTarget, openDatabaseSearchTarget, openDiagramTarget, openObjectBrowserTableTarget, onStructureEditorSaved, openTableTarget };
|
||
}
|