fix(connection): parse OceanBase JDBC URLs

This commit is contained in:
jearhe
2026-08-22 16:37:20 +08:00
committed by GitHub
parent 6304335fcc
commit 9774a194ec
5 changed files with 107 additions and 17 deletions
@@ -2481,6 +2481,13 @@ function isCustomCompatibleProfile() {
return selectedType.value === "custom_mysql" || selectedType.value === "custom_postgres";
}
function connectionUrlPreferredProfile() {
if (selectedType.value === "oceanbase" && form.value.driver_profile === "oceanbase-oracle") {
return "oceanbase-oracle";
}
return selectedType.value;
}
function applyProfile(val: string, preserveConnectionFields = false) {
const profile = driverProfiles[val];
if (!profile) return;
@@ -3870,7 +3877,7 @@ function applyConnectionUrlToForm(input: string): boolean {
return true;
}
const parsed = parseConnectionUrl(input, selectedType.value);
const parsed = parseConnectionUrl(input, connectionUrlPreferredProfile());
form.value = applyParsedConnectionUrl(form.value, parsed);
if (form.value.db_type === "victoriametrics") {
hydrateVictoriaMetricsFields(form.value.external_config);
@@ -3881,7 +3888,12 @@ function applyConnectionUrlToForm(input: string): boolean {
resetMeilisearchHostInput();
}
oracleTnsAdminPath.value = parseOracleTnsConnectionString(parsed.connectionString)?.tnsAdmin || "";
selectedType.value = parsed.driverProfile;
if (parsed.driverProfile === "oceanbase-oracle") {
oceanbaseSubMode.value = "oracle";
selectedType.value = "oceanbase";
} else {
selectedType.value = parsed.driverProfile;
}
customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : "";
mongoUseUrl.value = !!parsed.useMongoUrl;
if (form.value.db_type === "h2") {
@@ -3936,7 +3948,7 @@ function formValueForSubmit(): Omit<ConnectionConfig, "id"> {
return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined });
}
return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value));
return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, connectionUrlPreferredProfile()));
}
if (form.value.db_type === "meilisearch" && hasPendingMeilisearchHostInput()) {
@@ -1,6 +1,6 @@
// @vitest-environment happy-dom
import { createApp, nextTick, type App, type ComputedRef, type InjectionKey } from "vue";
import { createApp, defineComponent, h, nextTick, type App, type ComputedRef, type InjectionKey } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const backend = vi.hoisted(() => ({
@@ -93,8 +93,7 @@ vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => settings,
}));
vi.mock("@/components/grid/DataGrid.vue", async () => {
const { defineComponent, h } = await import("vue");
vi.mock("@/components/grid/DataGrid.vue", () => {
return {
default: defineComponent({
name: "DataGridStub",
@@ -1,7 +1,8 @@
// @vitest-environment happy-dom
import { createApp, nextTick, type App } from "vue";
import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { isDataGridPrefixAppend } from "@/lib/dataGrid/dataGridInfiniteScroll";
const backend = vi.hoisted(() => ({
documentFindDocuments: vi.fn(),
@@ -77,9 +78,7 @@ vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => settings,
}));
vi.mock("@/components/grid/DataGrid.vue", async () => {
const { defineComponent, h } = await import("vue");
const { isDataGridPrefixAppend } = await import("@/lib/dataGrid/dataGridInfiniteScroll");
vi.mock("@/components/grid/DataGrid.vue", () => {
return {
default: defineComponent({
name: "DataGridStub",
@@ -31,6 +31,7 @@ export type ConnectionProfile = {
const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
mysql: { type: "mysql", profile: "mysql", label: "MySQL", defaultPort: 3306 },
oceanbase: { type: "mysql", profile: "oceanbase", label: "OceanBase", defaultPort: 2883 },
mariadb: { type: "mysql", profile: "mariadb", label: "MariaDB", defaultPort: 3306 },
postgres: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 },
postgresql: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 },
@@ -82,6 +83,13 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
victoriametrics: { type: "victoriametrics", profile: "victoriametrics", label: "VictoriaMetrics", defaultPort: 8428 },
};
const OCEANBASE_ORACLE_PROFILE: ConnectionProfile = {
type: "oceanbase-oracle",
profile: "oceanbase-oracle",
label: "OceanBase Oracle Mode",
defaultPort: 2883,
};
const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
clickhouse: SCHEME_PROFILES.clickhouse,
dynamodb: SCHEME_PROFILES.dynamodb,
@@ -376,6 +384,9 @@ export function connectionProfileForScheme(scheme: string, preferredProfile?: st
if ((normalizedScheme === "http" || normalizedScheme === "https") && normalizedPreferredProfile) {
return HTTP_SELECTED_PROFILES[normalizedPreferredProfile];
}
if (normalizedScheme === "oceanbase" && normalizedPreferredProfile === "oceanbase-oracle") {
return OCEANBASE_ORACLE_PROFILE;
}
// PostgreSQL-compatible products use standard PostgreSQL URLs, so keep the
// selected product profile when parsing a pasted URL.
if ((normalizedScheme === "postgres" || normalizedScheme === "postgresql") && (normalizedPreferredProfile === "cloudberry" || normalizedPreferredProfile === "opentenbase")) {
@@ -657,6 +668,9 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
if (!input) {
throw new Error("Connection URL is empty");
}
if (/^jdbc:oceanbase:(?:oracle:)?loadbalance:\/\//i.test(input)) {
throw new Error("Unsupported OceanBase JDBC URL variant: loadbalance");
}
const jdbcHive = parseJdbcHiveUrl(input);
if (jdbcHive) return jdbcHive;
const jdbcH2 = parseH2JdbcUrl(input);
@@ -676,7 +690,8 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
const jdbcSqlServer = parseJdbcSqlServerUrl(input);
if (jdbcSqlServer) return jdbcSqlServer;
const isJdbcUrl = /^jdbc:/i.test(input);
const source = isJdbcUrl ? input.replace(/^jdbc:/i, "") : input;
const isOceanBaseOracleJdbc = /^jdbc:oceanbase:oracle:\/\//i.test(input);
const source = isOceanBaseOracleJdbc ? input.replace(/^jdbc:oceanbase:oracle:/i, "oceanbase:") : isJdbcUrl ? input.replace(/^jdbc:/i, "") : input;
const mongoResult = parseMongoUrl(source);
if (mongoResult) return mongoResult;
@@ -692,7 +707,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
}
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
const profile = connectionProfileForScheme(scheme, preferredProfile);
const profile = connectionProfileForScheme(scheme, isOceanBaseOracleJdbc ? "oceanbase-oracle" : preferredProfile);
if (!profile) {
throw new Error(`Unsupported connection URL scheme: ${scheme}`);
}
@@ -702,8 +717,8 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
const urlParamsWithoutName = stripConnectionNameParam(urlParams);
const normalizedFragment = decodeUrlPart(parsed.hash.replace(/^#/, "")).trim().toLowerCase();
const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParamsWithoutName, "insecure=true"].filter(Boolean).join("&") : urlParamsWithoutName;
const mysqlCredentials = isJdbcUrl && profile.type === "mysql" ? extractMysqlCredentialParams(parsedUrlParams) : undefined;
const effectiveUrlParams = mysqlCredentials?.urlParams ?? parsedUrlParams;
const jdbcCredentials = isJdbcUrl && (profile.type === "mysql" || profile.profile === "oceanbase-oracle") ? extractMysqlCredentialParams(parsedUrlParams) : undefined;
const effectiveUrlParams = jdbcCredentials?.urlParams ?? parsedUrlParams;
if (profile.type === "mongodb") {
return {
dbType: profile.type,
@@ -738,7 +753,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
}
const isMeilisearch = profile.type === "meilisearch";
const defaultPort = isMeilisearch && scheme === "http" ? 80 : isMeilisearch && scheme === "https" ? 443 : profile.defaultPort;
const defaultPort = isJdbcUrl && scheme === "oceanbase" ? 3306 : isMeilisearch && scheme === "http" ? 80 : isMeilisearch && scheme === "https" ? 443 : profile.defaultPort;
return {
...(name ? { name } : {}),
@@ -748,8 +763,8 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
host: parsed.hostname,
port: parsed.port ? Number(parsed.port) : defaultPort,
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username),
password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password),
username: jdbcCredentials?.username ?? decodeUrlPart(parsed.username),
password: jdbcCredentials?.password ?? decodeUrlPart(parsed.password),
database: profile.type === "victoriametrics" ? "metrics" : profile.type === "dynamodb" ? dynamodbRegionFromHost(parsed.hostname) : isMeilisearch ? undefined : databaseFromPath(parsed.pathname),
urlParams: effectiveUrlParams,
ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)),
+65
View File
@@ -192,6 +192,71 @@ test("parses MySQL JDBC user and password URL params as credentials", () => {
assert.equal(parsed.urlParams, "useUnicode=true&characterEncoding=UTF8&useSSL=false");
});
test("parses OceanBase native JDBC URLs", () => {
const parsed = parseConnectionUrl("jdbc:oceanbase://127.0.0.1:2881/test");
assert.equal(parsed.dbType, "mysql");
assert.equal(parsed.driverProfile, "oceanbase");
assert.equal(parsed.driverLabel, "OceanBase");
assert.equal(parsed.host, "127.0.0.1");
assert.equal(parsed.port, 2881);
assert.equal(parsed.database, "test");
assert.equal(parsed.urlParams, "");
});
test("uses the OceanBase profile default port for non-JDBC URLs without an explicit port", () => {
assert.equal(parseConnectionUrl("oceanbase://127.0.0.1/test").port, 2883);
});
test("uses the OceanBase JDBC driver default port when the URL omits it", () => {
assert.equal(parseConnectionUrl("jdbc:oceanbase://127.0.0.1/test").port, 3306);
});
test("parses OceanBase JDBC user and password URL params as credentials", () => {
const parsed = parseConnectionUrl("jdbc:oceanbase://127.0.0.1:2881/test?user=root%40tenant&password=p%40ss&useUnicode=true");
assert.equal(parsed.username, "root@tenant");
assert.equal(parsed.password, "p@ss");
assert.equal(parsed.urlParams, "useUnicode=true");
});
test("preserves the selected OceanBase MySQL profile for native JDBC URLs", () => {
const parsed = parseConnectionUrl("jdbc:oceanbase://127.0.0.1:2881/test", "oceanbase");
assert.equal(parsed.dbType, "mysql");
assert.equal(parsed.driverProfile, "oceanbase");
assert.equal(parsed.driverLabel, "OceanBase");
});
test("preserves the selected OceanBase Oracle profile for native JDBC URLs", () => {
const parsed = parseConnectionUrl("jdbc:oceanbase://127.0.0.1:2881/sys?user=SYS&password=secret&useSSL=false", "oceanbase-oracle");
assert.equal(parsed.dbType, "oceanbase-oracle");
assert.equal(parsed.driverProfile, "oceanbase-oracle");
assert.equal(parsed.driverLabel, "OceanBase Oracle Mode");
assert.equal(parsed.username, "SYS");
assert.equal(parsed.password, "secret");
assert.equal(parsed.urlParams, "useSSL=false");
});
test("parses explicit OceanBase Oracle-mode JDBC URLs", () => {
const parsed = parseConnectionUrl("jdbc:oceanbase:oracle://ob.example.com/sys?user=SYS&password=secret&useSSL=false");
assert.equal(parsed.dbType, "oceanbase-oracle");
assert.equal(parsed.driverProfile, "oceanbase-oracle");
assert.equal(parsed.host, "ob.example.com");
assert.equal(parsed.port, 3306);
assert.equal(parsed.database, "sys");
assert.equal(parsed.username, "SYS");
assert.equal(parsed.password, "secret");
assert.equal(parsed.urlParams, "useSSL=false");
});
test("does not parse OceanBase load-balance JDBC URLs as single-host connections", () => {
assert.throws(() => parseConnectionUrl("jdbc:oceanbase:loadbalance://host1:2881,host2:2881/test"), /loadbalance/);
assert.throws(() => parseConnectionUrl("jdbc:oceanbase:oracle:loadbalance://host1:2881,host2:2881/sys"), /loadbalance/);
});
test("parses MySQL JDBC URL params with ProxySQL multi-at usernames", () => {
const parsed = parseConnectionUrl("jdbc:mysql://127.0.0.1:6033/example?user=xxxxx%40db_readonly%40127.0.0.1&password=p%40wd&useSSL=false");