fix(jdbc): pair oracle ojdbc maven installs with orai18n

Closes #7261
This commit is contained in:
t8y2
2026-08-27 12:19:16 +08:00
parent d70a1231b0
commit e6d25b016e
5 changed files with 166 additions and 12 deletions
+96 -10
View File
@@ -212,10 +212,57 @@ pub async fn install_jdbc_driver_from_maven(
let local_repo = plugin_dir.join("maven-cache");
std::fs::create_dir_all(&local_repo).map_err(|err| err.to_string())?;
let mut command = crate::process::new_tokio_command(&resolver);
let mut resolved = resolve_maven_artifacts(&resolver, &coordinate, &local_repo, &repositories, &env).await?;
if let Some(orai18n) = oracle_orai18n_companion(&resolved) {
match resolve_maven_artifacts(&resolver, &orai18n, &local_repo, &repositories, &env).await {
Ok(mut companion) => resolved.artifacts.append(&mut companion.artifacts),
Err(_) => {
// A missing orai18n companion must not block the driver install;
// affected charsets surface a dedicated hint at runtime.
}
}
}
let root = plugins_root.to_path_buf();
tokio::task::spawn_blocking(move || install_jdbc_maven_bundle(&root, &coordinate, &resolved))
.await
.map_err(|err| err.to_string())??;
list_jdbc_drivers(plugins_root)
}
const ORACLE_JDBC_GROUP_ID: &str = "com.oracle.database.jdbc";
const ORACLE_NLS_GROUP_ID: &str = "com.oracle.database.nls";
const ORAI18N_ARTIFACT_ID: &str = "orai18n";
/// Oracle's thin driver jar ships converters for a small default charset set;
/// multibyte charsets such as ZHS16GBK need the orai18n companion jar, otherwise
/// every metadata call that reads dictionary comments fails wholesale. DBeaver
/// ships the same pairing.
fn oracle_orai18n_companion(resolved: &MavenResolveOutput) -> Option<String> {
let ojdbc = resolved
.artifacts
.iter()
.find(|artifact| artifact.group_id == ORACLE_JDBC_GROUP_ID && artifact.artifact_id.starts_with("ojdbc"))?;
if resolved
.artifacts
.iter()
.any(|artifact| artifact.group_id == ORACLE_NLS_GROUP_ID && artifact.artifact_id == ORAI18N_ARTIFACT_ID)
{
return None;
}
Some(format!("{ORACLE_NLS_GROUP_ID}:{ORAI18N_ARTIFACT_ID}:{}", ojdbc.version))
}
async fn resolve_maven_artifacts(
resolver: &Path,
coordinate: &str,
local_repo: &Path,
repositories: &[String],
env: &PluginRuntimeEnv,
) -> Result<MavenResolveOutput, String> {
let mut command = crate::process::new_tokio_command(resolver);
env.apply_to(&mut command);
command.arg("resolve").arg("--coordinate").arg(&coordinate).arg("--local-repo").arg(&local_repo);
for repo in &repositories {
command.arg("resolve").arg("--coordinate").arg(coordinate).arg("--local-repo").arg(local_repo);
for repo in repositories {
command.arg("--repo").arg(repo);
}
let output = command.output().await.map_err(|err| format!("Failed to run JDBC Maven resolver: {err}"))?;
@@ -224,13 +271,7 @@ pub async fn install_jdbc_driver_from_maven(
return Err(if stderr.is_empty() { "JDBC Maven resolver failed".to_string() } else { stderr });
}
let stdout = String::from_utf8_lossy(&output.stdout);
let resolved: MavenResolveOutput =
serde_json::from_str(&stdout).map_err(|err| format!("Failed to parse JDBC Maven resolver output: {err}"))?;
let root = plugins_root.to_path_buf();
tokio::task::spawn_blocking(move || install_jdbc_maven_bundle(&root, &coordinate, &resolved))
.await
.map_err(|err| err.to_string())??;
list_jdbc_drivers(plugins_root)
serde_json::from_str(&stdout).map_err(|err| format!("Failed to parse JDBC Maven resolver output: {err}"))
}
pub async fn install_prestosql_jdbc_driver(plugins_root: &Path) -> Result<Vec<JdbcDriverInfo>, String> {
@@ -1254,6 +1295,51 @@ mod tests {
assert!(!is_safe_bundle_id("../hive"));
}
fn resolved_artifact(group_id: &str, artifact_id: &str, version: &str) -> MavenResolvedArtifact {
MavenResolvedArtifact {
group_id: group_id.to_string(),
artifact_id: artifact_id.to_string(),
version: version.to_string(),
classifier: String::new(),
extension: "jar".to_string(),
file: format!("/tmp/{artifact_id}-{version}.jar"),
}
}
fn resolved_with(artifacts: Vec<MavenResolvedArtifact>) -> MavenResolveOutput {
MavenResolveOutput {
coordinate: String::new(),
scope: "runtime".to_string(),
repositories: vec!["https://repo.maven.apache.org/maven2/".to_string()],
artifacts,
}
}
#[test]
fn oracle_ojdbc_resolves_matching_orai18n_companion() {
let resolved = resolved_with(vec![resolved_artifact("com.oracle.database.jdbc", "ojdbc11", "21.9.0.0")]);
assert_eq!(oracle_orai18n_companion(&resolved).as_deref(), Some("com.oracle.database.nls:orai18n:21.9.0.0"));
}
#[test]
fn oracle_orai18n_companion_skips_existing_nls_artifact() {
let resolved = resolved_with(vec![
resolved_artifact("com.oracle.database.jdbc", "ojdbc11", "21.9.0.0"),
resolved_artifact("com.oracle.database.nls", "orai18n", "21.9.0.0"),
]);
assert_eq!(oracle_orai18n_companion(&resolved), None);
}
#[test]
fn oracle_orai18n_companion_ignores_non_oracle_drivers() {
for coordinate in
[("org.postgresql", "postgresql"), ("com.oracle.database.xml", "xdb6"), ("mysql", "mysql-connector-java")]
{
let resolved = resolved_with(vec![resolved_artifact(coordinate.0, coordinate.1, "8.0.33")]);
assert_eq!(oracle_orai18n_companion(&resolved), None, "{}", coordinate.1);
}
}
#[test]
fn imports_offline_jdbc_plugin_and_rewrites_bundle_paths() {
let root = std::env::temp_dir().join(format!("dbx-offline-jdbc-test-{}", uuid::Uuid::new_v4()));
+2
View File
@@ -116,6 +116,8 @@ DBX 不会自动下载数据库厂商驱动。你需要从数据库官网或 Mav
<Callout type="warn">不同数据库和驱动版本的 URL 参数可能不同。以厂商官方文档为准。如果驱动依赖多个 JAR,请把需要的 JAR 路径都导入或填写进去。</Callout>
<Callout type="info">Oracle 驱动请同时导入与 `ojdbc` 同版本的 `com.oracle.database.nls:orai18n`。基础 ojdbc jar 不支持 `ZHS16GBK` 等多字节字符集,缺少它时表/字段注释等元数据无法加载。通过 Maven 安装 `com.oracle.database.jdbc:ojdbc*` 时会自动附带同版本的 `orai18n`。使用 DBX 内置 Oracle 连接(默认驱动)则无需额外 JAR,已支持这些字符集。</Callout>
## 第四步:导入 JDBC 驱动 JAR
在 DBX 中打开:
+2
View File
@@ -116,6 +116,8 @@ Common examples:
<Callout type="warn">JDBC URL parameters vary by database and driver version. Follow the vendor documentation. If a driver depends on multiple JARs, import or list all required JAR paths.</Callout>
<Callout type="info">For Oracle, also import `com.oracle.database.nls:orai18n` with the same version as `ojdbc`. The base ojdbc jar cannot convert multibyte charsets such as `ZHS16GBK`, so table and column comments fail to load without it. Maven installs of `com.oracle.database.jdbc:ojdbc*` fetch the matching `orai18n` jar automatically. DBX's built-in Oracle connection (default driver) supports these charsets without any extra JAR.</Callout>
## Step 4: Import JDBC Driver JARs
In DBX:
@@ -273,10 +273,11 @@ public final class DbxJdbcPlugin {
ObjectNode response = MAPPER.createObjectNode();
response.set("id", id.isMissingNode() ? MAPPER.getNodeFactory().numberNode(1) : id);
JsonNode connection = MAPPER.createObjectNode();
try {
String method = requireText(request, "method");
JsonNode params = request.path("params");
JsonNode connection = params.path("connection");
connection = params.path("connection");
if ("close".equals(method)) {
closeSharedConnection();
ObjectNode result = MAPPER.createObjectNode();
@@ -290,12 +291,31 @@ public final class DbxJdbcPlugin {
} catch (Throwable error) {
// The plugin protocol boundary must report linkage errors from vendor drivers instead of exiting silently.
ObjectNode errorNode = MAPPER.createObjectNode();
errorNode.put("message", throwableMessage(error));
errorNode.put("message", enrichDriverHint(connection, throwableMessage(error)));
response.set("error", errorNode);
}
return response;
}
private static final Pattern ORACLE_UNSUPPORTED_CHARSET_PATTERN =
Pattern.compile("(?i)unsupported charset|不支持的字符集");
// The base Oracle thin driver jar ships converters for a handful of charsets only;
// databases such as ZHS16GBK need orai18n.jar, otherwise every metadata call that
// reads dictionary comments fails wholesale.
static String enrichDriverHint(JsonNode connection, String message) {
if (message == null || !ORACLE_UNSUPPORTED_CHARSET_PATTERN.matcher(message).find()) {
return message;
}
if (!isOracleUrl(jdbcUrl(connection))) {
return message;
}
String hint = message.contains("不支持的字符集")
? "。请在 设置 → JDBC 驱动 中为该 Oracle 驱动一并导入同版本的 orai18n.jar,或改用 DBX 内置 Oracle 连接(默认驱动已支持中文多字节字符集)"
: ". Import orai18n.jar (same version as the ojdbc driver) next to the Oracle driver under Settings -> JDBC Drivers, or use DBX's built-in Oracle connection instead, whose default driver supports multibyte Chinese charsets";
return message.endsWith(hint) ? message : message + hint;
}
private static String throwableMessage(Throwable error) {
List<Throwable> causes = new ArrayList<>();
Throwable cause = error;
@@ -41,6 +41,7 @@ import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class DbxJdbcPluginTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -4448,4 +4449,47 @@ final class DbxJdbcPluginTest {
""".formatted(method, params);
return MAPPER.valueToTree(handleLine.invoke(null, line));
}
@Test
void enrichDriverHintAppendsChineseOrlai18nGuidanceForOracleCharsetErrors() {
JsonNode connection = MAPPER.createObjectNode()
.put("connection_string", "jdbc:oracle:thin:@//db:1521/ORCL");
String enriched = DbxJdbcPlugin.enrichDriverHint(
connection,
"不支持的字符集 (在类路径中添加 orai18n.jar): ZHS16GBK"
);
assertTrue(enriched.startsWith("不支持的字符集 (在类路径中添加 orai18n.jar): ZHS16GBK"));
assertTrue(enriched.contains("orai18n.jar"));
assertTrue(enriched.contains("内置 Oracle 连接"));
}
@Test
void enrichDriverHintAppendsEnglishOrlai18nGuidanceForOracleCharsetErrors() {
JsonNode connection = MAPPER.createObjectNode()
.put("connection_string", "jdbc:oracle:thin:@//db:1521/ORCL");
String enriched = DbxJdbcPlugin.enrichDriverHint(connection, "Unsupported charset: ZHS16GBK");
assertTrue(enriched.startsWith("Unsupported charset: ZHS16GBK"));
assertTrue(enriched.contains("Settings -> JDBC Drivers"));
}
@Test
void enrichDriverHintKeepsMessagesForOtherUrlsAndErrors() {
JsonNode oracleConnection = MAPPER.createObjectNode()
.put("connection_string", "jdbc:oracle:thin:@//db:1521/ORCL");
JsonNode mysqlConnection = MAPPER.createObjectNode()
.put("connection_string", "jdbc:mysql://db:3306/test");
assertEquals("ORA-12505", DbxJdbcPlugin.enrichDriverHint(oracleConnection, "ORA-12505"));
assertEquals(
"Unsupported charset: ZHS16GBK",
DbxJdbcPlugin.enrichDriverHint(mysqlConnection, "Unsupported charset: ZHS16GBK")
);
}
@Test
void enrichDriverHintDoesNotStackRepeatedHints() {
JsonNode connection = MAPPER.createObjectNode()
.put("connection_string", "jdbc:oracle:thin:@//db:1521/ORCL");
String once = DbxJdbcPlugin.enrichDriverHint(connection, "Unsupported charset: ZHS16GBK");
assertEquals(once, DbxJdbcPlugin.enrichDriverHint(connection, once));
}
}