From 3d450a42cc358ddb8f6db85dee70afc8e95dcc22 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 9 Sep 2026 00:15:19 +0800 Subject: [PATCH] fix(sidebar): locate reloaded data tables Closes #8413 --- .../src/components/sidebar/ConnectionTree.vue | 45 ++++++++++++++----- .../__tests__/useDataGridActions.spec.ts | 28 ++++++++++++ .../src/composables/useDataGridActions.ts | 6 ++- .../sidebar/sidebarActiveTabTarget.spec.ts | 23 ++++++++++ .../src/lib/sidebar/sidebarActiveTabTarget.ts | 8 ++-- .../desktop/src/lib/table/tableDataTabMeta.ts | 2 +- apps/desktop/src/stores/queryStore.ts | 2 +- 7 files changed, 97 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 0c2e70dc1e..ce71f33955 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -37,7 +37,7 @@ import { formatSidebarTableCopyText } from "@/lib/sidebar/sidebarTableNameCopy"; import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSelection"; import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch"; import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; -import { connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { connectionObjectTreeNodeSchema, connectionShouldDiscoverJdbcSchemas, connectionUsesConnectionRootSchemaMode, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { activeTabSidebarTarget, findSidebarConnectionNode, @@ -1582,8 +1582,10 @@ async function locateTabInSidebar(tab: QueryTab | undefined | null, align: Sideb const config = connId ? store.getConfig(connId) : undefined; const cursorCandidate = locatesSavedSql ? null : queryCursorTableCandidate(tab, effectiveDatabaseTypeForConnection(config)); + const tabTableCandidate = locatesSavedSql || cursorCandidate ? null : tableLocateCandidateFromTarget(tabTarget, config); + const locateTableCandidate = cursorCandidate ?? tabTableCandidate; const fallbackTarget = locatesSavedSql ? tabTarget : (queryContextTargetFromCandidate(tab, cursorCandidate) ?? tabTarget); - const initialTarget = cursorCandidate ? tableTargetFromCandidate(cursorCandidate) : fallbackTarget; + const initialTarget = locateTableCandidate ? tableTargetFromCandidate(locateTableCandidate) : fallbackTarget; if (!initialTarget) return; // Ensure the tree is loaded deep enough to contain the preferred target. @@ -1608,7 +1610,7 @@ async function locateTabInSidebar(tab: QueryTab | undefined | null, align: Sideb clearSearchScopeFilter(); } - let target = resolveLoadedLocateTarget(initialTarget, cursorCandidate); + let target = resolveLoadedLocateTarget(initialTarget, locateTableCandidate); let nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null; if (!nodePath && !locatesSavedSql) { // The first load may have served a stale schema cache whose async refresh @@ -1616,13 +1618,13 @@ async function locateTabInSidebar(tab: QueryTab | undefined | null, align: Sideb // table isn't in the tree yet. Force a synchronous reload and retry once so // locate reaches the table, not just the database (issue #715). await ensureTreeLoadedForTarget(treeLoadTarget, { force: true }); - target = resolveLoadedLocateTarget(initialTarget, cursorCandidate); + target = resolveLoadedLocateTarget(initialTarget, locateTableCandidate); nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null; } - if (!nodePath && cursorCandidate) { - await store.loadTableForLocate(cursorCandidate); - target = resolveLoadedLocateTarget(initialTarget, cursorCandidate); + if (!nodePath && locateTableCandidate) { + await store.loadTableForLocate(locateTableCandidate); + target = resolveLoadedLocateTarget(initialTarget, locateTableCandidate); nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null; } @@ -1678,6 +1680,17 @@ function tableTargetFromCandidate(candidate: QueryCursorTableCandidate): ActiveT }; } +function tableLocateCandidateFromTarget(target: ActiveTabSidebarTarget | null, config: ReturnType): QueryCursorTableCandidate | null { + if (target?.type !== "table") return null; + const database = connectionUsesConnectionRootSchemaMode(config) && target.schema ? target.schema : target.database; + return { + connectionId: target.connectionId, + database, + schema: target.schema, + tableName: target.tableName, + }; +} + function resolveLoadedLocateTarget(target: ActiveTabSidebarTarget, candidate: QueryCursorTableCandidate | null): ActiveTabSidebarTarget | null { if (!candidate) return target; return findLoadedTableTargetForCandidate(store.treeNodes, candidate); @@ -1741,12 +1754,24 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?: } // Find the database node + const targetSchema = "schema" in target ? target.schema : undefined; + const effectiveDbType = effectiveDatabaseTypeForConnection(config); + if (target.type === "table" && connectionUsesConnectionRootSchemaMode(config)) { + const schemaName = targetSchema || target.database; + if (!schemaName) return; + const schemaNode = findSchemaNode(store.treeNodes, connId, schemaName, schemaName); + if (!schemaNode) return; + if (force || !schemaNode.children || schemaNode.children.length === 0) { + await store.loadTables(connId, schemaNode.database || schemaName, schemaNode.schema ?? schemaName, loadOptions); + } + await ensureTableObjectGroupsLoaded({ ...target, database: schemaNode.database || schemaName, schema: schemaNode.schema ?? schemaName }, loadOptions); + return; + } + const dbNode = findDatabaseNode(store.treeNodes, connId, target.database, targetCatalog, usesExactCatalogScope); if (!dbNode) return; - const targetSchema = "schema" in target ? target.schema : undefined; const databaseChildrenLoaded = !!dbNode.children && dbNode.children.length > 0; - const effectiveDbType = effectiveDatabaseTypeForConnection(config); - const usesSchemaTree = usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config); + const usesSchemaTree = (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) || connectionShouldDiscoverJdbcSchemas(config); const shouldLoadSchemaTables = target.type === "table" && !!targetSchema && usesSchemaTree; if (!force && databaseChildrenLoaded && !shouldLoadSchemaTables) return; diff --git a/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts index 22a34b1fe0..f5d167df19 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts @@ -214,6 +214,34 @@ describe("useDataGridActions", () => { expect(tab.orderByInput).toBe('"name" DESC'); }); + it("preserves a restored data tab's source database when rebuilding stale metadata", async () => { + mocks.getConfig.mockReturnValue({ id: "dameng-1", db_type: "dameng" }); + const tab = tableDataTab({ + connectionId: "dameng-1", + database: "SERVICE_DB", + schema: "APP_OWNER", + title: "ORDERS", + sql: "SELECT * FROM APP_OWNER.ORDERS", + tableMetaUpdatedAt: undefined, + tableMeta: { + database: "APP_OWNER", + schema: "APP_OWNER", + tableName: "ORDERS", + tableType: "TABLE", + columns: [{ name: "ID", data_type: "INTEGER", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["ID"], + }, + }); + mocks.tabs.push(tab); + const actions = useDataGridActions(computed(() => tab)); + + await actions.onReloadData(tab.id, undefined, undefined, "", ""); + + expect(mocks.getColumns).toHaveBeenCalledWith("dameng-1", "APP_OWNER", "APP_OWNER", "ORDERS", undefined); + expect(mocks.setTableMeta).toHaveBeenCalledWith("tab-1", expect.objectContaining({ database: "APP_OWNER", schema: "APP_OWNER", tableName: "ORDERS" })); + expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ database: "APP_OWNER", schema: "APP_OWNER", tableName: "ORDERS" })); + }); + it("still clears the stored filter when a mounted grid refreshes with an emptied WHERE input", async () => { // 对照:DataGrid 的 currentWhereInput() 在用户清空筛选时返回 undefined, // 所以 onReloadData 里不能无条件回退到 tab.whereInput。 diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index 0370e5729c..099b10b288 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -120,7 +120,7 @@ export function useDataGridActions(activeTab: ComputedRef) const target = { tabId: tab.id, connectionId: tab.connectionId, - database: tab.database, + database: tableMeta.database ?? tab.database, catalog: tableMeta.catalog, schema: tableMeta.schema, tableName: tableMeta.tableName, @@ -161,13 +161,15 @@ export function useDataGridActions(activeTab: ComputedRef) } const current = queryStore.tabs.find((item) => item.id === target.tabId); const currentMeta = current ? tableMetaForDataTab(current) : undefined; - if (!current || current.mode !== "data" || current.connectionId !== target.connectionId || current.database !== target.database || currentMeta?.tableName !== target.tableName || (currentMeta.schema ?? "") !== (target.schema ?? "") || (currentMeta.catalog ?? "") !== (target.catalog ?? "")) { + const currentSourceDatabase = currentMeta?.database ?? current?.database; + if (!current || current.mode !== "data" || current.connectionId !== target.connectionId || currentSourceDatabase !== target.database || currentMeta?.tableName !== target.tableName || (currentMeta.schema ?? "") !== (target.schema ?? "") || (currentMeta.catalog ?? "") !== (target.catalog ?? "")) { console.info("[DBX][reloadData:metadata:stale-tab]", { traceId: trace?.traceId, elapsed: trace?.elapsed(), table: target.tableName }); return false; } const primaryKeys = metadata.primaryKeys; queryStore.setTableMeta(target.tabId, { catalog: target.catalog, + database: target.database, schema: target.schema, tableName: target.tableName, tableType: target.tableType, diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarActiveTabTarget.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarActiveTabTarget.spec.ts index e9c4a214a8..aa2f51f1db 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/sidebarActiveTabTarget.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarActiveTabTarget.spec.ts @@ -27,4 +27,27 @@ describe("sidebar active-tab targets", () => { expect(matchesTarget(icebergDatabase, target!)).toBe(false); expect(matchesTarget(defaultDatabase, target!)).toBe(false); }); + + it("uses data-tab table metadata as the locate identity after a reload", () => { + const target = activeTabSidebarTarget({ + mode: "data", + connectionId: "dm-1", + database: "SERVICE_DB", + schema: "APP_OWNER", + title: "ORDERS", + sql: "SELECT * FROM APP_OWNER.ORDERS", + tableMeta: { + database: "APP_OWNER", + schema: "APP_OWNER", + tableName: "ORDERS", + tableType: "TABLE", + columns: [{ name: "ID", data_type: "INTEGER", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["ID"], + }, + } as QueryTab); + + expect(target).toEqual({ type: "table", connectionId: "dm-1", database: "APP_OWNER", schema: "APP_OWNER", tableName: "ORDERS" }); + expect(matchesTarget({ id: "dm-1:APP_OWNER:APP_OWNER:tables:ORDERS", label: "ORDERS", type: "table", connectionId: "dm-1", database: "APP_OWNER", schema: "APP_OWNER" } as TreeNode, target!)).toBe(true); + expect(matchesTarget({ id: "dm-1:SERVICE_DB", label: "SERVICE_DB", type: "database", connectionId: "dm-1", database: "SERVICE_DB" } as TreeNode, target!)).toBe(false); + }); }); diff --git a/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts b/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts index 89621d651d..73b604b79c 100644 --- a/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts +++ b/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts @@ -1,4 +1,5 @@ import { SIDEBAR_TREE_ROW_HEIGHT, type FlatTreeNode } from "@/composables/useFlatTree"; +import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta"; import type { QueryTab, TreeNode } from "@/types/database"; export type ActiveTabSidebarTarget = @@ -114,13 +115,14 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active } if (tab.mode === "data") { - const tableName = tab.tableMeta?.tableName || tab.title; + const tableMeta = tableMetaForDataTab(tab); + const tableName = tableMeta?.tableName || tab.title; if (!tableName) return null; return { type: "table", connectionId: tab.connectionId, - database: tab.database, - schema: tab.tableMeta?.schema ?? tab.schema, + database: tableMeta?.database ?? tab.database, + schema: tableMeta?.schema ?? tab.schema, tableName, }; } diff --git a/apps/desktop/src/lib/table/tableDataTabMeta.ts b/apps/desktop/src/lib/table/tableDataTabMeta.ts index 00d92dbc9b..f87464d97c 100644 --- a/apps/desktop/src/lib/table/tableDataTabMeta.ts +++ b/apps/desktop/src/lib/table/tableDataTabMeta.ts @@ -35,7 +35,7 @@ export function tableMetaForDataTab(tab: QueryTab | undefined): DataTabTableMeta // Keep filters usable when the table identity loaded but its column metadata did not. return { ...tab.tableMeta, - schema: tab.schema, + schema: tab.tableMeta?.schema ?? tab.schema, tableName, columns: (tab.result?.columns ?? []).map(fallbackColumnInfo), primaryKeys: tab.tableMeta?.primaryKeys ?? [], diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 2fcc424dda..733911cd78 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -3873,7 +3873,7 @@ export const useQueryStore = defineStore("query", () => { const metadataGenerationAtStart = connectionGeneration; const reloadedMetadata = await loadTableMetadata({ connectionId: tab.connectionId, - database: tab.database, + database: tableMeta.database ?? tab.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType,