diff --git a/README.md b/README.md index 36c99fc17b..c56da1333a 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ ### 60+ Databases, One Tool -MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend DBX to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and more. Message queue admin is also available for Pulsar, Kafka, and RocketMQ. All in a single ~20 MB app. No bundled Chromium. +MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend DBX to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and more. Message queue admin is also available for Pulsar, Kafka, and RocketMQ. All in a single ~20 MB app. No bundled Chromium. ### Query Editor @@ -404,7 +404,7 @@ DBX is 20 MB with no runtime dependencies (no Java, no Python). It includes AI a
What databases are supported? -MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend support to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and custom JDBC connections. Message queue admin (Pulsar, Kafka, RocketMQ) is also supported. +MySQL, PostgreSQL, SQLite, Cloudflare D1, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, Qdrant, Milvus, Weaviate, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend support to H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, JDBCX, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase 8a/8s, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, ZooKeeper, Nacos, IRIS, and custom JDBC connections. Message queue admin (Pulsar, Kafka, RocketMQ) is also supported.
diff --git a/README.zh-CN.md b/README.zh-CN.md index 04d0d22caa..ee3e003e6a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -91,7 +91,7 @@ ### 60+ 种数据库,一个工具搞定 -MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等数据库都能直接连接。Agent/JDBC 方向的配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、IRIS 等。全部装进约 20 MB 的应用里,不内嵌 Chromium。 +MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等数据库都能直接连接。Agent/JDBC 方向的配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、IRIS 等。全部装进约 20 MB 的应用里,不内嵌 Chromium。 ### 查询编辑器 @@ -404,7 +404,7 @@ DBX 仅 20 MB,无需运行时依赖(无需 Java、无需 Python)。AI 和
支持哪些数据库? -MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Qdrant、Milvus、Weaviate、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等。JDBC 方向配置可扩展到 H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase 8a/8s、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、ZooKeeper、Nacos、IRIS 及自定义 JDBC 连接,并支持消息队列管理(Pulsar、Kafka、RocketMQ)。 +MySQL、PostgreSQL、SQLite、Cloudflare D1、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、Qdrant、Milvus、Weaviate、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等。JDBC 方向配置可扩展到 H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、JDBCX、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase 8a/8s、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、ZooKeeper、Nacos、IRIS 及自定义 JDBC 连接,并支持消息队列管理(Pulsar、Kafka、RocketMQ)。
diff --git a/apps/desktop/public/icons/database/jdbcx.svg b/apps/desktop/public/icons/database/jdbcx.svg new file mode 100644 index 0000000000..9bb01be83a --- /dev/null +++ b/apps/desktop/public/icons/database/jdbcx.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 4c96f331eb..c628815715 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -88,6 +88,7 @@ import { isSchemaAware, isSingleDatabase, usesTreeSchemaMode } from "@/lib/datab import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection"; +import { ensureJdbcxRuntimeDrivers } from "@/lib/database/jdbcxBuiltinDriver"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -117,6 +118,7 @@ const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); const savedSqlStore = useSavedSqlStore(); +connectionStore.setBeforeConnectHandler((config) => ensureJdbcxRuntimeDrivers(config, api).then(() => undefined)); const { message: toastMessage, visible: toastVisible, toast } = useToast(); const { isDark, themeMode, applyTheme, setThemeMode } = useTheme(); const { checkingUpdates, updateInfo, updateCheckMessage, showUpdateDialog, isDownloadingUpdate, downloadProgress, updateReady, hasUpdateAvailable, openUrl, checkUpdates, openLatestRelease, downloadAndInstallUpdate, restartApp } = useAppUpdater(); diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 26b5c2aa51..f28346e351 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -40,6 +40,7 @@ import { copyToClipboard } from "@/lib/common/clipboard"; import { configuredDatabaseProductName, connectionConfigFingerprint, databaseInfoCopyText, databaseInfoRows, normalizeDatabaseConnectionInfo, type DatabaseInfoField } from "@/lib/connection/connectionDatabaseInfo"; import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState, type DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver"; +import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensureJdbcxRuntimeDrivers } from "@/lib/database/jdbcxBuiltinDriver"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout"; import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints"; @@ -722,6 +723,7 @@ const driverProfiles: Record< db2: { type: "db2", port: 50000, user: "db2inst1", label: "IBM DB2", icon: "db2" }, informix: { type: "informix", port: 9088, user: "informix", label: "Informix", icon: "informix" }, dremio: { type: "jdbc", port: 31010, user: "", label: "Dremio", icon: "dremio" }, + jdbcx: { type: "jdbc", port: 0, user: "", label: "JDBCX", icon: "jdbcx" }, neo4j: { type: "neo4j", port: 7687, user: "neo4j", label: "Neo4j", icon: "neo4j" }, cassandra: { type: "cassandra", port: 9042, user: "cassandra", label: "Cassandra", icon: "cassandra" }, bigquery: { @@ -1198,6 +1200,21 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro } } +async function ensureRequiredJdbcxDriverInstalled(config: ConnectionConfig): Promise { + const result = await ensureJdbcxRuntimeDrivers(config, api, () => { + testResult.value = { ok: true, message: "Installing JDBC plugin..." }; + }); + if (!result) return; + + jdbcMavenBundles.value = result.bundles; + addJdbcDriverPaths(result.paths); + form.value.jdbc_driver_paths = [...(config.jdbc_driver_paths ?? [])]; + selectedJdbcDriverPath.value = result.paths.length > 0 ? (jdbcDriverSelectItems.value.find((item) => result.paths.every((path) => item.paths.includes(path)))?.id ?? "") : ""; + if (result.paths.length > 0) { + jdbcManualClasspathOpen.value = false; + } +} + async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise { if (await api.isAgentInstalled(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY)) return true; @@ -1470,6 +1487,9 @@ function applyProfile(val: string, preserveConnectionFields = false) { if (val === "dremio") { resetDremioConnectionUrls(); applyDremioConnectionMode("legacy"); + } else if (val === JDBCX_DRIVER_PROFILE) { + form.value.connection_string = JDBCX_DEFAULT_URL; + form.value.jdbc_driver_class = JDBCX_JDBC_DRIVER_CLASS; } } if (profile.type === "prestosql") { @@ -1836,6 +1856,7 @@ const iconTypeMap: Record = { db2: "db2", informix: "informix", dremio: "dremio", + jdbcx: "jdbcx", iris: "iris", neo4j: "neo4j", cassandra: "cassandra", @@ -1921,6 +1942,8 @@ const dbOptions: DbOption[] = [ { value: "nacos", label: "Nacos" }, { value: "influxdb", label: "InfluxDB" }, { value: "iris", label: "IRIS" }, + { value: "jdbc", label: "JDBC" }, + { value: "jdbcx", label: "JDBCX" }, { value: "manticoresearch", label: "Manticore Search" }, { value: "custom_mysql", label: "Custom (MySQL)" }, { value: "custom_postgres", label: "Custom (PostgreSQL)" }, @@ -2316,6 +2339,7 @@ async function testConnection() { try { config = connectionConfigForSubmit(editingId.value || draftTestConnectionId.value); await ensureRequiredAgentDriverInstalled(config); + await ensureRequiredJdbcxDriverInstalled(config); const result = await testConnectionWithTimeout(config, runId); if (runId !== testRunId) return; let successfulConfig = config; @@ -2710,6 +2734,11 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo if (config.db_type === "jdbc") { if (config.driver_profile === "dremio") { applyDremioJdbcMetadata(config); + } else if (config.driver_profile === JDBCX_DRIVER_PROFILE) { + config.host = ""; + config.port = 0; + config.connection_string = config.connection_string?.trim() || JDBCX_DEFAULT_URL; + config.jdbc_driver_class = config.jdbc_driver_class?.trim() || JDBCX_JDBC_DRIVER_CLASS; } else { config.host = ""; config.port = 0; @@ -3629,11 +3658,13 @@ async function save() { if (editingId.value) { const updated = withSavedDatabaseInfo(connectionConfigForSubmit(editingId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(updated); + await ensureRequiredJdbcxDriverInstalled(updated); await store.updateConnection(updated); store.stopEditing(); } else { const config = withSavedDatabaseInfo(connectionConfigForSubmit(draftTestConnectionId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(config); + await ensureRequiredJdbcxDriverInstalled(config); await store.addConnection(config); draftTestConnectionId.value = uuid(); if (config.db_type === "jdbc") { diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index b99cab5ddd..4de8ccabb6 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -92,6 +92,7 @@ const assetIcons: Record = { iris: "iris.png", influxdb: "influxdb", zookeeper: "zookeeper", + jdbcx: "jdbcx", }; const letterIcons: Record = { diff --git a/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts new file mode 100644 index 0000000000..8cd974fc47 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vitest"; +import { ensureJdbcxRuntimeDrivers, isJdbcxRuntimePath } from "@/lib/database/jdbcxBuiltinDriver"; +import type { ConnectionConfig } from "@/types/database"; + +function jdbcxConfig(): ConnectionConfig { + return { + id: "jdbcx-1", + name: "JDBCX database", + db_type: "jdbc", + driver_profile: "jdbcx", + driver_label: "JDBCX", + host: "", + port: 0, + username: "root", + password: "", + connection_string: "jdbcx:prql:vendor://127.0.0.1:1234/test", + jdbc_driver_paths: [], + }; +} + +function runtimeApi(paths: string[]) { + return { + listJdbcMavenBundles: async () => [], + listJdbcDrivers: async () => paths.map((path) => ({ name: path.split("/").at(-1) ?? path, path, size: 1 })), + jdbcPluginStatus: async () => ({ installed: true, compatible: true }), + installJdbcPlugin: vi.fn(async () => undefined), + }; +} + +describe("jdbcxBuiltinDriver", () => { + it("recognizes user-installed JDBCX runtime JARs", () => { + expect(isJdbcxRuntimePath("/drivers/jdbcx-driver-0.8.0.jar")).toBe(true); + expect(isJdbcxRuntimePath("C:\\drivers\\jdbcx-core-0.8.0.jar")).toBe(true); + expect(isJdbcxRuntimePath("/drivers/postgresql-42.7.7.jar")).toBe(false); + }); + + it("uses the user-installed JDBCX runtime and all delegate drivers", async () => { + const config = jdbcxConfig(); + const result = await ensureJdbcxRuntimeDrivers(config, runtimeApi(["/drivers/jdbcx-driver-0.8.0.jar", "/drivers/postgresql-42.7.7.jar", "/drivers/acme-proprietary-driver.jar"])); + + expect(result?.paths).toEqual(["/drivers/jdbcx-driver-0.8.0.jar", "/drivers/postgresql-42.7.7.jar", "/drivers/acme-proprietary-driver.jar"]); + expect(config.jdbc_driver_paths).toEqual(result?.paths); + }); + + it("asks the user to install JDBCX when its runtime is missing", async () => { + await expect(ensureJdbcxRuntimeDrivers(jdbcxConfig(), runtimeApi(["/drivers/mysql-connector-j-9.2.0.jar"]))).rejects.toThrow("Install io.github.jdbcx:jdbcx-driver: in Driver Store"); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/imports/jdbcxImport.spec.ts b/apps/desktop/src/lib/__tests__/imports/jdbcxImport.spec.ts new file mode 100644 index 0000000000..0bccdfa74d --- /dev/null +++ b/apps/desktop/src/lib/__tests__/imports/jdbcxImport.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { JDBCX_JDBC_DRIVER_CLASS } from "@/lib/database/jdbcxBuiltinDriver"; +import { parseDbeaverConnections } from "@/lib/imports/dbeaverImport"; + +describe("JDBCX connection import", () => { + it("imports JDBCX connections with the JDBCX profile", async () => { + const dataSources = JSON.stringify({ + connections: { + "jdbcx-1": { + id: "jdbcx-1", + name: "federated-query", + provider: "generic", + driver: "jdbcx", + configuration: { + url: "jdbcx:script:mysql://db.example.com:3306/app", + user: "analyst", + }, + }, + }, + }); + const payload = JSON.stringify({ format: "dbeaver-import", dataSources }); + + const [connection] = await parseDbeaverConnections(payload); + + expect(connection?.db_type).toBe("jdbc"); + expect(connection?.driver_profile).toBe("jdbcx"); + expect(connection?.driver_label).toBe("JDBCX"); + expect(connection?.connection_string).toBe("jdbcx:script:mysql://db.example.com:3306/app"); + expect(connection?.jdbc_driver_class).toBe(JDBCX_JDBC_DRIVER_CLASS); + }); +}); diff --git a/apps/desktop/src/lib/database/jdbcxBuiltinDriver.ts b/apps/desktop/src/lib/database/jdbcxBuiltinDriver.ts new file mode 100644 index 0000000000..fe156b1462 --- /dev/null +++ b/apps/desktop/src/lib/database/jdbcxBuiltinDriver.ts @@ -0,0 +1,47 @@ +import type { ConnectionConfig, JdbcDriverInfo, JdbcMavenBundleInfo } from "@/types/database"; + +export const JDBCX_DRIVER_PROFILE = "jdbcx"; +export const JDBCX_JDBC_DRIVER_CLASS = "io.github.jdbcx.WrappedDriver"; +export const JDBCX_DEFAULT_URL = "jdbcx:"; + +export type JdbcxRuntimeDriverApi = { + listJdbcDrivers: () => Promise; + listJdbcMavenBundles: () => Promise; + jdbcPluginStatus: () => Promise<{ installed: boolean; compatible: boolean }>; + installJdbcPlugin: () => Promise; +}; + +export type JdbcxRuntimeDriverResult = { + bundles: JdbcMavenBundleInfo[]; + paths: string[]; +}; + +export function isJdbcxRuntimePath(path: string): boolean { + return /(?:^|[/\\])jdbcx-(?:driver|core)(?:-|\.)/i.test(path); +} + +export async function ensureJdbcxRuntimeDrivers(config: ConnectionConfig, api: JdbcxRuntimeDriverApi, onInstalling?: (coordinates: string[]) => void): Promise { + if (config.db_type !== "jdbc" || config.driver_profile !== JDBCX_DRIVER_PROFILE) return undefined; + + config.connection_string = config.connection_string?.trim() || JDBCX_DEFAULT_URL; + config.jdbc_driver_class = config.jdbc_driver_class?.trim() || JDBCX_JDBC_DRIVER_CLASS; + const configuredPaths = (config.jdbc_driver_paths ?? []).map((path) => path.trim()).filter(Boolean); + const pluginStatus = await api.jdbcPluginStatus(); + if (!pluginStatus.installed || !pluginStatus.compatible) { + onInstalling?.([]); + await api.installJdbcPlugin(); + } + + const [bundles, installedDrivers] = await Promise.all([api.listJdbcMavenBundles(), api.listJdbcDrivers()]); + const installedPaths = installedDrivers.map((driver) => driver.path).filter(Boolean); + if (![...configuredPaths, ...installedPaths].some(isJdbcxRuntimePath)) { + throw new Error("JDBCX runtime is not installed. Install io.github.jdbcx:jdbcx-driver: in Driver Store, then retry."); + } + + // JDBCX discovers the delegate driver through JDBC ServiceLoader/Driver.acceptsURL. + // Supplying the DBX driver-store classpath keeps this vendor-neutral: any Maven + // bundle or local JAR imported by the user becomes available without URL-specific code. + const paths = Array.from(new Set(installedPaths)); + config.jdbc_driver_paths = Array.from(new Set([...configuredPaths, ...paths])); + return { bundles, paths }; +} diff --git a/apps/desktop/src/lib/imports/dbeaverImport.ts b/apps/desktop/src/lib/imports/dbeaverImport.ts index a66d0ac80f..1b598a4b38 100644 --- a/apps/desktop/src/lib/imports/dbeaverImport.ts +++ b/apps/desktop/src/lib/imports/dbeaverImport.ts @@ -1,5 +1,6 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database"; import { uuid } from "@/lib/common/utils"; +import { JDBCX_JDBC_DRIVER_CLASS } from "@/lib/database/jdbcxBuiltinDriver"; type PartialConnection = Omit; @@ -52,6 +53,7 @@ const profileMap: Record = { opengauss: { dbType: "gaussdb", profile: "opengauss", label: "openGauss", port: 5432, user: "gaussdb" }, questdb: { dbType: "questdb", profile: "questdb", label: "QuestDB", port: 8812, user: "questdb" }, influxdb: { dbType: "influxdb", profile: "influxdb", label: "InfluxDB", port: 8086, user: "" }, + jdbcx: { dbType: "jdbc", profile: "jdbcx", label: "JDBCX", port: 0, user: "" }, }; function normalizeKey(value: unknown) { @@ -70,6 +72,8 @@ function getNumber(value: unknown) { } function inferProfile(entry: DbeaverConnectionEntry): ConnectionProfile { + const driverProfile = profileMap[normalizeKey(entry.driver)]; + if (driverProfile) return driverProfile; const candidates = [entry.provider, entry.driver, entry.configuration?.url, entry.name].map(normalizeKey).join(" "); for (const [needle, profile] of Object.entries(profileMap)) { if (candidates.includes(needle)) return profile; @@ -218,7 +222,7 @@ function buildConnection(entry: DbeaverConnectionEntry, credentials: ReturnType< ssl: false, oracle_connection_type: profile.dbType === "oracle" ? parsedUrl.oracleConnectionType || "service_name" : undefined, connection_string: profile.dbType === "jdbc" || profile.dbType === "mongodb" ? url || undefined : undefined, - jdbc_driver_class: profile.dbType === "jdbc" ? getString(config["driver-class"] || entry.driver) || undefined : undefined, + jdbc_driver_class: profile.dbType === "jdbc" ? getString(config["driver-class"] || (profile.profile === "jdbcx" ? JDBCX_JDBC_DRIVER_CLASS : entry.driver)) || undefined : undefined, jdbc_driver_paths: [], }; diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 0d539c4d4c..aafb6f46ef 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -1861,7 +1861,7 @@ fn rewrite_mongo_uri_host(uri: &str, new_host: &str, new_port: u16) -> String { } pub fn parse_jdbc_host_port(url: &str) -> Option<(String, u16)> { - let rest = url.strip_prefix("jdbc:")?; + let rest = jdbc_transport_rest(url)?; // jdbc:oracle:thin:@host:port:SID or jdbc:oracle:thin:@//host:port/service if let Some(after) = rest.strip_prefix("oracle:") { @@ -1903,11 +1903,27 @@ pub fn parse_jdbc_host_port(url: &str) -> Option<(String, u16)> { } } +fn jdbc_transport_rest(url: &str) -> Option<&str> { + if let Some(rest) = url.strip_prefix("jdbc:").or_else(|| url.strip_prefix("JDBC:")) { + return Some(rest); + } + + let rest = url.strip_prefix("jdbcx:").or_else(|| url.strip_prefix("JDBCX:"))?; + let (first, tail) = rest.split_once(':').unwrap_or((rest, "")); + if matches!( + first.to_ascii_lowercase().as_str(), + "codeql" | "graphql" | "prompt" | "prql" | "rest" | "script" | "shell" | "sql" + ) { + Some(tail) + } else { + Some(rest) + } +} + pub fn rewrite_jdbc_url_host(url: &str, new_host: &str, new_port: u16) -> String { let normalized_url = url.to_ascii_uppercase(); - if normalized_url.starts_with("JDBC:ORACLE:") - && normalized_url.contains("(HOST=") - && normalized_url.contains("(PORT=") + let normalized_rest = jdbc_transport_rest(url).map(str::to_ascii_uppercase).unwrap_or_default(); + if normalized_rest.starts_with("ORACLE:") && normalized_url.contains("(HOST=") && normalized_url.contains("(PORT=") { return rewrite_oracle_descriptor_host(url, new_host, new_port); } @@ -3135,6 +3151,18 @@ mod tests { assert_eq!(p, 3306); } + #[test] + fn parse_jdbcx_host_port_with_and_without_extension() { + assert_eq!( + super::parse_jdbc_host_port("jdbcx:mysql://db.example.com:3306/app"), + Some(("db.example.com".to_string(), 3306)) + ); + assert_eq!( + super::parse_jdbc_host_port("jdbcx:prql:postgresql://pg.example.com:5432/app"), + Some(("pg.example.com".to_string(), 5432)) + ); + } + #[test] fn parse_jdbc_host_port_with_userinfo() { let (h, p) = super::parse_jdbc_host_port("jdbc:postgresql://user:pass@pghost:5433/db").unwrap(); @@ -3208,6 +3236,23 @@ mod tests { assert_eq!(rewritten, "jdbc:postgresql://127.0.0.1:54321/mydb"); } + #[test] + fn rewrite_jdbcx_url_preserves_extension() { + let url = "jdbcx:script:mysql://db.example.com:3306/app"; + let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 13306); + assert_eq!(rewritten, "jdbcx:script:mysql://127.0.0.1:13306/app"); + } + + #[test] + fn rewrite_jdbcx_oracle_descriptor() { + let url = "jdbcx:sql:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=orahost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orcl)))"; + let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 11521); + assert_eq!( + rewritten, + "jdbcx:sql:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=11521))(CONNECT_DATA=(SERVICE_NAME=orcl)))" + ); + } + #[test] fn rewrite_jdbc_url_oracle() { let url = "jdbc:oracle:thin:@orahost:1521:ORCL"; diff --git a/plugins/jdbc/README.md b/plugins/jdbc/README.md index f69d3c029b..4b652277ce 100644 --- a/plugins/jdbc/README.md +++ b/plugins/jdbc/README.md @@ -36,3 +36,8 @@ lib/dbx-jdbc-plugin.jar ``` DBX does not bundle Java or JDBC drivers. Install Java locally and add database-specific driver JAR paths in the DBX JDBC connection form. + +The first-class JDBCX profile uses `io.github.jdbcx.WrappedDriver` and +`jdbcx:[extension:][vendor://host:port/database]` URLs. Install a JDBCX Maven bundle such as +`io.github.jdbcx:jdbcx-driver:0.8.0` in the DBX JDBC driver store, together with the database vendor's JDBC driver. +JDBCX discovers delegate drivers through JDBC `ServiceLoader`/`Driver.acceptsURL`, without vendor-specific DBX code.