diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index bd013e5829..1d26b7d96f 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1913,6 +1913,7 @@ onUnmounted(() => { connectionId: activeTab.connectionId, database: activeTab.database, schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: target.tableType, }) diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index e0e8be1710..c6ce0995aa 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -145,7 +145,7 @@ const emit = defineEmits<{ viewTableData: [tableName: string]; viewTableDdl: [tableName: string]; editTableStructure: [tableName: string]; - openObjectTable: [target: { tableName: string; schema?: string; tableType?: string }]; + openObjectTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; objectSchemaChange: [schema: string | undefined]; objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport]; structureEditorSaved: [commentChanged: boolean]; @@ -1602,6 +1602,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe :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" diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index c648912f10..1ef4c8a19c 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -102,12 +102,13 @@ type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "to const props = defineProps<{ connection: ConnectionConfig; database: string; + catalog?: string; schema?: string; viewport?: ObjectBrowserViewport; }>(); const emit = defineEmits<{ - openTable: [target: { tableName: string; schema?: string; tableType?: string }]; + openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; schemaChange: [schema: string | undefined]; viewportChange: [viewport: ObjectBrowserViewport]; }>(); @@ -712,7 +713,7 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction) void openTableInfo(row); break; case "open-table": - emit("openTable", { tableName: row.name, schema: row.schema }); + emit("openTable", { tableName: row.name, schema: row.schema, catalog: props.catalog }); break; case "open-source": void openSource(row); @@ -1258,7 +1259,7 @@ async function saveFileContent(content: string, defaultFileName: string, filterN } function openViewData(row: ObjectBrowserRow) { - emit("openTable", { tableName: row.name, schema: row.schema, tableType: row.type }); + emit("openTable", { tableName: row.name, schema: row.schema, tableType: row.type, catalog: props.catalog }); } function openStructureEditor(row: ObjectBrowserRow) { @@ -1903,7 +1904,7 @@ async function loadObjects() { rows.value = []; try { const schema = needsSchema.value ? selectedSchema.value || "" : props.database; - const objects: ObjectInfo[] = await api.listObjects(props.connection.id, props.database, schema); + const objects: ObjectInfo[] = await api.listObjects(props.connection.id, props.database, schema, undefined, undefined, undefined, undefined, props.catalog); if (id !== loadId) return; rows.value = buildObjectBrowserRows({ objects, diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index ed0d90d7d6..4d6debe2a4 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1110,7 +1110,7 @@ async function openObjectBrowser() { connectionStore.activeConnectionId = node.connectionId; if (hasTreeNodeDatabaseContext(node)) { - queryStore.openObjectBrowser(node.connectionId, node.database, node.schema); + queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog); return; } diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index cf37dc1b4d..5aa513a645 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -12,6 +12,7 @@ import type { TableInfoTab } from "@/types/database"; export type NavigationTarget = { connectionId: string; database: string; + catalog?: string; schema?: string; tableName: string; tableType?: string; @@ -27,7 +28,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab connectionStore.activeConnectionId = target.connectionId; const config = connectionStore.getConfig(target.connectionId); - const tabTitle = target.schema ? `${target.schema}.${target.tableName}` : target.tableName; + const tabTitle = target.catalog ? `${target.catalog}.${target.schema || target.database}.${target.tableName}` : target.schema ? `${target.schema}.${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"); @@ -36,7 +37,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab } const tabId = (() => { if (settingsStore.editorSettings.reuseDataTab) { - const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === target.connectionId && tab.database === target.database); + const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === target.connectionId && tab.database === target.database && (tab.tableMeta?.catalog || "") === (target.catalog || "")); if (existing) { existing.title = tabTitle; existing.schema = target.schema; @@ -63,6 +64,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab const sql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, columns: columns.map((column) => column.name), @@ -72,6 +74,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab }); queryStore.updateSql(tabId, sql); queryStore.setTableMeta(tabId, { + catalog: target.catalog, schema: target.schema, tableName: target.tableName, tableType: targetTableType, @@ -84,6 +87,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab const sql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, @@ -92,6 +96,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.updateSql(tabId, sql); queryStore.setTableMeta(tabId, { schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, columns: [], @@ -109,6 +114,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab const emptySql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, @@ -118,12 +124,13 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab await queryStore.executeTabSql(tabId, emptySql, { pagination: { limit: pageLimit, offset: 0 } }); } try { - const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); - const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName).catch(() => []); + const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName, target.catalog); + const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName, target.catalog).catch(() => []); const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, indexes, targetTableType); const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, targetTableType); queryStore.setTableMeta(tabId, { schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, columns, @@ -133,6 +140,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab const newSql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, + catalog: target.catalog, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, 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 3a079382b8..8350367e3a 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts @@ -56,6 +56,19 @@ describe("queryStore database open state", () => { expect(tab?.objectBrowser?.viewport).toBeUndefined(); }); + it("keeps external catalog object browsers isolated", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + + const icebergTabId = store.openObjectBrowser("doris-1", "default", undefined, "iceberg_catalog"); + const hiveTabId = store.openObjectBrowser("doris-1", "default", undefined, "hive_catalog"); + + expect(hiveTabId).not.toBe(icebergTabId); + expect(store.tabs.find((tab) => tab.id === icebergTabId)?.objectBrowser?.catalog).toBe("iceberg_catalog"); + expect(store.tabs.find((tab) => tab.id === hiveTabId)?.objectBrowser?.catalog).toBe("hive_catalog"); + expect(store.openObjectBrowser("doris-1", "default", 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 04a903e8e6..b4eaef8f42 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -864,9 +864,9 @@ export const useQueryStore = defineStore("query", () => { return id; } - function openObjectBrowser(connectionId: string, database: string, schema?: string) { - const title = schema ? `${schema} objects` : `${database} objects`; - const existing = tabs.value.find((tab) => tab.mode === "objects" && tab.connectionId === connectionId && tab.database === database && (tab.objectBrowser?.schema || "") === (schema || "")); + function openObjectBrowser(connectionId: string, database: string, schema?: string, catalog?: string) { + const title = catalog ? `${catalog}.${database} objects` : schema ? `${schema} objects` : `${database} objects`; + const existing = tabs.value.find((tab) => tab.mode === "objects" && tab.connectionId === connectionId && tab.database === database && (tab.objectBrowser?.catalog || "") === (catalog || "") && (tab.objectBrowser?.schema || "") === (schema || "")); if (existing) { switchTab(existing.id); return existing.id; @@ -885,6 +885,7 @@ export const useQueryStore = defineStore("query", () => { isExplaining: false, mode: "objects", objectBrowser: { + catalog, schema, objectType: "tables", }, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 66581508f6..c57f9d51db 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -752,6 +752,7 @@ export interface QueryTab { structureInitialTarget?: TableStructureEditorTarget; structureDraft?: TableStructureEditorDraft; objectBrowser?: { + catalog?: string; schema?: string; objectType?: "tables"; viewport?: ObjectBrowserViewport;