diff --git a/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts b/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts index f7ba726477..b9281bf4f8 100644 --- a/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts +++ b/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { SidebarLayout, SidebarOrderEntry } from "@/types/database"; -import { parseDbeaverConnections, parseDbeaverImport } from "@/lib/imports/dbeaverImport"; +import type { SidebarLayout, SidebarOrderEntry } from "../../../types/database"; +import { parseDbeaverConnections, parseDbeaverImport } from "../../imports/dbeaverImport"; function payload(dataSources: Record) { return JSON.stringify({ format: "dbeaver-import", dataSources: JSON.stringify(dataSources) }); @@ -17,6 +17,16 @@ function mysqlConnection(id: string, name: string, folder?: string) { }; } +function clickhouseConnection(id: string, name: string, configuration: Record) { + return { + id, + name, + provider: "clickhouse", + driver: "com_clickhouse", + configuration, + }; +} + function layoutLabels(layout: SidebarLayout, connectionNames: Map): unknown[] { const groupNames = new Map(layout.groups.map((group) => [group.id, group.name])); const visit = (entries: SidebarOrderEntry[]): unknown[] => entries.map((entry) => (entry.type === "connection" ? connectionNames.get(entry.id) : { group: groupNames.get(entry.id), children: visit(entry.children ?? []) })); @@ -50,6 +60,251 @@ describe("DBeaver folder import", () => { expect(connection?.database).toBeUndefined(); }); + it("keeps the default database empty for MySQL root JDBC URLs", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + mysql: { + id: "mysql", + name: "208", + provider: "mysql", + driver: "mysql8", + configuration: { + host: "192.168.3.12", + port: "51345", + url: "jdbc:mysql://192.168.3.12:51345/", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "208", + db_type: "mysql", + host: "192.168.3.12", + port: 51345, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for ClickHouse root JDBC URLs", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + clickhouse: clickhouseConnection("clickhouse", "CH", { + host: "192.168.1.39", + port: "8123", + url: "jdbc:clickhouse://192.168.1.39:8123", + }), + }, + }), + ); + + expect(connection).toMatchObject({ + name: "CH", + db_type: "clickhouse", + host: "192.168.1.39", + port: 8123, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for SQL Server connections without databaseName", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + sqlserver: { + id: "sqlserver", + name: "MSSQL", + provider: "sqlserver", + driver: "mssql_jdbc", + configuration: { + host: "192.168.10.25", + port: "1433", + url: "jdbc:sqlserver://192.168.10.25:1433;encrypt=true", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "MSSQL", + db_type: "sqlserver", + host: "192.168.10.25", + port: 1433, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for Oracle connections without a configured service", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + oracle: { + id: "oracle", + name: "Oracle", + provider: "oracle", + driver: "oracle_jdbc", + configuration: { + host: "192.168.10.30", + port: "1521", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "Oracle", + db_type: "oracle", + host: "192.168.10.30", + port: 1521, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for PostgreSQL root JDBC URLs", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + postgres: { + id: "postgres", + name: "PG", + provider: "postgresql", + driver: "postgres-jdbc", + configuration: { + host: "192.168.10.40", + port: "5432", + url: "jdbc:postgresql://192.168.10.40:5432/", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "PG", + db_type: "postgres", + host: "192.168.10.40", + port: 5432, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for MariaDB root JDBC URLs", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + mariadb: { + id: "mariadb", + name: "MariaDB", + provider: "mysql", + driver: "mariadb", + configuration: { + host: "192.168.10.50", + port: "3306", + url: "jdbc:mariadb://192.168.10.50:3306/", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "MariaDB", + db_type: "mysql", + driver_profile: "mariadb", + host: "192.168.10.50", + port: 3306, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("keeps the default database empty for DB2 connections (fallback to JDBC) without database configured", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + db2: { + id: "db2", + name: "DB2", + provider: "db2", + driver: "db2", + configuration: { + host: "192.168.10.60", + port: "50000", + url: "jdbc:db2://192.168.10.60:50000/", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "DB2", + db_type: "jdbc", + host: "192.168.10.60", + port: 50000, + }); + expect(connection?.database).toBeUndefined(); + }); + + it("does not treat an opaque JDBC host and port as the database", async () => { + const [connection] = await parseDbeaverConnections( + payload({ + connections: { + opaque: { + id: "opaque", + name: "Opaque JDBC", + provider: "generic", + driver: "com.example.Driver", + configuration: { + url: "jdbc:example:db.example.com:1234", + }, + }, + }, + }), + ); + + expect(connection).toMatchObject({ + name: "Opaque JDBC", + db_type: "jdbc", + connection_string: "jdbc:example:db.example.com:1234", + }); + expect(connection?.database).toBeUndefined(); + }); + + it("preserves explicit database names that resemble the host or port", async () => { + const connections = await parseDbeaverConnections( + payload({ + connections: { + mysql: { + id: "mysql", + name: "208", + provider: "mysql", + driver: "mysql8", + configuration: { + host: "192.168.3.12", + port: "51345", + database: "192.168.3.12:51345", + url: "jdbc:mysql://192.168.3.12:51345/", + }, + }, + clickhouse: clickhouseConnection("clickhouse", "CH", { + host: "192.168.1.39", + port: "8123", + database: "8123", + url: "jdbc:clickhouse://192.168.1.39:8123", + }), + }, + }), + ); + + expect(connections[0]?.database).toBe("192.168.3.12:51345"); + expect(connections[1]?.database).toBe("8123"); + }); + it("keeps parseDbeaverConnections compatible when no folders exist", async () => { const connections = await parseDbeaverConnections(payload({ connections: { root: mysqlConnection("root", "Root") } })); diff --git a/apps/desktop/src/lib/imports/dbeaverImport.ts b/apps/desktop/src/lib/imports/dbeaverImport.ts index 1bb5023e29..c00d230838 100644 --- a/apps/desktop/src/lib/imports/dbeaverImport.ts +++ b/apps/desktop/src/lib/imports/dbeaverImport.ts @@ -81,6 +81,15 @@ function getNumber(value: unknown) { return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } +function firstNonEmptyString(...values: unknown[]) { + for (const value of values) { + if (typeof value !== "string") continue; + const normalized = value.trim(); + if (normalized) return normalized; + } + return ""; +} + function inferProfile(entry: DbeaverConnectionEntry): ConnectionProfile { if (/^jdbcx:/i.test(getString(entry.configuration?.url))) return profileMap.jdbcx; if (normalizeKey(entry.provider) === "opentenbase") return profileMap.opentenbase; @@ -179,6 +188,7 @@ function parseJdbcUrl(url: string, profile: ConnectionProfile) { try { const parsed = new URL(withoutJdbc); + if (!parsed.hostname) return result; result.host = parsed.hostname; result.port = parsed.port ? Number(parsed.port) : profile.port; result.database = parsed.pathname.replace(/^\/+/, "").split("/")[0] || undefined; @@ -223,7 +233,8 @@ function buildConnection(entry: DbeaverConnectionEntry, credentials: ReturnType< const config = entry.configuration || {}; const url = getString(config.url); const parsedUrl = parseJdbcUrl(url, profile); - const configuredDatabase = getString(config.database || config["database-name"] || config.schema || parsedUrl.database); + const configuredPort = getNumber(config.port || config["host-port"] || parsedUrl.port) || profile.port; + const configuredDatabase = firstNonEmptyString(config.database, config["database-name"], config.schema, parsedUrl.database); const host = getString(config.host || config["host-name"] || parsedUrl.host || (profile.dbType === "sqlite" ? configuredDatabase : "127.0.0.1")); const database = profile.dbType === "sqlite" ? "" : configuredDatabase; const name = getString(entry.name || database || host || profile.label); @@ -236,7 +247,7 @@ function buildConnection(entry: DbeaverConnectionEntry, credentials: ReturnType< driver_label: profile.label, url_params: getString(parsedUrl.params), host, - port: getNumber(config.port || config["host-port"] || parsedUrl.port) || profile.port, + port: configuredPort, username: credentials.username || getString(parsedUrl.username) || profile.user, password: credentials.password || getString(parsedUrl.password), database: database || undefined,