fix(sidebar): locate reloaded data tables

Closes #8413
This commit is contained in:
t8y2
2026-09-09 00:38:56 +08:00
parent 03cfeba8f7
commit 3d450a42cc
7 changed files with 97 additions and 17 deletions
@@ -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<typeof store.getConfig>): 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;
@@ -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。
@@ -120,7 +120,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
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<QueryTab | undefined>)
}
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,
@@ -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);
});
});
@@ -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,
};
}
@@ -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 ?? [],
+1 -1
View File
@@ -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,