fix(export): show all dameng databases in export dropdown
This commit is contained in:
10 files changed
+143
-33
No files matched your search
@@ -10,6 +10,7 @@ import PasswordInput from "@/components/ui/PasswordInput.vue";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useConnectionStore } from "@/stores/connectionStore";
|
import { useConnectionStore } from "@/stores/connectionStore";
|
||||||
import { useToast } from "@/composables/useToast";
|
import { useToast } from "@/composables/useToast";
|
||||||
|
import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||||
import type { ConnectionConfig } from "@/types/database";
|
import type { ConnectionConfig } from "@/types/database";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
@@ -223,7 +224,8 @@ async function openCreateUserDialog() {
|
|||||||
createDatabasesLoading.value = true;
|
createDatabasesLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await ensureConnection();
|
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) {
|
} catch (error: any) {
|
||||||
toast(t("userAdmin.loadDatabasesFailed", { message: error?.message || String(error) }), 5000);
|
toast(t("userAdmin.loadDatabasesFailed", { message: error?.message || String(error) }), 5000);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { translateBackendError } from "@/i18n/backend-errors";
|
|||||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||||
import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
|
import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
|
||||||
import { useConnectionStore } from "@/stores/connectionStore";
|
import { useConnectionStore } from "@/stores/connectionStore";
|
||||||
|
import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -139,7 +140,8 @@ async function loadDatabases(connectionId: string, preserveSelection: boolean) {
|
|||||||
loadingDatabases.value = true;
|
loadingDatabases.value = true;
|
||||||
try {
|
try {
|
||||||
await connectionStore.ensureConnected(connectionId);
|
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;
|
databaseOptions.value = names;
|
||||||
if (!preserveSelection) {
|
if (!preserveSelection) {
|
||||||
selectedDatabases.value = [];
|
selectedDatabases.value = [];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
|||||||
import { useGraphStore } from "@/lib/diagram/graph-store";
|
import { useGraphStore } from "@/lib/diagram/graph-store";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import { DIAGRAM_SQL_TYPES, isSchemaAware as isSchemaAwareDatabase } from "@/lib/database/databaseCapabilities";
|
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 { 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 { createDraftTable } from "@/lib/diagram/draft-table";
|
||||||
import { cardinalityPairFromChoice } from "@/lib/diagram/cardinality";
|
import { cardinalityPairFromChoice } from "@/lib/diagram/cardinality";
|
||||||
@@ -1116,11 +1116,17 @@ async function loadDatabases(id: string) {
|
|||||||
databases.value = [];
|
databases.value = [];
|
||||||
try {
|
try {
|
||||||
await store.ensureConnected(id);
|
await store.ensureConnected(id);
|
||||||
const dbs = await api.listDatabases(id);
|
const config = store.getConfig(id);
|
||||||
databases.value = databaseOptionsForConnection(
|
if (config?.db_type === "dameng") {
|
||||||
dbs.map((db) => db.name),
|
// 达梦的"数据库"概念对应 schema,使用 fetchNamespaceOptionsForConnection
|
||||||
store.getConfig(id),
|
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) {
|
} catch (e: any) {
|
||||||
toast(e?.message || String(e), 5000);
|
toast(e?.message || String(e), 5000);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect
|
|||||||
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
|
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
|
||||||
import { useConnectionStore } from "@/stores/connectionStore";
|
import { useConnectionStore } from "@/stores/connectionStore";
|
||||||
import { useToast } from "@/composables/useToast";
|
import { useToast } from "@/composables/useToast";
|
||||||
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
|
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||||
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
||||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||||
import type { DataCompareCellValue, DataCompareModifiedRow, DataCompareResult, DataCompareRow, DataCompareSyncPlan, DataCompareSyncPlanTableOptions } from "@/lib/dataGrid/dataCompare";
|
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") {
|
async function loadDatabases(connectionId: string, side: "source" | "target") {
|
||||||
if (!connectionId) return;
|
if (!connectionId) return;
|
||||||
await store.ensureConnected(connectionId);
|
await store.ensureConnected(connectionId);
|
||||||
const names = databaseOptionsForConnection(
|
const config = store.getConfig(connectionId);
|
||||||
(await api.listDatabases(connectionId)).map((database) => database.name),
|
const names =
|
||||||
store.getConfig(connectionId),
|
config?.db_type === "dameng"
|
||||||
);
|
? await fetchNamespaceOptionsForConnection(connectionId, config)
|
||||||
|
: databaseOptionsForConnection(
|
||||||
|
(await api.listDatabases(connectionId)).map((database) => database.name),
|
||||||
|
config,
|
||||||
|
);
|
||||||
if (side === "source") {
|
if (side === "source") {
|
||||||
sourceDatabases.value = names;
|
sourceDatabases.value = names;
|
||||||
sourceDatabase.value = names.length === 1 ? names[0] : "";
|
sourceDatabase.value = names.length === 1 ? names[0] : "";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
|||||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
||||||
|
import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||||
import { ArrowLeftRight, GitCompareArrows, Save, FolderOpen, Settings, X } from "@lucide/vue";
|
import { ArrowLeftRight, GitCompareArrows, Save, FolderOpen, Settings, X } from "@lucide/vue";
|
||||||
import type { SchemaDiffConfig, SchemaDiffCompareOptions, FieldMappingEntry } from "@/types/schemaDiff";
|
import type { SchemaDiffConfig, SchemaDiffCompareOptions, FieldMappingEntry } from "@/types/schemaDiff";
|
||||||
|
|
||||||
@@ -75,8 +76,15 @@ async function loadDatabases(connectionId: string, side: "source" | "target") {
|
|||||||
if (!connectionId) return;
|
if (!connectionId) return;
|
||||||
try {
|
try {
|
||||||
await store.ensureConnected(connectionId);
|
await store.ensureConnected(connectionId);
|
||||||
const dbs = await api.listDatabases(connectionId);
|
const config = store.getConfig(connectionId);
|
||||||
const dbNames = Array.isArray(dbs) ? dbs.map((db: any) => (typeof db === "string" ? db : db.name || db.database)) : [];
|
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") {
|
if (side === "source") {
|
||||||
sourceDatabases.value = dbNames;
|
sourceDatabases.value = dbNames;
|
||||||
if (props.sourceDatabase) {
|
if (props.sourceDatabase) {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
|||||||
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
|
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import type { ExportProgress } from "@/lib/backend/api";
|
import type { ExportProgress } from "@/lib/backend/api";
|
||||||
import { isSchemaAware } from "@/lib/database/databaseFeatureSupport";
|
import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
|
||||||
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
|
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||||
import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport";
|
import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport";
|
||||||
import { buildSelectedTablesPayload } from "@/lib/export/databaseExportSelection";
|
import { buildSelectedTablesPayload } from "@/lib/export/databaseExportSelection";
|
||||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||||
@@ -125,6 +125,13 @@ function connectionIconType(connId: string) {
|
|||||||
return config?.driver_profile || config?.db_type || "mysql";
|
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 {
|
function sanitizeFileName(value: string): string {
|
||||||
return (value || "database").replace(/[\\/:*?"<>|]+/g, "_").trim() || "database";
|
return (value || "database").replace(/[\\/:*?"<>|]+/g, "_").trim() || "database";
|
||||||
}
|
}
|
||||||
@@ -139,18 +146,37 @@ async function loadDatabases(connId: string) {
|
|||||||
loadingMeta.value = true;
|
loadingMeta.value = true;
|
||||||
try {
|
try {
|
||||||
await store.ensureConnected(connId);
|
await store.ensureConnected(connId);
|
||||||
const dbs = await api.listDatabases(connId);
|
const config = store.getConfig(connId);
|
||||||
const names = databaseOptionsForConnection(
|
let names: string[];
|
||||||
dbs.map((d) => d.name),
|
if (config?.db_type === "dameng") {
|
||||||
store.getConfig(connId),
|
// 达梦的"数据库"概念对应 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;
|
databases.value = names;
|
||||||
database.value = names.length === 1 ? names[0] : "";
|
|
||||||
selectedDatabases.value = exportAllDatabases.value ? [...names] : [];
|
selectedDatabases.value = exportAllDatabases.value ? [...names] : [];
|
||||||
schemas.value = [];
|
|
||||||
schema.value = "";
|
|
||||||
tables.value = [];
|
tables.value = [];
|
||||||
selectedTables.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 {
|
} catch {
|
||||||
databases.value = [];
|
databases.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
@@ -236,14 +262,15 @@ function clearSelectedDatabases() {
|
|||||||
|
|
||||||
async function buildExportPlanForDatabases(dbs: string[]): Promise<AllDatabaseExportPlanItem[]> {
|
async function buildExportPlanForDatabases(dbs: string[]): Promise<AllDatabaseExportPlanItem[]> {
|
||||||
const config = store.getConfig(connectionId.value);
|
const config = store.getConfig(connectionId.value);
|
||||||
const schemaAware = isSchemaAware(config?.db_type);
|
const dbType = config?.db_type;
|
||||||
|
const schemaAware = isSchemaAware(dbType);
|
||||||
const schemasByDatabase: Record<string, string[]> = {};
|
const schemasByDatabase: Record<string, string[]> = {};
|
||||||
if (schemaAware) {
|
if (schemaAware) {
|
||||||
for (const db of dbs) {
|
for (const db of dbs) {
|
||||||
schemasByDatabase[db] = await api.listSchemas(connectionId.value, db);
|
schemasByDatabase[db] = await api.listSchemas(connectionId.value, db);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buildAllDatabaseExportPlan({ databases: dbs, schemaAware, schemasByDatabase });
|
return buildAllDatabaseExportPlan({ databases: dbs, schemaAware, schemasByDatabase, dbType });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startExport() {
|
async function startExport() {
|
||||||
@@ -635,6 +662,8 @@ watch(connectionId, (id) => {
|
|||||||
|
|
||||||
watch(database, (db) => {
|
watch(database, (db) => {
|
||||||
if (exportAllDatabases.value) return;
|
if (exportAllDatabases.value) return;
|
||||||
|
// 单数据库架构下,"源数据库"由 schema 驱动,跳过此处的 schema 重新加载
|
||||||
|
if (isSingleDb.value) return;
|
||||||
schema.value = "";
|
schema.value = "";
|
||||||
schemas.value = [];
|
schemas.value = [];
|
||||||
tables.value = [];
|
tables.value = [];
|
||||||
@@ -645,6 +674,10 @@ watch(database, (db) => {
|
|||||||
|
|
||||||
watch(schema, (value) => {
|
watch(schema, (value) => {
|
||||||
if (exportAllDatabases.value) return;
|
if (exportAllDatabases.value) return;
|
||||||
|
// 单数据库架构下,schema 即 database,同步 database 值
|
||||||
|
if (isSingleDb.value) {
|
||||||
|
database.value = value;
|
||||||
|
}
|
||||||
tables.value = [];
|
tables.value = [];
|
||||||
selectedTables.value = [];
|
selectedTables.value = [];
|
||||||
tableError.value = null;
|
tableError.value = null;
|
||||||
@@ -742,7 +775,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="databases.length" class="space-y-1.5">
|
<div v-else-if="databases.length && !isSingleDb" class="space-y-1.5">
|
||||||
<Label class="text-xs">{{ t("transfer.sourceDatabase") }}</Label>
|
<Label class="text-xs">{{ t("transfer.sourceDatabase") }}</Label>
|
||||||
<Select :model-value="database" @update:model-value="(v: any) => (database = String(v))">
|
<Select :model-value="database" @update:model-value="(v: any) => (database = String(v))">
|
||||||
<SelectTrigger class="h-8 text-xs">
|
<SelectTrigger class="h-8 text-xs">
|
||||||
|
|||||||
@@ -177,16 +177,17 @@ describe("namespace options", () => {
|
|||||||
expect(namespaceOptionsAreSchemas({ db_type: "postgres" })).toBe(false);
|
expect(namespaceOptionsAreSchemas({ db_type: "postgres" })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not expand the global database options composable to Dameng schemas", async () => {
|
it("expands Dameng schemas via listSchemas", async () => {
|
||||||
mocks.getConfig.mockReturnValue({ db_type: "dameng" });
|
mocks.getConfig.mockReturnValue({ db_type: "dameng" });
|
||||||
mocks.listDatabases.mockResolvedValue([]);
|
mocks.listSchemas.mockResolvedValue(["APP_USER", "REPORTING", "SYS"]);
|
||||||
|
|
||||||
const { databaseOptions, loadDatabaseOptions } = useDatabaseOptions();
|
const { databaseOptions, loadDatabaseOptions } = useDatabaseOptions();
|
||||||
await loadDatabaseOptions("connection-1");
|
await loadDatabaseOptions("connection-1");
|
||||||
|
|
||||||
expect(databaseOptions.value["connection-1"]).toEqual([]);
|
// 达梦现在通过 listSchemas 获取 schema 列表,SYS 作为系统 schema 被过滤掉
|
||||||
expect(mocks.listDatabases).toHaveBeenCalledWith("connection-1");
|
expect(databaseOptions.value["connection-1"]).toEqual(["APP_USER", "REPORTING"]);
|
||||||
expect(mocks.listSchemas).not.toHaveBeenCalled();
|
expect(mocks.listSchemas).toHaveBeenCalledWith("connection-1", "", true);
|
||||||
|
expect(mocks.listDatabases).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export function useDatabaseOptions() {
|
|||||||
);
|
);
|
||||||
} else if (connection.db_type === "mongodb") {
|
} else if (connection.db_type === "mongodb") {
|
||||||
databaseOptions.value[connectionId] = filterDatabaseNamesForConnection(await api.mongoListDatabases(connectionId), connection);
|
databaseOptions.value[connectionId] = filterDatabaseNamesForConnection(await api.mongoListDatabases(connectionId), connection);
|
||||||
|
} else if (connection.db_type === "dameng") {
|
||||||
|
// 达梦的"数据库"概念对应 schema,使用 listSchemas 获取 schema 列表
|
||||||
|
databaseOptions.value[connectionId] = await fetchNamespaceOptionsForConnection(connectionId, connection);
|
||||||
} else if (catalog && isDorisFamilyCatalogCapable(connection?.db_type, connection?.driver_profile)) {
|
} else if (catalog && isDorisFamilyCatalogCapable(connection?.db_type, connection?.driver_profile)) {
|
||||||
const dbs = await api.listDorisCatalogDatabases(connectionId, catalog);
|
const dbs = await api.listDorisCatalogDatabases(connectionId, catalog);
|
||||||
databaseOptions.value[connectionId] = databaseOptionsForConnection(
|
databaseOptions.value[connectionId] = databaseOptionsForConnection(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { DatabaseType, QueryResult } from "@/types/database.ts";
|
|||||||
import * as api from "@/lib/backend/api.ts";
|
import * as api from "@/lib/backend/api.ts";
|
||||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql.ts";
|
import { buildTableSelectSql } from "@/lib/table/tableSelectSql.ts";
|
||||||
import { uuid } from "@/lib/common/utils.ts";
|
import { uuid } from "@/lib/common/utils.ts";
|
||||||
|
import { SINGLE_DATABASE_TYPES } from "@/lib/database/databaseCapabilitySets";
|
||||||
|
|
||||||
export const DATABASE_EXPORT_ROW_LIMIT = 10_000;
|
export const DATABASE_EXPORT_ROW_LIMIT = 10_000;
|
||||||
export const DATABASE_EXPORT_PAGE_SIZE = 500;
|
export const DATABASE_EXPORT_PAGE_SIZE = 500;
|
||||||
@@ -58,6 +59,10 @@ export interface AllDatabaseExportPlanInput {
|
|||||||
databases: string[];
|
databases: string[];
|
||||||
schemaAware: boolean;
|
schemaAware: boolean;
|
||||||
schemasByDatabase?: Record<string, string[]>;
|
schemasByDatabase?: Record<string, string[]>;
|
||||||
|
/** 数据库类型,用于判断是否为单数据库架构(如达梦、Oracle)。
|
||||||
|
* 对于这类数据库,选中的"数据库"实际上就是 schema 本身,
|
||||||
|
* 不应再对每个"数据库"展开所有 schema 做笛卡尔积。 */
|
||||||
|
dbType?: DatabaseType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllDatabaseExportPlanItem {
|
export interface AllDatabaseExportPlanItem {
|
||||||
@@ -130,6 +135,19 @@ export async function runWithDatabaseBackupSnapshot<T>(options: DatabaseBackupSn
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildAllDatabaseExportPlan(options: AllDatabaseExportPlanInput): AllDatabaseExportPlanItem[] {
|
export function buildAllDatabaseExportPlan(options: AllDatabaseExportPlanInput): AllDatabaseExportPlanItem[] {
|
||||||
|
// 对于 schema-aware 的单库类型(达梦、Oracle、OceanBase-Oracle 等),选中的"数据库"
|
||||||
|
// 实际上就是 schema 本身,不应再展开所有 schema 做笛卡尔积,直接将选中项作为 schema 导出。
|
||||||
|
// firebird/questdb/access 等单库但非 schema-aware 类型必须走 flatMap 保留真实 database,
|
||||||
|
// 否则 database:"" 会覆盖后端 db_config.database 破坏连接。
|
||||||
|
const singleDatabase = options.dbType ? SINGLE_DATABASE_TYPES.has(options.dbType) : false;
|
||||||
|
if (singleDatabase && options.schemaAware) {
|
||||||
|
return options.databases.map((schema) => ({
|
||||||
|
database: "",
|
||||||
|
schema,
|
||||||
|
fileStem: schema,
|
||||||
|
displayName: schema,
|
||||||
|
}));
|
||||||
|
}
|
||||||
return options.databases.flatMap((database) => {
|
return options.databases.flatMap((database) => {
|
||||||
const schemas = options.schemaAware ? (options.schemasByDatabase?.[database] ?? []).filter((schema) => schema.trim()) : [database];
|
const schemas = options.schemaAware ? (options.schemasByDatabase?.[database] ?? []).filter((schema) => schema.trim()) : [database];
|
||||||
const exportSchemas = schemas.length > 0 ? schemas : [database];
|
const exportSchemas = schemas.length > 0 ? schemas : [database];
|
||||||
|
|||||||
+33
@@ -33,3 +33,36 @@ test("all-database export uses the database as schema for non-schema-aware datab
|
|||||||
{ database: "analytics", schema: "analytics", fileStem: "analytics", displayName: "analytics" },
|
{ database: "analytics", schema: "analytics", fileStem: "analytics", displayName: "analytics" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("all-database export treats selected items as schemas for single-database types (dameng)", () => {
|
||||||
|
// 达梦等单数据库架构:选中的"数据库"就是 schema 本身,不应做笛卡尔积展开
|
||||||
|
const plan = buildAllDatabaseExportPlan({
|
||||||
|
databases: ["COSIMULATION", "DATAMANAGE"],
|
||||||
|
schemaAware: true,
|
||||||
|
schemasByDatabase: {
|
||||||
|
COSIMULATION: ["COSIMULATION", "DATAMANAGE", "MULTITEST"],
|
||||||
|
DATAMANAGE: ["COSIMULATION", "DATAMANAGE", "MULTITEST"],
|
||||||
|
},
|
||||||
|
dbType: "dameng",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(plan, [
|
||||||
|
{ database: "", schema: "COSIMULATION", fileStem: "COSIMULATION", displayName: "COSIMULATION" },
|
||||||
|
{ database: "", schema: "DATAMANAGE", fileStem: "DATAMANAGE", displayName: "DATAMANAGE" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("all-database export preserves real database for non-schema-aware single-database types (firebird)", () => {
|
||||||
|
// firebird/questdb/access 是单库但非 schema-aware:不能短路成 database:"",
|
||||||
|
// 否则空 database 会覆盖后端 db_config.database 破坏连接。
|
||||||
|
const plan = buildAllDatabaseExportPlan({
|
||||||
|
databases: ["inventory", "archive"],
|
||||||
|
schemaAware: false,
|
||||||
|
dbType: "firebird",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(plan, [
|
||||||
|
{ database: "inventory", schema: "inventory", fileStem: "inventory", displayName: "inventory" },
|
||||||
|
{ database: "archive", schema: "archive", fileStem: "archive", displayName: "archive" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user