From 9bb1b7aa91e239e89f950fc78feb5db19d270a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E6=A1=A5=E5=87=89=E4=BB=8B?= <113153113+HighBigSky@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:48:10 +0800 Subject: [PATCH] fix(export): show all dameng databases in export dropdown --- .../components/admin/DatabaseUserAdmin.vue | 4 +- .../ScheduledDatabaseBackupSettings.vue | 4 +- .../diagram/SchemaDiagramDialog.vue | 18 ++++-- .../src/components/diff/DataCompareDialog.vue | 14 +++-- .../components/diff/SchemaDiffConfigStep.vue | 12 +++- .../export/DatabaseExportDialog.vue | 59 +++++++++++++++---- .../__tests__/useDatabaseOptions.spec.ts | 11 ++-- .../src/composables/useDatabaseOptions.ts | 3 + apps/desktop/src/lib/export/databaseExport.ts | 18 ++++++ packages/app-tests/databaseExportPlan.test.ts | 33 +++++++++++ 10 files changed, 143 insertions(+), 33 deletions(-) diff --git a/apps/desktop/src/components/admin/DatabaseUserAdmin.vue b/apps/desktop/src/components/admin/DatabaseUserAdmin.vue index 8299e7b88..4d99f1919 100644 --- a/apps/desktop/src/components/admin/DatabaseUserAdmin.vue +++ b/apps/desktop/src/components/admin/DatabaseUserAdmin.vue @@ -10,6 +10,7 @@ import PasswordInput from "@/components/ui/PasswordInput.vue"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; +import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; import { useSqlHighlighter } from "@/composables/useSqlHighlighter"; import type { ConnectionConfig } from "@/types/database"; import * as api from "@/lib/backend/api"; @@ -223,7 +224,8 @@ async function openCreateUserDialog() { createDatabasesLoading.value = true; try { await ensureConnection(); - createDatabases.value = (await api.listDatabases(props.connection.id)).map((database) => database.name); + const config = props.connection; + createDatabases.value = config.db_type === "dameng" ? await fetchNamespaceOptionsForConnection(config.id, config) : (await api.listDatabases(config.id)).map((database) => database.name); } catch (error: any) { toast(t("userAdmin.loadDatabasesFailed", { message: error?.message || String(error) }), 5000); } finally { diff --git a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue index eef971363..9c4d54874 100644 --- a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue +++ b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue @@ -16,6 +16,7 @@ import { translateBackendError } from "@/i18n/backend-errors"; import { generateDatabaseExportId } from "@/lib/export/databaseExport"; import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup"; import { useConnectionStore } from "@/stores/connectionStore"; +import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; const { t, locale } = useI18n(); const { toast } = useToast(); @@ -139,7 +140,8 @@ async function loadDatabases(connectionId: string, preserveSelection: boolean) { loadingDatabases.value = true; try { await connectionStore.ensureConnected(connectionId); - const names = (await api.listDatabases(connectionId)).map((database) => database.name); + const config = connectionStore.getConfig(connectionId); + const names = config?.db_type === "dameng" ? await fetchNamespaceOptionsForConnection(connectionId, config) : (await api.listDatabases(connectionId)).map((database) => database.name); databaseOptions.value = names; if (!preserveSelection) { selectedDatabases.value = []; diff --git a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue index 28603de71..6e6e26adc 100644 --- a/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue +++ b/apps/desktop/src/components/diagram/SchemaDiagramDialog.vue @@ -16,7 +16,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useGraphStore } from "@/lib/diagram/graph-store"; import * as api from "@/lib/backend/api"; import { DIAGRAM_SQL_TYPES, isSchemaAware as isSchemaAwareDatabase } from "@/lib/database/databaseCapabilities"; -import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions"; +import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; import { buildDiagramJoinSql, buildDiagramRelationships, filterDiagramTables, mergeRelationshipsWithInferred, normalizeCustomDiagramRelationship, type CustomDiagramRelationship, type DiagramPosition, type DiagramTable, isDraftTable, needsDiagramSync } from "@/lib/diagram/erDiagram"; import { createDraftTable } from "@/lib/diagram/draft-table"; import { cardinalityPairFromChoice } from "@/lib/diagram/cardinality"; @@ -1116,11 +1116,17 @@ async function loadDatabases(id: string) { databases.value = []; try { await store.ensureConnected(id); - const dbs = await api.listDatabases(id); - databases.value = databaseOptionsForConnection( - dbs.map((db) => db.name), - store.getConfig(id), - ); + const config = store.getConfig(id); + if (config?.db_type === "dameng") { + // 达梦的"数据库"概念对应 schema,使用 fetchNamespaceOptionsForConnection + databases.value = await fetchNamespaceOptionsForConnection(id, config); + } else { + const dbs = await api.listDatabases(id); + databases.value = databaseOptionsForConnection( + dbs.map((db) => db.name), + config, + ); + } } catch (e: any) { toast(e?.message || String(e), 5000); } finally { diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index 291ba736c..9c8009c3f 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -10,7 +10,7 @@ import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; -import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions"; +import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; import { isSchemaAware } from "@/lib/database/databaseCapabilities"; import { copyToClipboard } from "@/lib/common/clipboard"; import type { DataCompareCellValue, DataCompareModifiedRow, DataCompareResult, DataCompareRow, DataCompareSyncPlan, DataCompareSyncPlanTableOptions } from "@/lib/dataGrid/dataCompare"; @@ -334,10 +334,14 @@ async function loadSchemas(side: "source" | "target", preferredSchema = "") { async function loadDatabases(connectionId: string, side: "source" | "target") { if (!connectionId) return; await store.ensureConnected(connectionId); - const names = databaseOptionsForConnection( - (await api.listDatabases(connectionId)).map((database) => database.name), - store.getConfig(connectionId), - ); + const config = store.getConfig(connectionId); + const names = + config?.db_type === "dameng" + ? await fetchNamespaceOptionsForConnection(connectionId, config) + : databaseOptionsForConnection( + (await api.listDatabases(connectionId)).map((database) => database.name), + config, + ); if (side === "source") { sourceDatabases.value = names; sourceDatabase.value = names.length === 1 ? names[0] : ""; diff --git a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue index aa6b0283e..7779d3adb 100644 --- a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue +++ b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue @@ -10,6 +10,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/backend/api"; import { isSchemaAware } from "@/lib/database/databaseCapabilities"; +import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; import { ArrowLeftRight, GitCompareArrows, Save, FolderOpen, Settings, X } from "@lucide/vue"; import type { SchemaDiffConfig, SchemaDiffCompareOptions, FieldMappingEntry } from "@/types/schemaDiff"; @@ -75,8 +76,15 @@ async function loadDatabases(connectionId: string, side: "source" | "target") { if (!connectionId) return; try { await store.ensureConnected(connectionId); - const dbs = await api.listDatabases(connectionId); - const dbNames = Array.isArray(dbs) ? dbs.map((db: any) => (typeof db === "string" ? db : db.name || db.database)) : []; + const config = store.getConfig(connectionId); + let dbNames: string[]; + if (config?.db_type === "dameng") { + // 达梦的"数据库"概念对应 schema,使用 fetchNamespaceOptionsForConnection + dbNames = await fetchNamespaceOptionsForConnection(connectionId, config); + } else { + const dbs = await api.listDatabases(connectionId); + dbNames = Array.isArray(dbs) ? dbs.map((db: any) => (typeof db === "string" ? db : db.name || db.database)) : []; + } if (side === "source") { sourceDatabases.value = dbNames; if (props.sourceDatabase) { diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index 48102cf87..15645ccb2 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -10,8 +10,8 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue"; import * as api from "@/lib/backend/api"; import type { ExportProgress } from "@/lib/backend/api"; -import { isSchemaAware } from "@/lib/database/databaseFeatureSupport"; -import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions"; +import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport"; +import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport"; import { buildSelectedTablesPayload } from "@/lib/export/databaseExportSelection"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; @@ -125,6 +125,13 @@ function connectionIconType(connId: string) { return config?.driver_profile || config?.db_type || "mysql"; } +// 当前连接是否为单数据库架构(达梦、Oracle 等),这类数据库的"源数据库"与"Schema"没有层级关系 +const isSingleDb = computed(() => { + if (!connectionId.value) return false; + const config = store.getConfig(connectionId.value); + return isSingleDatabase(config?.db_type); +}); + function sanitizeFileName(value: string): string { return (value || "database").replace(/[\\/:*?"<>|]+/g, "_").trim() || "database"; } @@ -139,18 +146,37 @@ async function loadDatabases(connId: string) { loadingMeta.value = true; try { await store.ensureConnected(connId); - const dbs = await api.listDatabases(connId); - const names = databaseOptionsForConnection( - dbs.map((d) => d.name), - store.getConfig(connId), - ); + const config = store.getConfig(connId); + let names: string[]; + if (config?.db_type === "dameng") { + // 达梦的"数据库"概念对应 schema,使用 fetchNamespaceOptionsForConnection + // 内部已正确处理达梦:通过 listSchemas 获取 schema 列表而非用户列表 + names = await fetchNamespaceOptionsForConnection(connId, config); + } else { + const dbs = await api.listDatabases(connId); + names = databaseOptionsForConnection( + dbs.map((d) => d.name), + config, + ); + } databases.value = names; - database.value = names.length === 1 ? names[0] : ""; selectedDatabases.value = exportAllDatabases.value ? [...names] : []; - schemas.value = []; - schema.value = ""; tables.value = []; selectedTables.value = []; + + // 单数据库架构下(达梦、Oracle 等),databases 实际是 schema 列表, + // 直接作为 schemas 使用,并将 database 初始值设为第一个 schema + // 这样做的原因是:单数据库架构中"源数据库"与"Schema"没有层级关系, + // 所有 schema 都在同一个数据库实例中,因此 schema 同时作为 database 参数 + if (config?.db_type && isSingleDatabase(config.db_type)) { + schemas.value = names; + schema.value = names.length === 1 ? names[0] : ""; + database.value = schema.value; + } else { + database.value = names.length === 1 ? names[0] : ""; + schemas.value = []; + schema.value = ""; + } } catch { databases.value = []; } finally { @@ -236,14 +262,15 @@ function clearSelectedDatabases() { async function buildExportPlanForDatabases(dbs: string[]): Promise { const config = store.getConfig(connectionId.value); - const schemaAware = isSchemaAware(config?.db_type); + const dbType = config?.db_type; + const schemaAware = isSchemaAware(dbType); const schemasByDatabase: Record = {}; if (schemaAware) { for (const db of dbs) { schemasByDatabase[db] = await api.listSchemas(connectionId.value, db); } } - return buildAllDatabaseExportPlan({ databases: dbs, schemaAware, schemasByDatabase }); + return buildAllDatabaseExportPlan({ databases: dbs, schemaAware, schemasByDatabase, dbType }); } async function startExport() { @@ -635,6 +662,8 @@ watch(connectionId, (id) => { watch(database, (db) => { if (exportAllDatabases.value) return; + // 单数据库架构下,"源数据库"由 schema 驱动,跳过此处的 schema 重新加载 + if (isSingleDb.value) return; schema.value = ""; schemas.value = []; tables.value = []; @@ -645,6 +674,10 @@ watch(database, (db) => { watch(schema, (value) => { if (exportAllDatabases.value) return; + // 单数据库架构下,schema 即 database,同步 database 值 + if (isSingleDb.value) { + database.value = value; + } tables.value = []; selectedTables.value = []; tableError.value = null; @@ -742,7 +775,7 @@ watch( -
+