From 23f0b1255b8480c79a182148e646c3365c34dde0 Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Tue, 1 Sep 2026 11:00:24 +0800 Subject: [PATCH] fix(compare): let oracle pick schemas in data compare --- .../src/components/diff/DataCompareDialog.vue | 13 +- .../DataCompareDialog.prefill.spec.ts | 111 ++++++++++++++++-- .../__tests__/useDatabaseOptions.spec.ts | 35 +++++- .../src/composables/useDatabaseOptions.ts | 15 ++- 4 files changed, 148 insertions(+), 26 deletions(-) diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index a3351692a5..e21283e0ce 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -319,13 +319,12 @@ async function loadDatabases(connectionId: string, side: "source" | "target") { if (!connectionId) return; await store.ensureConnected(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, - ); + const names = config + ? 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/__tests__/DataCompareDialog.prefill.spec.ts b/apps/desktop/src/components/diff/__tests__/DataCompareDialog.prefill.spec.ts index 6ec1735051..844c3b40a8 100644 --- a/apps/desktop/src/components/diff/__tests__/DataCompareDialog.prefill.spec.ts +++ b/apps/desktop/src/components/diff/__tests__/DataCompareDialog.prefill.spec.ts @@ -7,18 +7,44 @@ import DataCompareDialog from "@/components/diff/DataCompareDialog.vue"; const mocks = vi.hoisted(() => ({ ensureConnected: vi.fn().mockResolvedValue(undefined), - listDatabases: vi.fn().mockResolvedValue([{ name: "DBX_TEST" }, { name: "OPS$ORACLE" }]), - listSchemas: vi.fn().mockResolvedValue(["DBX_TEST"]), - listTables: vi.fn().mockResolvedValue([{ name: "CODEX_7046_META", table_type: "TABLE" }]), + listDatabases: vi.fn().mockResolvedValue([]), + listSchemas: vi.fn().mockResolvedValue(["DBX_TEST", "REPORTING", "SYS"]), + listTables: vi.fn().mockResolvedValue([{ name: "CODEX_7467_META", table_type: "TABLE" }]), getColumns: vi.fn().mockResolvedValue([{ name: "ID", data_type: "NUMBER", is_primary_key: true }]), })); -vi.mock("@/stores/connectionStore", () => ({ - useConnectionStore: () => ({ - connections: [{ id: "oracle-11g", name: "Oracle XE 11g", db_type: "oracle", driver_profile: "oracle", database: "XE" }], - getConfig: (id: string) => (id === "oracle-11g" ? { id, name: "Oracle XE 11g", db_type: "oracle", driver_profile: "oracle", database: "XE" } : undefined), - ensureConnected: mocks.ensureConnected, - }), +function passthrough(tag: string) { + return defineComponent({ + inheritAttrs: false, + setup(_, { attrs, slots }) { + return () => h(tag, attrs, slots.default?.()); + }, + }); +} + +vi.mock("@/stores/connectionStore", () => { + const connections = [ + { id: "oracle-11g", name: "Oracle XE 11g", db_type: "oracle", driver_profile: "oracle", database: "XE" }, + { + id: "oracle-jdbc-11g", + name: "Oracle JDBC 11g", + db_type: "jdbc", + driver_profile: "oracle", + connection_string: "jdbc:oracle:thin:@//localhost:1521/XE", + jdbc_driver_class: "oracle.jdbc.OracleDriver", + }, + ]; + return { + useConnectionStore: () => ({ + connections, + getConfig: (id: string) => connections.find((connection) => connection.id === id), + ensureConnected: mocks.ensureConnected, + }), + }; +}); + +vi.mock("@/components/connection/ConnectionGroupBadge.vue", () => ({ + default: passthrough("span"), })); vi.mock("@/lib/backend/api", () => ({ @@ -39,7 +65,7 @@ async function flushAsyncSetup() { afterEach(() => { for (const app of mountedApps.splice(0)) app.unmount(); - document.body.innerHTML = ""; + document.body.textContent = ""; vi.clearAllMocks(); }); @@ -64,8 +90,71 @@ describe("DataCompareDialog source prefill", () => { app.mount(container); await flushAsyncSetup(); + expect(mocks.listDatabases).not.toHaveBeenCalled(); + expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-11g", "XE", true); + + const searchableSelectTriggers = [...document.querySelectorAll("button.dbx-searchable-select-trigger")]; + const sourceDatabaseTrigger = searchableSelectTriggers[1]; + expect(sourceDatabaseTrigger?.title).toBe("DBX_TEST"); + expect(sourceDatabaseTrigger?.disabled).toBe(false); + sourceDatabaseTrigger?.click(); + await flushAsyncSetup(); + + const databaseOptions = [...document.querySelectorAll(".dbx-searchable-select-list button")].map((button) => button.textContent?.trim()); + expect(databaseOptions).toEqual(expect.arrayContaining(["DBX_TEST", "REPORTING"])); expect(mocks.listTables).toHaveBeenCalledWith("oracle-11g", "DBX_TEST", "DBX_TEST"); - expect(document.body.textContent).toContain("CODEX_7046_META"); + expect(document.body.textContent).toContain("CODEX_7467_META"); expect(document.body.textContent).not.toContain("暂无可比较的表"); }); + + it("loads schemas and tables after selecting an Oracle JDBC target connection", async () => { + const container = document.createElement("div"); + document.body.append(container); + const app = createApp( + defineComponent({ + setup: () => () => + h(DataCompareDialog, { + open: true, + prefillConnectionId: "oracle-11g", + prefillDatabase: "DBX_TEST", + prefillSchema: "DBX_TEST", + prefillTable: "CODEX_7467_META", + }), + }), + ); + mountedApps.push(app); + app.use(i18n); + app.mount(container); + await flushAsyncSetup(); + + const searchableSelectTriggers = [...document.querySelectorAll("button.dbx-searchable-select-trigger")]; + const targetConnectionTrigger = searchableSelectTriggers[3]; + expect(targetConnectionTrigger).toBeDefined(); + targetConnectionTrigger?.click(); + await flushAsyncSetup(); + + const jdbcConnectionOption = [...document.querySelectorAll(".dbx-searchable-select-list button")].find((button) => button.textContent?.trim() === "Oracle JDBC 11g"); + expect(jdbcConnectionOption).toBeDefined(); + jdbcConnectionOption?.click(); + await flushAsyncSetup(); + + expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-jdbc-11g", "", true); + + const triggersAfterTargetLoad = [...document.querySelectorAll("button.dbx-searchable-select-trigger")]; + const targetDatabaseTrigger = triggersAfterTargetLoad[4]; + expect(targetDatabaseTrigger?.disabled).toBe(false); + targetDatabaseTrigger?.click(); + await flushAsyncSetup(); + + const targetDatabaseOptions = [...document.querySelectorAll(".dbx-searchable-select-list button")].map((button) => button.textContent?.trim()); + expect(targetDatabaseOptions).toEqual(expect.arrayContaining(["DBX_TEST", "REPORTING"])); + + const reportingOption = [...document.querySelectorAll(".dbx-searchable-select-list button")].find((button) => button.textContent?.trim() === "REPORTING"); + expect(reportingOption).toBeDefined(); + reportingOption?.click(); + await flushAsyncSetup(); + + expect(mocks.listTables).toHaveBeenCalledWith("oracle-jdbc-11g", "REPORTING", "DBX_TEST"); + expect(document.body.textContent).toContain("CODEX_7467_META"); + }); }); diff --git a/apps/desktop/src/composables/__tests__/useDatabaseOptions.spec.ts b/apps/desktop/src/composables/__tests__/useDatabaseOptions.spec.ts index 29988c64d2..080ce3cff8 100644 --- a/apps/desktop/src/composables/__tests__/useDatabaseOptions.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDatabaseOptions.spec.ts @@ -112,6 +112,31 @@ describe("namespace options", () => { expect(options).toEqual(["ARCHIVE"]); }); + it("uses schemas for native and inferred JDBC Oracle namespaces", async () => { + mocks.listSchemas.mockResolvedValue(["APP", "REPORTING", "SYS"]); + + await expect( + fetchNamespaceOptionsForConnection("oracle-1", { + db_type: "oracle", + database: "XE", + }), + ).resolves.toEqual(["APP", "REPORTING"]); + expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-1", "XE", true); + + vi.clearAllMocks(); + mocks.listSchemas.mockResolvedValue(["APP", "REPORTING", "SYS"]); + + await expect( + fetchNamespaceOptionsForConnection("oracle-jdbc-1", { + db_type: "jdbc", + connection_string: "jdbc:oracle:thin:@//localhost:1521/XE", + jdbc_driver_class: "oracle.jdbc.OracleDriver", + }), + ).resolves.toEqual(["APP", "REPORTING"]); + expect(mocks.listSchemas).toHaveBeenCalledWith("oracle-jdbc-1", "", true); + expect(mocks.listDatabases).not.toHaveBeenCalled(); + }); + it("preserves listDatabases and visible database filtering for other databases", async () => { mocks.listDatabases.mockResolvedValue([{ name: "app" }, { name: "analytics" }, { name: "postgres" }]); @@ -187,9 +212,15 @@ describe("namespace options", () => { ).resolves.toEqual(["APP_USER", "REPORTING"]); }); - it("identifies only Dameng top-level options as schemas", () => { + it("identifies connection-root namespace options as schemas", () => { expect(namespaceOptionsAreSchemas({ db_type: "dameng" })).toBe(true); - expect(namespaceOptionsAreSchemas({ db_type: "oracle" })).toBe(false); + expect(namespaceOptionsAreSchemas({ db_type: "oracle" })).toBe(true); + expect( + namespaceOptionsAreSchemas({ + db_type: "jdbc", + connection_string: "jdbc:oracle:thin:@//localhost:1521/XE", + }), + ).toBe(true); expect(namespaceOptionsAreSchemas({ db_type: "postgres" })).toBe(false); }); diff --git a/apps/desktop/src/composables/useDatabaseOptions.ts b/apps/desktop/src/composables/useDatabaseOptions.ts index 28ce03bcb0..0e6989c7d3 100644 --- a/apps/desktop/src/composables/useDatabaseOptions.ts +++ b/apps/desktop/src/composables/useDatabaseOptions.ts @@ -4,10 +4,12 @@ import { filterDatabaseNamesForConnection, filterSchemaNamesForConnection } from import { isDorisFamilyCatalogCapable, supportsQueryTargetDatabaseListing } from "@/lib/database/databaseFeatureSupport"; import { usesTreeSchemaMode } from "@/lib/database/databaseCapabilities"; import { isInternalDorisCatalog } from "@/lib/database/databaseFeatureSupport"; +import { connectionUsesConnectionRootSchemaMode } from "@/lib/database/jdbcDialect"; import type { CatalogInfo, ConnectionConfig } from "@/types/database"; import * as api from "@/lib/backend/api"; -type NamespaceOptionsConnection = Pick & Partial>; +type NamespaceOptionsConnection = Pick & + Partial>; export function catalogDatabaseOptionsKey(connectionId: string, catalog: string): string { return `${connectionId}:${catalog}`; } @@ -39,15 +41,16 @@ export function databaseOptionsForConnection(databaseNames: string[], connection return names; } -export function namespaceOptionsAreSchemas(connection: Pick | undefined): boolean { - return connection?.db_type === "dameng"; +export function namespaceOptionsAreSchemas(connection: NamespaceOptionsConnection | undefined): boolean { + return connectionUsesConnectionRootSchemaMode(connection); } export async function fetchNamespaceOptionsForConnection(connectionId: string, connection: NamespaceOptionsConnection): Promise { - if (connection.db_type === "dameng") { + if (namespaceOptionsAreSchemas(connection)) { const database = connection.database || ""; - // Dameng users and schemas are not interchangeable: independent schemas - // appear in listSchemas but not in the user-backed listDatabases result. + // Oracle, Dameng, and their inferred JDBC dialects expose schemas directly + // below the connection; their catalog/database listing is not the namespace + // picker used by schema-aware UI surfaces. const schemas = await api.listSchemas(connectionId, database, true); return filterSchemaNamesForConnection(schemas, connection, database); }