fix(doris): preserve catalog in metadata views

This commit is contained in:
jischeng 2026-07-16 18:20:12 +08:00 committed by GitHub
parent 8254cfd948
commit a9e76dc130
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 75 additions and 25 deletions

View File

@ -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"

View File

@ -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() {

View File

@ -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"

View File

@ -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);

View File

@ -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)"

View File

@ -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);
}
}

View File

@ -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<string | undefined> {
async function fetchTableCommentValue(connectionId: string, database: string, schema: string, tableName: string, catalog?: string): Promise<string | undefined> {
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<string, string> } = {}) {
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);

View File

@ -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\)/);
});
});

View File

@ -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();

View File

@ -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);