fix(transfer): list Dameng schemas as databases

This commit is contained in:
t8y2 2026-07-22 12:54:04 +08:00
parent b7f1820e12
commit 0b86d8278d
3 changed files with 74 additions and 14 deletions

View File

@ -15,7 +15,7 @@ import * as api from "@/lib/backend/api";
import type { TransferMode, TransferTableNameCase } from "@/lib/backend/api";
import type { DatabaseType } from "@/types/database";
import { isSchemaAware, supportsTransfer } from "@/lib/database/databaseCapabilities";
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection, namespaceOptionsAreSchemas } from "@/composables/useDatabaseOptions";
import { useExportTracker } from "@/composables/useExportTracker";
import { ArrowRightLeft, ArrowLeftRight, Loader2, Square, CheckSquare } from "@lucide/vue";
@ -99,8 +99,9 @@ async function loadDatabases(connectionId: string, target: "source" | "target")
if (!connectionId) return;
try {
await store.ensureConnected(connectionId);
const rawNames = isMongoConnection(connectionId) ? await api.mongoListDatabases(connectionId) : (await api.listDatabases(connectionId)).map((d) => d.name);
const names = databaseOptionsForConnection(rawNames, store.getConfig(connectionId));
const config = store.getConfig(connectionId);
if (!config) return;
const names = isMongoConnection(connectionId) ? databaseOptionsForConnection(await api.mongoListDatabases(connectionId), config) : await fetchNamespaceOptionsForConnection(connectionId, config);
if (target === "source") {
sourceDatabases.value = names;
sourceDatabase.value = names.length === 1 ? names[0] : "";
@ -188,7 +189,12 @@ watch(sourceConnectionId, (id) => {
watch(sourceDatabase, async (db) => {
if (db) {
const config = store.getConfig(sourceConnectionId.value);
if (isSchemaAware(config?.db_type)) {
if (namespaceOptionsAreSchemas(config)) {
// Dameng has no selectable catalog, so the top-level namespace option is
// also the schema used for metadata lookup and qualified transfer SQL.
sourceSchemas.value = [];
sourceSchema.value = db;
} else if (isSchemaAware(config?.db_type)) {
await loadSchemas(sourceConnectionId.value, db, "source");
} else {
sourceSchema.value = db;
@ -208,7 +214,10 @@ watch(targetConnectionId, (id) => {
watch(targetDatabase, async (db) => {
if (db) {
const config = store.getConfig(targetConnectionId.value);
if (isSchemaAware(config?.db_type)) {
if (namespaceOptionsAreSchemas(config)) {
targetSchemas.value = [];
targetSchema.value = db;
} else if (isSchemaAware(config?.db_type)) {
await loadSchemas(targetConnectionId.value, db, "target");
} else {
targetSchema.value = db;

View File

@ -1,7 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchSqlFileTargetOptions } from "@/composables/useDatabaseOptions";
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection, fetchSqlFileTargetOptions, namespaceOptionsAreSchemas, useDatabaseOptions } from "@/composables/useDatabaseOptions";
const mocks = vi.hoisted(() => ({
ensureConnected: vi.fn(),
getConfig: vi.fn(),
listDatabases: vi.fn(),
listSchemas: vi.fn(),
}));
@ -12,10 +14,13 @@ vi.mock("@/lib/backend/api", () => ({
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: vi.fn(),
useConnectionStore: () => ({
ensureConnected: mocks.ensureConnected,
getConfig: mocks.getConfig,
}),
}));
describe("fetchSqlFileTargetOptions", () => {
describe("namespace options", () => {
beforeEach(() => {
vi.clearAllMocks();
});
@ -23,7 +28,7 @@ describe("fetchSqlFileTargetOptions", () => {
it("uses Dameng schemas so independent schemas remain selectable", async () => {
mocks.listSchemas.mockResolvedValue(["APP_USER", "REPORTING", "SYS"]);
const options = await fetchSqlFileTargetOptions("connection-1", {
const options = await fetchNamespaceOptionsForConnection("connection-1", {
db_type: "dameng",
database: "APP_USER",
visible_databases: ["APP_USER", "REPORTING"],
@ -37,7 +42,7 @@ describe("fetchSqlFileTargetOptions", () => {
it("honors the configured Dameng schema filter before the legacy database filter", async () => {
mocks.listSchemas.mockResolvedValue(["APP_USER", "REPORTING", "ARCHIVE"]);
const options = await fetchSqlFileTargetOptions("connection-1", {
const options = await fetchNamespaceOptionsForConnection("connection-1", {
db_type: "dameng",
database: "APP_USER",
visible_databases: ["APP_USER", "REPORTING"],
@ -50,7 +55,7 @@ describe("fetchSqlFileTargetOptions", () => {
it("preserves listDatabases and visible database filtering for other databases", async () => {
mocks.listDatabases.mockResolvedValue([{ name: "app" }, { name: "analytics" }, { name: "postgres" }]);
const options = await fetchSqlFileTargetOptions("connection-2", {
const options = await fetchNamespaceOptionsForConnection("connection-2", {
db_type: "postgres",
database: "app",
visible_databases: ["analytics"],
@ -61,15 +66,53 @@ describe("fetchSqlFileTargetOptions", () => {
expect(mocks.listSchemas).not.toHaveBeenCalled();
});
it("preserves visible database filtering for MongoDB transfer options", () => {
expect(
databaseOptionsForConnection(["app", "analytics", "admin"], {
db_type: "mongodb",
visible_databases: ["analytics"],
}),
).toEqual(["analytics"]);
});
it("propagates metadata loading errors", async () => {
const error = new Error("schema metadata failed");
mocks.listSchemas.mockRejectedValue(error);
await expect(
fetchNamespaceOptionsForConnection("connection-1", {
db_type: "dameng",
database: "APP_USER",
}),
).rejects.toBe(error);
});
it("keeps the SQL file target on the shared namespace loader", async () => {
mocks.listSchemas.mockResolvedValue(["APP_USER", "REPORTING"]);
await expect(
fetchSqlFileTargetOptions("connection-1", {
db_type: "dameng",
database: "APP_USER",
}),
).rejects.toBe(error);
).resolves.toEqual(["APP_USER", "REPORTING"]);
});
it("identifies only Dameng top-level options as schemas", () => {
expect(namespaceOptionsAreSchemas({ db_type: "dameng" })).toBe(true);
expect(namespaceOptionsAreSchemas({ db_type: "oracle" })).toBe(false);
expect(namespaceOptionsAreSchemas({ db_type: "postgres" })).toBe(false);
});
it("does not expand the global database options composable to Dameng schemas", async () => {
mocks.getConfig.mockReturnValue({ db_type: "dameng" });
mocks.listDatabases.mockResolvedValue([]);
const { databaseOptions, loadDatabaseOptions } = useDatabaseOptions();
await loadDatabaseOptions("connection-1");
expect(databaseOptions.value["connection-1"]).toEqual([]);
expect(mocks.listDatabases).toHaveBeenCalledWith("connection-1");
expect(mocks.listSchemas).not.toHaveBeenCalled();
});
});

View File

@ -5,7 +5,7 @@ import { usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import type { ConnectionConfig } from "@/types/database";
import * as api from "@/lib/backend/api";
type SqlFileTargetConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "visible_databases" | "visible_schemas">;
type NamespaceOptionsConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "visible_databases" | "visible_schemas">;
export function databaseOptionsForConnection(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_databases"> | undefined): string[] {
const names = filterDatabaseNamesForConnection(databaseNames, connection);
@ -13,7 +13,11 @@ export function databaseOptionsForConnection(databaseNames: string[], connection
return names;
}
export async function fetchSqlFileTargetOptions(connectionId: string, connection: SqlFileTargetConnection): Promise<string[]> {
export function namespaceOptionsAreSchemas(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
return connection?.db_type === "dameng";
}
export async function fetchNamespaceOptionsForConnection(connectionId: string, connection: NamespaceOptionsConnection): Promise<string[]> {
if (connection.db_type === "dameng") {
const database = connection.database || "";
// Dameng users and schemas are not interchangeable: independent schemas
@ -29,6 +33,10 @@ export async function fetchSqlFileTargetOptions(connectionId: string, connection
);
}
export async function fetchSqlFileTargetOptions(connectionId: string, connection: NamespaceOptionsConnection): Promise<string[]> {
return fetchNamespaceOptionsForConnection(connectionId, connection);
}
export function useDatabaseOptions() {
const connectionStore = useConnectionStore();