From a9e76dc130a2d5f6a187576aedd7d00ce55ba53b Mon Sep 17 00:00:00 2001 From: jischeng <49861575+jischeng@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:20:12 +0800 Subject: [PATCH] fix(doris): preserve catalog in metadata views --- apps/desktop/src/App.vue | 9 ++++-- apps/desktop/src/components/grid/DataGrid.vue | 2 +- .../src/components/layout/ContentArea.vue | 3 +- .../src/components/objects/DdlViewDialog.vue | 3 +- .../src/components/sidebar/ConnectionTree.vue | 1 + .../src/components/sidebar/TreeItem.vue | 12 ++++---- .../structure/TableStructureEditor.vue | 27 ++++++++++-------- .../contentAreaObjectBrowserCatalog.spec.ts | 28 +++++++++++++++++++ .../queryStore.database-open.spec.ts | 13 +++++++++ apps/desktop/src/stores/queryStore.ts | 2 +- 10 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/table/contentAreaObjectBrowserCatalog.spec.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 4d652d9dd..6622d51f8 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -162,7 +162,7 @@ const cursorPos = ref(0); const formatSqlRequest = ref<{ id: number; tabId: string } | null>(null); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const newQueryContextSource = ref<"tab" | "sidebar">("tab"); -const queryEditorDdlTarget = ref<{ connectionId: string; database: string; schema?: string; tableName: string; objectType?: ObjectSourceKind } | null>(null); +const queryEditorDdlTarget = ref<{ connectionId: string; database: string; catalog?: string; schema?: string; tableName: string; objectType?: ObjectSourceKind } | null>(null); const queryEditorObjectSourceTarget = ref<{ connectionId: string; database: string; schema?: string; name: string; objectType: ObjectSourceKind; initialEditing: boolean } | null>(null); const showSaveSqlDialog = ref(false); const saveSqlName = ref(""); @@ -1250,11 +1250,13 @@ function tableTargetFromActiveTab(table: string | SqlObjectNavigationTarget) { const tab = activeTab.value; if (!tab) return null; const connectionId = tab.connectionId; + const catalog = tab.tableMeta?.catalog || tab.catalog; if (typeof table !== "string") { // Structured targets already separate qualifiers; reparsing would corrupt quoted object names that contain dots. return { connectionId, database: table.database || tab.database, + catalog, schema: table.schema || tab.schema, tableName: table.name, tableType: table.type ? sqlObjectNavigationTableType(table) : undefined, @@ -1280,7 +1282,7 @@ function tableTargetFromActiveTab(table: string | SqlObjectNavigationTarget) { } } - return { connectionId, database, schema, tableName: rawTableName, tableType: undefined }; + return { connectionId, database, catalog, schema, tableName: rawTableName, tableType: undefined }; } async function onClickTable(table: SqlObjectNavigationTarget) { @@ -1321,7 +1323,7 @@ function onEditTableStructure(table: SqlObjectNavigationTarget) { const target = tableTargetFromActiveTab(table); // Keep view-like objects out of the table editor even if a stale menu dispatches this event. if (!target || sqlObjectNavigationSourceKind(table)) return; - queryStore.openTableStructure(target.connectionId, target.database, target.schema, target.tableName); + queryStore.openTableStructure(target.connectionId, target.database, target.schema, target.tableName, undefined, undefined, target.catalog); } async function onOpenObjectSource(table: SqlObjectNavigationTarget, initialEditing: boolean) { @@ -2286,6 +2288,7 @@ onUnmounted(() => { v-model:open="showQueryEditorDdlDialog" :connection-id="queryEditorDdlTarget.connectionId" :database="queryEditorDdlTarget.database" + :catalog="queryEditorDdlTarget.catalog" :schema="queryEditorDdlTarget.schema" :table-name="queryEditorDdlTarget.tableName" :object-type="queryEditorDdlTarget.objectType" diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 3f39fbfe1..f115b43a0 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -6744,7 +6744,7 @@ function copyDdl() { function openTableStructureEditor() { if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !canOpenTableStructureEditor.value) return; - queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName, activeTableInfoTab.value); + queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName, activeTableInfoTab.value, undefined, props.tableMeta.catalog); } function toggleDdlWrap() { diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 96a645eb2..fff60dc8b 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -1475,7 +1475,6 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand :database-type="activeEffectiveDatabaseType" :connection-id="activeTab.connectionId" :database="activeTab.database" - :catalog="activeTab.objectBrowser?.catalog" :execution-database="activeDataTabExecutionDatabase" :table-meta="activeDataTabTableMeta" :table-info-tab="activeTab.tableInfoTab" @@ -1587,6 +1586,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand :key="`${activeTab.id}-${activeTab.objectBrowser?.schema || ''}`" :connection="activeConnection" :database="activeTab.database" + :catalog="activeTab.objectBrowser?.catalog" :schema="activeTab.objectBrowser?.schema" :viewport="activeTab.objectBrowser?.viewport" @open-table="emit('openObjectTable', $event)" @@ -1603,6 +1603,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand :key="activeTab.id" :connection-id="activeTab.connectionId" :database="activeTab.database" + :catalog="activeTab.catalog" :schema="activeTab.schema" :table-name="activeTab.structureTableName || ''" :initial-tab="activeTab.structureInitialTab" diff --git a/apps/desktop/src/components/objects/DdlViewDialog.vue b/apps/desktop/src/components/objects/DdlViewDialog.vue index e1ba067e7..73690bb09 100644 --- a/apps/desktop/src/components/objects/DdlViewDialog.vue +++ b/apps/desktop/src/components/objects/DdlViewDialog.vue @@ -21,6 +21,7 @@ const props = withDefaults( open: boolean; connectionId: string; database: string; + catalog?: string; schema?: string; tableName: string; objectType?: ObjectSourceKind; @@ -60,7 +61,7 @@ watch( ddlLoading.value = true; try { const schema = props.schema || props.database; - const ddl = await api.getTableDdl(props.connectionId, props.database, schema, props.tableName, props.objectType); + const ddl = await api.getTableDdl(props.connectionId, props.database, schema, props.tableName, props.objectType, props.catalog); ddlContent.value = await formatSqlForDisplay(ddl, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter); } catch (e: any) { ddlError.value = e?.message || String(e); diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index cc83dff08..c86d72dc8 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -1416,6 +1416,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes }); v-model:open="sidebarDdlOpen" :connection-id="sidebarDdlTarget.connectionId!" :database="sidebarDdlTarget.database!" + :catalog="sidebarDdlTarget.catalog" :schema="sidebarDdlTarget.schema" :table-name="sidebarDdlTarget.label" :object-type="tableDdlObjectTypeForSidebarNode(sidebarDdlTarget.type)" diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index eb12d0e2d..8e01763c2 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1802,9 +1802,9 @@ async function generateDdlTemplate() { const schema = node.schema || node.database; let ddl: string; if (node.type === "table") { - ddl = await api.getTableDdl(node.connectionId, node.database, schema, node.label); + ddl = await api.getTableDdl(node.connectionId, node.database, schema, node.label, undefined, node.catalog); } else if (node.type === "materialized_view") { - ddl = await api.getTableDdl(node.connectionId, node.database, schema, node.label, "MATERIALIZED_VIEW"); + ddl = await api.getTableDdl(node.connectionId, node.database, schema, node.label, "MATERIALIZED_VIEW", node.catalog); } else { const result = await api.getObjectSource(node.connectionId, node.database, schema, node.label, "VIEW"); ddl = await buildViewDdl({ @@ -3707,7 +3707,7 @@ async function exportStructure() { const parts: string[] = []; for (const target of targets) { await connectionStore.ensureConnected(target.connectionId); - const ddl = await api.getTableDdl(target.connectionId, target.database, target.schema || target.database, target.label, tableDdlObjectTypeForNode(target.type)); + const ddl = await api.getTableDdl(target.connectionId, target.database, target.schema || target.database, target.label, tableDdlObjectTypeForNode(target.type), target.catalog); parts.push(ddl.trim()); } structurePreviewSql.value = joinExportedDdls(parts); @@ -4204,19 +4204,19 @@ function openStructureEditor() { const node = props.node; if (!node.connectionId || !node.database) return; if (node.type === "table") { - queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.label); + queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.label, undefined, undefined, node.catalog); return; } if (node.type === "column" && node.tableName) { const columnName = tableChildDropObjectName(node).trim(); if (!columnName) return; - queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "columns", { kind: "column", name: columnName }); + queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "columns", { kind: "column", name: columnName }, node.catalog); return; } if (node.type === "index" && node.tableName) { const indexName = tableChildDropObjectName(node).trim(); if (!indexName) return; - queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "indexes", { kind: "index", name: indexName }); + queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "indexes", { kind: "index", name: indexName }, node.catalog); } } diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 78d361bef..29a507cae 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -109,6 +109,7 @@ const previewSqlText = computed(() => joinSqlStatementsForScript(pendingStatemen const props = defineProps<{ connectionId: string; database: string; + catalog?: string; schema?: string; tableName: string; initialTab?: TableInfoTab; @@ -153,7 +154,7 @@ async function fetchDdl() { if (!props.connectionId || !props.database || !props.tableName || ddlFetched.value || !tableMetadataCapabilities.value.ddl) return; ddlLoading.value = true; try { - const ddl = await api.getTableDdl(props.connectionId, props.database, metadataSchema.value, props.tableName); + const ddl = await api.getTableDdl(props.connectionId, props.database, metadataSchema.value, props.tableName, undefined, props.catalog); ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter); ddlFetched.value = true; } catch (e: any) { @@ -958,6 +959,7 @@ async function hydrateRestoredDraftFromDatabase() { if (!needsColumnDraftMetadataHydration() || hydratingRestoredDraft) return; const connectionId = props.connectionId; const database = props.database; + const catalog = props.catalog; const schema = metadataSchema.value; const tableName = props.tableName; if (!connectionId || !database || !tableName) return; @@ -966,10 +968,10 @@ async function hydrateRestoredDraftFromDatabase() { let shouldRefreshPreview = false; try { await store.ensureConnected(connectionId); - let nextColumns = await api.getColumns(connectionId, database, schema, tableName); + let nextColumns = await api.getColumns(connectionId, database, schema, tableName, catalog); if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) { try { - const ddl = await api.getTableDdl(connectionId, database, schema, tableName); + const ddl = await api.getTableDdl(connectionId, database, schema, tableName, undefined, catalog); ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter); ddlFetched.value = true; nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl); @@ -1220,12 +1222,12 @@ function setSecondaryMetadataLoading(scope: StructureRefreshScope, value: boolea if (scope.triggers && tableMetadataCapabilities.value.triggers) triggersLoading.value = value; } -async function fetchTableCommentValue(connectionId: string, database: string, schema: string, tableName: string): Promise { +async function fetchTableCommentValue(connectionId: string, database: string, schema: string, tableName: string, catalog?: string): Promise { try { - return (await api.getTableComment(connectionId, database, schema, tableName)) || ""; + return (await api.getTableComment(connectionId, database, schema, tableName, catalog)) || ""; } catch { try { - const tables = await api.listTables(connectionId, database, schema); + const tables = await api.listTables(connectionId, database, schema, undefined, undefined, undefined, undefined, catalog); const table = tables.find((t) => t.name.toLowerCase() === tableName.toLowerCase() && t.table_type !== "VIEW"); return table?.comment || ""; } catch { @@ -1237,6 +1239,7 @@ async function fetchTableCommentValue(connectionId: string, database: string, sc async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean; damengLengthUnitsAfterSave?: ReadonlyMap } = {}) { const connectionId = props.connectionId; const database = props.database; + const catalog = props.catalog; const schema = metadataSchema.value; const tableName = props.tableName; if (!connectionId || !database || !tableName) return; @@ -1249,17 +1252,17 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL try { await store.ensureConnected(connectionId); - const columnsPromise = scope.columns ? api.getColumns(connectionId, database, schema, tableName) : Promise.resolve(undefined); - const indexesPromise = scope.indexes ? (tableMetadataCapabilities.value.indexes ? api.listIndexes(connectionId, database, schema, tableName).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); - const foreignKeysPromise = scope.foreignKeys ? (tableMetadataCapabilities.value.foreignKeys ? api.listForeignKeys(connectionId, database, schema, tableName).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); - const triggersPromise = scope.triggers ? (tableMetadataCapabilities.value.triggers ? api.listTriggers(connectionId, database, schema, tableName).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); - const tableCommentPromise = scope.tableComment && structureCapabilities.value.comment ? fetchTableCommentValue(connectionId, database, schema, tableName) : Promise.resolve(undefined); + const columnsPromise = scope.columns ? api.getColumns(connectionId, database, schema, tableName, catalog) : Promise.resolve(undefined); + const indexesPromise = scope.indexes ? (tableMetadataCapabilities.value.indexes ? api.listIndexes(connectionId, database, schema, tableName, catalog).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); + const foreignKeysPromise = scope.foreignKeys ? (tableMetadataCapabilities.value.foreignKeys ? api.listForeignKeys(connectionId, database, schema, tableName, catalog).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); + const triggersPromise = scope.triggers ? (tableMetadataCapabilities.value.triggers ? api.listTriggers(connectionId, database, schema, tableName, catalog).catch(() => []) : Promise.resolve([])) : Promise.resolve(undefined); + const tableCommentPromise = scope.tableComment && structureCapabilities.value.comment ? fetchTableCommentValue(connectionId, database, schema, tableName, catalog) : Promise.resolve(undefined); let nextColumns = await columnsPromise; if (nextColumns) { if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) { try { - const ddl = await api.getTableDdl(connectionId, database, schema, tableName); + const ddl = await api.getTableDdl(connectionId, database, schema, tableName, undefined, catalog); ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter); ddlFetched.value = true; nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl); diff --git a/apps/desktop/src/lib/__tests__/table/contentAreaObjectBrowserCatalog.spec.ts b/apps/desktop/src/lib/__tests__/table/contentAreaObjectBrowserCatalog.spec.ts new file mode 100644 index 000000000..45a1b37de --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/contentAreaObjectBrowserCatalog.spec.ts @@ -0,0 +1,28 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8"); +const connectionTreeSource = readFileSync(new URL("../../../components/sidebar/ConnectionTree.vue", import.meta.url), "utf8"); +const ddlViewDialogSource = readFileSync(new URL("../../../components/objects/DdlViewDialog.vue", import.meta.url), "utf8"); + +function openingTag(source: string, componentName: string): string { + return source.match(new RegExp(`<${componentName}\\b[\\s\\S]*?>`))?.[0] ?? ""; +} + +describe("ContentArea external catalog wiring", () => { + it("passes the object browser catalog to ObjectBrowser", () => { + expect(openingTag(contentAreaSource, "ObjectBrowser")).toContain(':catalog="activeTab.objectBrowser?.catalog"'); + }); + + it("does not attach object browser state to DataGrid", () => { + expect(openingTag(contentAreaSource, "DataGrid")).not.toContain("activeTab.objectBrowser?.catalog"); + }); + + it("passes the sidebar catalog to the DDL dialog", () => { + expect(openingTag(connectionTreeSource, "SidebarDdlViewDialog")).toContain(':catalog="sidebarDdlTarget.catalog"'); + }); + + it("forwards the DDL dialog catalog to the metadata API", () => { + expect(ddlViewDialogSource).toMatch(/api\.getTableDdl\([\s\S]*?props\.objectType, props\.catalog\)/); + }); +}); diff --git a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts index 8350367e3..57c47d219 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts @@ -69,6 +69,19 @@ describe("queryStore database open state", () => { expect(store.openObjectBrowser("doris-1", "default", undefined, "iceberg_catalog")).toBe(icebergTabId); }); + it("keeps external catalog structure editors isolated", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + + const icebergTabId = store.openTableStructure("doris-1", "sales", undefined, "orders", undefined, undefined, "iceberg_catalog"); + const hiveTabId = store.openTableStructure("doris-1", "sales", undefined, "orders", undefined, undefined, "hive_catalog"); + + expect(hiveTabId).not.toBe(icebergTabId); + expect(store.tabs.find((tab) => tab.id === icebergTabId)?.catalog).toBe("iceberg_catalog"); + expect(store.tabs.find((tab) => tab.id === hiveTabId)?.catalog).toBe("hive_catalog"); + expect(store.openTableStructure("doris-1", "sales", undefined, "orders", undefined, undefined, "iceberg_catalog")).toBe(icebergTabId); + }); + it("closes data and structure tabs for a dropped table object", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 975022556..aafedcc5b 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1344,7 +1344,7 @@ export const useQueryStore = defineStore("query", () => { function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget, catalog?: string) { const resolvedTableName = tableName || ""; if (resolvedTableName) { - const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName); + const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.catalog || "") === (catalog || "") && (tab.structureTableName || "") === resolvedTableName); if (existing) { applyTableStructureInitialTab(existing, initialTab, initialTarget); switchTab(existing.id);