diff --git a/crates/dbx-core/src/jdbc.rs b/crates/dbx-core/src/jdbc.rs index 020236b585..3b665963bd 100644 --- a/crates/dbx-core/src/jdbc.rs +++ b/crates/dbx-core/src/jdbc.rs @@ -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 { + 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 { + 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, 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) -> 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())); diff --git a/docs/content/docs/plugins.cn.mdx b/docs/content/docs/plugins.cn.mdx index b6a1cfc579..ec030740fc 100644 --- a/docs/content/docs/plugins.cn.mdx +++ b/docs/content/docs/plugins.cn.mdx @@ -116,6 +116,8 @@ DBX 不会自动下载数据库厂商驱动。你需要从数据库官网或 Mav 不同数据库和驱动版本的 URL 参数可能不同。以厂商官方文档为准。如果驱动依赖多个 JAR,请把需要的 JAR 路径都导入或填写进去。 +Oracle 驱动请同时导入与 `ojdbc` 同版本的 `com.oracle.database.nls:orai18n`。基础 ojdbc jar 不支持 `ZHS16GBK` 等多字节字符集,缺少它时表/字段注释等元数据无法加载。通过 Maven 安装 `com.oracle.database.jdbc:ojdbc*` 时会自动附带同版本的 `orai18n`。使用 DBX 内置 Oracle 连接(默认驱动)则无需额外 JAR,已支持这些字符集。 + ## 第四步:导入 JDBC 驱动 JAR 在 DBX 中打开: diff --git a/docs/content/docs/plugins.mdx b/docs/content/docs/plugins.mdx index 0e8de964a8..56d54a9f72 100644 --- a/docs/content/docs/plugins.mdx +++ b/docs/content/docs/plugins.mdx @@ -116,6 +116,8 @@ Common examples: 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. +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. + ## Step 4: Import JDBC Driver JARs In DBX: diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 59571d32f2..02be8f6806 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -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 causes = new ArrayList<>(); Throwable cause = error; diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index fcc54b23fb..b16bb31e2d 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -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)); + } }