From 44884efa9bfda70f113a92bea8797647c22ebd51 Mon Sep 17 00:00:00 2001 From: Orwen Xiang Date: Sat, 27 Jun 2026 20:03:16 +0800 Subject: [PATCH] feat(h2): add split JDBC URL support --- .../connection/ConnectionDialog.vue | 82 +++++++--- apps/desktop/src/lib/connectionUrl.ts | 21 ++- apps/desktop/src/lib/h2Connection.ts | 140 +++++++++++++++++- crates/dbx-core/src/agent_connection.rs | 45 ++++-- packages/app-tests/connectionUrl.test.ts | 56 ++++++- 5 files changed, 305 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index a200a44f87..2e933a40cd 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -26,7 +26,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime"; import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl"; import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; -import { h2ConnectionModeForConfig, h2FileJdbcUrl, h2FilePathFromJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection"; +import { h2ConnectionModeForConfig, h2FileJdbcUrlWithPath, h2FilePathFromJdbcUrl, isH2SplitJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection"; import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection"; import { isLocalFileTypeDb } from "@/lib/connectionFile"; import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mqPinnedVersionOptions"; @@ -302,6 +302,7 @@ const agentDrivers = ref([]); const selectedJdbcDriverPath = ref(""); const jdbcManualClasspathOpen = ref(false); const connectionUrlInput = ref(""); +const appliedConnectionUrlInput = ref(""); const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql"); const h2ConnectionMode = ref("file"); const dialogStep = ref("select"); @@ -898,7 +899,7 @@ watch( driver_profile: oceanbasePatch?.driver_profile || profile, driver_label: config.driver_label || oceanbasePatch?.driver_label || driverProfiles[profile]?.label || config.db_type, url_params: config.url_params || "", - host: config.db_type === "h2" ? config.host || h2FilePathFromJdbcUrl(config.connection_string) : config.host, + host: config.db_type === "h2" && h2FilePathFromJdbcUrl(config.connection_string) ? h2FilePathFromJdbcUrl(config.connection_string) : config.host, port: profile === "tdengine" && (config.port === 0 || config.port === 6030) ? 6041 : config.port, username: config.username, password: config.password, @@ -932,6 +933,8 @@ watch( read_only: config.read_only || false, visible_databases: config.visible_databases, }; + connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : ""; + appliedConnectionUrlInput.value = connectionUrlInput.value.trim(); if (config.db_type === "mq") { hydrateMqFields(config.external_config); } else { @@ -1476,6 +1479,8 @@ function applyConnectionUrlToForm(input: string): boolean { const draft = parseConnectionDeepLink(input); if (draft) { applyConnectionDraftToForm({ ...draft, oneTime: undefined }); + resetTestState(); + appliedConnectionUrlInput.value = input.trim(); return true; } @@ -1484,10 +1489,14 @@ function applyConnectionUrlToForm(input: string): boolean { selectedType.value = parsed.driverProfile; customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : ""; mongoUseUrl.value = !!parsed.useMongoUrl; + if (form.value.db_type === "h2") { + h2ConnectionMode.value = h2ConnectionModeForConfig(form.value); + } if (!form.value.name.trim()) { form.value.name = parsed.database || parsed.host || parsed.driverLabel; } resetTestState(); + appliedConnectionUrlInput.value = input.trim(); return true; } catch (e: any) { toast(t("connection.parseConnectionUrlFailed", { message: e?.message || String(e) }), 5000); @@ -1495,11 +1504,26 @@ function applyConnectionUrlToForm(input: string): boolean { } } -function ensureConnectionHostResolvedFromUrl(): boolean { - if (form.value.host.trim()) return true; +function hasPendingConnectionUrlInput(): boolean { const url = connectionUrlInput.value.trim(); - if (!url) return true; - return applyConnectionUrlToForm(url); + return !!url && url !== appliedConnectionUrlInput.value; +} + +function ensureConnectionHostResolvedFromUrl(): boolean { + if (!hasPendingConnectionUrlInput()) return true; + return applyConnectionUrlToForm(connectionUrlInput.value.trim()); +} + +function formValueForSubmit(): Omit { + const url = connectionUrlInput.value.trim(); + if (!url || url === appliedConnectionUrlInput.value) return form.value; + + const draft = parseConnectionDeepLink(url); + if (draft) { + return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined }); + } + + return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value)); } function generateConnectionName(): string { @@ -1509,8 +1533,8 @@ function generateConnectionName(): string { } function connectionConfigForSubmit(id: string): ConnectionConfig { - const config = { ...form.value, id } as LegacyConnectionConfig; - if (selectedType.value === "oceanbase") { + const config = { ...formValueForSubmit(), id } as LegacyConnectionConfig; + if (selectedType.value === "oceanbase" && (config.driver_profile === "oceanbase" || config.driver_profile === "oceanbase-oracle")) { Object.assign(config, oceanbaseModeConnectionPatch(oceanbaseSubMode.value)); } if (!config.name?.trim()) { @@ -1679,14 +1703,16 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { .filter(Boolean); } if (config.db_type === "h2") { - if (h2ConnectionMode.value === "file") { - const filePath = config.host?.trim() || h2FilePathFromJdbcUrl(config.connection_string); + const h2Mode = connectionUrlInput.value.trim() ? h2ConnectionModeForConfig(config) : h2ConnectionMode.value; + if (h2Mode === "file") { + const jdbcFilePath = h2FilePathFromJdbcUrl(config.connection_string); + const filePath = config.host?.trim() || jdbcFilePath || ""; if (!filePath) { throw new Error(t("connection.h2FilePathRequired")); } config.host = filePath; config.port = 0; - config.connection_string = h2FileJdbcUrl(filePath); + config.connection_string = isH2SplitJdbcUrl(config.connection_string) ? h2FileJdbcUrlWithPath(config.connection_string, filePath) : h2FileJdbcUrlWithPath(undefined, filePath); config.transport_layers = []; } else { config.host = config.host?.trim() || "127.0.0.1"; @@ -2138,6 +2164,7 @@ function resetForm() { jdbcDriverPathsInput.value = ""; selectedJdbcDriverPath.value = ""; connectionUrlInput.value = ""; + appliedConnectionUrlInput.value = ""; dialogStep.value = "select"; dbPickerView.value = "icon"; dbSearchQuery.value = ""; @@ -2161,25 +2188,32 @@ function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) { void nextTick(() => save()); } -function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) { - applyProfile(draft.driverProfile); - form.value = { - ...form.value, +function applyConnectionDraftToConfig(config: Omit, draft: ConnectionDeepLinkDraft): Omit { + return { + ...config, db_type: draft.dbType, driver_profile: draft.driverProfile, driver_label: draft.driverLabel, - host: draft.host ?? form.value.host, - port: draft.port ?? form.value.port, - username: draft.username ?? form.value.username, - password: draft.password ?? form.value.password, - database: draft.database ?? form.value.database, - url_params: draft.urlParams ?? form.value.url_params, - ssl: draft.ssl ?? form.value.ssl, - connection_string: draft.connectionString ?? form.value.connection_string, - oracle_connection_type: draft.oracleConnectionType ?? form.value.oracle_connection_type, + host: draft.host ?? config.host, + port: draft.port ?? config.port, + username: draft.username ?? config.username, + password: draft.password ?? config.password, + database: draft.database ?? config.database, + url_params: draft.urlParams ?? config.url_params, + ssl: draft.ssl ?? config.ssl, + connection_string: draft.connectionString ?? config.connection_string, + oracle_connection_type: draft.oracleConnectionType ?? config.oracle_connection_type, one_time: draft.oneTime || undefined, }; +} + +function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) { + applyProfile(draft.driverProfile); + form.value = applyConnectionDraftToConfig(form.value, draft); selectedType.value = draft.driverProfile; + if (form.value.db_type === "h2") { + h2ConnectionMode.value = h2ConnectionModeForConfig(form.value); + } if (draft.driverProfile === "oceanbase-oracle") { oceanbaseSubMode.value = "oracle"; selectedType.value = "oceanbase"; diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts index 6e11e72131..34676c14c7 100644 --- a/apps/desktop/src/lib/connectionUrl.ts +++ b/apps/desktop/src/lib/connectionUrl.ts @@ -1,4 +1,5 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database"; +import { h2JdbcUrlHasPasswordParam, h2JdbcUrlHasUserParam, parseH2JdbcUrl } from "@/lib/h2Connection"; export interface ParsedConnectionUrl { name?: string; @@ -423,6 +424,8 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa if (!input) { throw new Error("Connection URL is empty"); } + const jdbcH2 = parseH2JdbcUrl(input); + if (jdbcH2) return jdbcH2; const jdbcUCanAccess = parseJdbcUCanAccessUrl(input); if (jdbcUCanAccess) return jdbcUCanAccess; const jdbcGbase8s = parseJdbcGbase8sUrl(input); @@ -515,6 +518,20 @@ function zookeeperConnectStringFromUrl(parsed: URL, defaultPort: number): string return `${host}:${port}${chroot}`; } +function applyParsedUsername(config: Omit, parsed: ParsedConnectionUrl): string { + if (parsed.dbType === "h2" && config.db_type === "h2" && !h2JdbcUrlHasUserParam(parsed.connectionString)) { + return config.username || parsed.username; + } + return parsed.username; +} + +function applyParsedPassword(config: Omit, parsed: ParsedConnectionUrl): string { + if (parsed.dbType === "h2" && config.db_type === "h2" && !h2JdbcUrlHasPasswordParam(parsed.connectionString)) { + return config.password || parsed.password; + } + return parsed.password; +} + export function applyParsedConnectionUrl(config: Omit, parsed: ParsedConnectionUrl): Omit { return { ...config, @@ -524,8 +541,8 @@ export function applyParsedConnectionUrl(config: Omit, p host: parsed.host, port: parsed.port, name: parsed.name?.trim() || config.name, - username: parsed.username, - password: parsed.password, + username: applyParsedUsername(config, parsed), + password: applyParsedPassword(config, parsed), database: parsed.database, url_params: parsed.urlParams, ssl: parsed.ssl, diff --git a/apps/desktop/src/lib/h2Connection.ts b/apps/desktop/src/lib/h2Connection.ts index 9e2736dff5..299ecdbef4 100644 --- a/apps/desktop/src/lib/h2Connection.ts +++ b/apps/desktop/src/lib/h2Connection.ts @@ -3,15 +3,149 @@ import type { ConnectionConfig } from "@/types/database"; export type H2ConnectionMode = "file" | "tcp"; const H2_FILE_PREFIX = "jdbc:h2:file:"; +const H2_SPLIT_PREFIX = "jdbc:h2:split:"; +const H2_FILE_PREFIXES = [H2_FILE_PREFIX, H2_SPLIT_PREFIX]; +const H2_DEFAULT_PORT = 9092; +const H2_PARSED_PROFILE = { + dbType: "h2" as const, + driverProfile: "h2" as const, + driverLabel: "H2" as const, +}; + +function decodeH2UrlPart(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function extractH2JdbcSemicolonParams(source: string): { username?: string; password?: string; urlParams: string } { + let username: string | undefined; + let password: string | undefined; + const urlParams: string[] = []; + + for (const part of source.split(";").slice(1)) { + if (!part) continue; + const [rawKey, ...rest] = part.split("="); + const key = decodeH2UrlPart(rawKey).trim().toLowerCase(); + const value = decodeH2UrlPart(rest.join("=")).trim(); + if (key === "user") { + username = value; + } else if (key === "password") { + password = value; + } else { + urlParams.push(part); + } + } + + return { username, password, urlParams: urlParams.join(";") }; +} + +function hasH2JdbcSemicolonParam(source: string | undefined, name: string): boolean { + for (const part of (source || "").split(";").slice(1)) { + if (!part) continue; + const [rawKey] = part.split("="); + if (decodeH2UrlPart(rawKey).trim().toLowerCase() === name) return true; + } + return false; +} + +export function h2JdbcUrlHasUserParam(source: string | undefined): boolean { + return hasH2JdbcSemicolonParam(source, "user"); +} + +export function h2JdbcUrlHasPasswordParam(source: string | undefined): boolean { + return hasH2JdbcSemicolonParam(source, "password"); +} + +function databaseNameFromPath(path: string): string | undefined { + const name = path.split(/[\\/]/).filter(Boolean).pop()?.trim(); + return name || undefined; +} + +function h2FileJdbcUrlPrefix(value: string): string | undefined { + const lower = value.toLowerCase(); + return H2_FILE_PREFIXES.find((prefix) => lower.startsWith(prefix)); +} export function isH2FileJdbcUrl(value: string | undefined | null): boolean { - return (value || "").trim().toLowerCase().startsWith(H2_FILE_PREFIX); + return h2FileJdbcUrlPrefix((value || "").trim()) !== undefined; } export function h2FilePathFromJdbcUrl(value: string | undefined | null): string { const trimmed = (value || "").trim(); - if (!isH2FileJdbcUrl(trimmed)) return ""; - return trimmed.slice(H2_FILE_PREFIX.length).split(";")[0] || ""; + const prefix = h2FileJdbcUrlPrefix(trimmed); + if (!prefix) return ""; + const rawPath = trimmed.slice(prefix.length).split(";")[0] || ""; + if (prefix === H2_SPLIT_PREFIX) { + return rawPath.replace(/^\d+:/, ""); + } + return rawPath; +} + +export function isH2SplitJdbcUrl(value: string | undefined | null): value is string { + return (value || "").trim().toLowerCase().startsWith(H2_SPLIT_PREFIX); +} + +export function h2FileJdbcUrlWithPath(source: string | undefined | null, path: string): string { + const trimmed = (source || "").trim(); + if (!isH2SplitJdbcUrl(trimmed)) return h2FileJdbcUrl(path); + + const rest = trimmed.slice(H2_SPLIT_PREFIX.length); + const [rawPath, ...optionParts] = rest.split(";"); + const blockPrefix = rawPath.match(/^\d+:/)?.[0] || ""; + const options = optionParts.length > 0 ? `;${optionParts.join(";")}` : ""; + return `${H2_SPLIT_PREFIX}${blockPrefix}${h2JdbcFileBasePath(path)}${options}`; +} + +export function parseH2JdbcUrl(source: string) { + const trimmed = source.trim(); + if (!/^jdbc:h2:/i.test(trimmed)) return null; + + const credentials = extractH2JdbcSemicolonParams(trimmed); + const filePath = h2FilePathFromJdbcUrl(trimmed); + if (filePath) { + return { + ...H2_PARSED_PROFILE, + host: filePath, + port: 0, + username: credentials.username || "sa", + password: credentials.password || "", + database: databaseNameFromPath(filePath), + urlParams: credentials.urlParams, + ssl: false, + connectionString: trimmed, + }; + } + + const serverMatch = /^jdbc:h2:(?tcp|ssl):\/\/(?\[[^\]]+\]|[^:/;]+)(?::(?\d+))?\/(?[^;]*)/i.exec(trimmed); + if (serverMatch?.groups) { + return { + ...H2_PARSED_PROFILE, + host: serverMatch.groups.host.replace(/^\[/, "").replace(/\]$/, ""), + port: serverMatch.groups.port ? Number(serverMatch.groups.port) : H2_DEFAULT_PORT, + username: credentials.username || "sa", + password: credentials.password || "", + database: decodeH2UrlPart(serverMatch.groups.database || "") || undefined, + urlParams: credentials.urlParams, + ssl: serverMatch.groups.protocol.toLowerCase() === "ssl", + connectionString: trimmed, + }; + } + + const database = trimmed.replace(/^jdbc:h2:/i, "").split(";")[0] || undefined; + return { + ...H2_PARSED_PROFILE, + host: "", + port: H2_DEFAULT_PORT, + username: credentials.username || "sa", + password: credentials.password || "", + database: database ? decodeH2UrlPart(database) : undefined, + urlParams: credentials.urlParams, + ssl: false, + connectionString: trimmed, + }; } export function h2JdbcFileBasePath(path: string): string { diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs index 1de88a64ff..d2887334e8 100644 --- a/crates/dbx-core/src/agent_connection.rs +++ b/crates/dbx-core/src/agent_connection.rs @@ -143,11 +143,24 @@ pub fn h2_jdbc_file_base_path(path: &str) -> String { pub fn h2_file_path_from_jdbc_url(connection_string: &str) -> Option { let connection_string = connection_string.trim(); - let prefix = "jdbc:h2:file:"; - if connection_string.get(..prefix.len())?.eq_ignore_ascii_case(prefix) { - return Some(connection_string[prefix.len()..].split(';').next().unwrap_or("").to_string()); - } - None + h2_file_jdbc_url_prefix(connection_string).map(|prefix| { + let raw_path = connection_string[prefix.len()..].split(';').next().unwrap_or(""); + if prefix.eq_ignore_ascii_case("jdbc:h2:split:") { + raw_path + .split_once(':') + .and_then(|(block_size, path)| block_size.chars().all(|ch| ch.is_ascii_digit()).then_some(path)) + .unwrap_or(raw_path) + .to_string() + } else { + raw_path.to_string() + } + }) +} + +fn h2_file_jdbc_url_prefix(connection_string: &str) -> Option<&'static str> { + ["jdbc:h2:file:", "jdbc:h2:split:"] + .into_iter() + .find(|prefix| connection_string.get(..prefix.len()).is_some_and(|value| value.eq_ignore_ascii_case(prefix))) } fn normalize_h2_file_jdbc_url(connection_string: &str) -> Option { @@ -167,10 +180,7 @@ fn normalize_h2_file_jdbc_url(connection_string: &str) -> Option { } fn is_h2_file_jdbc_url(connection_string: &str) -> bool { - connection_string - .trim() - .get(.."jdbc:h2:file:".len()) - .is_some_and(|prefix| prefix.eq_ignore_ascii_case("jdbc:h2:file:")) + h2_file_jdbc_url_prefix(connection_string.trim()).is_some() } fn mongo_uri_database(uri: &str) -> Option { @@ -663,6 +673,23 @@ mod tests { assert_eq!(params["connection_string"], "jdbc:h2:file:/tmp/app;AUTO_SERVER=TRUE"); } + #[test] + fn h2_split_connection_string_is_treated_as_file_mode() { + let mut cfg = config(DatabaseType::H2, None); + cfg.connection_string = Some("jdbc:h2:split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE".to_string()); + + let params = agent_connect_params(&cfg, "127.0.0.1", 9092, "test"); + + assert_eq!( + h2_file_path_from_jdbc_url(cfg.connection_string.as_deref().unwrap()).as_deref(), + Some("C:/dbx-test/h2/sample-db") + ); + assert_eq!(params["host"], ""); + assert_eq!(params["port"], 0); + assert_eq!(params["database"], "split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE"); + assert_eq!(params["connection_string"], "jdbc:h2:split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE"); + } + #[test] fn h2_tcp_connection_keeps_empty_agent_connection_string() { let mut cfg = config(DatabaseType::H2, Some("test")); diff --git a/packages/app-tests/connectionUrl.test.ts b/packages/app-tests/connectionUrl.test.ts index 071fff8d65..78e8669c30 100644 --- a/packages/app-tests/connectionUrl.test.ts +++ b/packages/app-tests/connectionUrl.test.ts @@ -1,6 +1,7 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { normalizeMongoConnectionString, parseConnectionUrl } from "../../apps/desktop/src/lib/connectionUrl.ts"; +import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "../../apps/desktop/src/lib/connectionUrl.ts"; +import { h2FileJdbcUrlWithPath } from "../../apps/desktop/src/lib/h2Connection.ts"; test("parses postgres connection URLs", () => { assert.deepEqual(parseConnectionUrl("postgresql://alice:secret@db.example.com:5433/app?sslmode=require"), { @@ -263,6 +264,59 @@ test("parses SQL Server JDBC URLs with semicolon properties", () => { assert.equal(parsed.urlParams, "encrypt=true"); }); +test("parses H2 split JDBC URLs as file connections", () => { + const source = "jdbc:h2:split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE"; + const parsed = parseConnectionUrl(source); + + assert.equal(parsed.dbType, "h2"); + assert.equal(parsed.driverProfile, "h2"); + assert.equal(parsed.driverLabel, "H2"); + assert.equal(parsed.host, "C:/dbx-test/h2/sample-db"); + assert.equal(parsed.port, 0); + assert.equal(parsed.username, "sa"); + assert.equal(parsed.password, ""); + assert.equal(parsed.database, "sample-db"); + assert.equal(parsed.urlParams, "AUTO_SERVER=TRUE"); + assert.equal(parsed.connectionString, source); +}); + +test("parses H2 TCP JDBC URLs as server connections", () => { + const source = "jdbc:h2:tcp://localhost:9123/~/sample-db;USER=sa;PASSWORD=s%40cret;MODE=MySQL"; + const parsed = parseConnectionUrl(source); + + assert.equal(parsed.dbType, "h2"); + assert.equal(parsed.driverProfile, "h2"); + assert.equal(parsed.driverLabel, "H2"); + assert.equal(parsed.host, "localhost"); + assert.equal(parsed.port, 9123); + assert.equal(parsed.username, "sa"); + assert.equal(parsed.password, "s@cret"); + assert.equal(parsed.database, "~/sample-db"); + assert.equal(parsed.urlParams, "MODE=MySQL"); + assert.equal(parsed.ssl, false); + assert.equal(parsed.connectionString, source); +}); + +test("keeps typed H2 credentials when JDBC URL does not include them", () => { + const parsed = parseConnectionUrl("jdbc:h2:split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE"); + const applied = applyParsedConnectionUrl({ name: "", db_type: "h2", username: "typed-user", password: "typed-secret" } as any, parsed); + + assert.equal(applied.username, "typed-user"); + assert.equal(applied.password, "typed-secret"); +}); + +test("uses H2 JDBC URL credentials when they are included", () => { + const parsed = parseConnectionUrl("jdbc:h2:split:28:C:/dbx-test/h2/sample-db;USER=url-user;PASSWORD=url-secret;AUTO_SERVER=TRUE"); + const applied = applyParsedConnectionUrl({ name: "", db_type: "h2", username: "typed-user", password: "typed-secret" } as any, parsed); + + assert.equal(applied.username, "url-user"); + assert.equal(applied.password, "url-secret"); +}); + +test("rebuilds H2 split JDBC URLs with an edited file path", () => { + assert.equal(h2FileJdbcUrlWithPath("jdbc:h2:split:28:C:/dbx-test/h2/sample-db;AUTO_SERVER=TRUE", "D:/dbx/new-sample.mv.db"), "jdbc:h2:split:28:D:/dbx/new-sample;AUTO_SERVER=TRUE"); +}); + test("parses Oracle JDBC service URLs", () => { const parsed = parseConnectionUrl("jdbc:oracle:thin:@//oracle.example.com:1522/ORCLPDB1");