diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 5f41f7929b..8d2f32cffe 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -73,21 +73,27 @@ where row.get_opt::(index).and_then(|result| result.ok()) } +/// 字节转 String:合法 UTF-8(绝大多数场景)时直接复用入参缓冲零拷贝, +/// 仅在非法序列时退化为 lossy 替换。from_utf8_lossy(&b).to_string() 即使 +/// 对合法输入也会多一次分配+拷贝。 +fn bytes_to_string_lossy(bytes: Vec) -> String { + String::from_utf8(bytes).unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) +} + fn get_str(row: &mysql_async::Row, idx: usize) -> String { row_get::(row, idx) - .or_else(|| row_get::, _>(row, idx).map(|b| String::from_utf8_lossy(&b).to_string())) + .or_else(|| row_get::, _>(row, idx).map(bytes_to_string_lossy)) .unwrap_or_default() } fn get_str_by_name(row: &mysql_async::Row, name: &str) -> String { row_get::(row, name) - .or_else(|| row_get::, _>(row, name).map(|b| String::from_utf8_lossy(&b).to_string())) + .or_else(|| row_get::, _>(row, name).map(bytes_to_string_lossy)) .unwrap_or_default() } fn get_opt_str(row: &mysql_async::Row, name: &str) -> Option { - row_get::(row, name) - .or_else(|| row_get::, _>(row, name).map(|b| String::from_utf8_lossy(&b).to_string())) + row_get::(row, name).or_else(|| row_get::, _>(row, name).map(bytes_to_string_lossy)) } /// First non-empty string value among the named columns (e.g. Doris `CatalogName` @@ -279,7 +285,7 @@ fn mysql_bytes_to_json(bytes: Vec, column: &mysql_async::Column) -> serde_js .map(serde_json::Value::String) .unwrap_or_else(|| super::binary_value_to_json(&bytes)); } - serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string()) + serde_json::Value::String(bytes_to_string_lossy(bytes)) } /// Map a MySQL column to a user-facing type name for the result-grid header. @@ -3364,11 +3370,7 @@ pub async fn show_create_table_ddl(pool: &MySqlPool, database: &str, table: &str let row = rows.first().ok_or("DDL not found")?; row.get_opt::(1) .and_then(|result| result.ok()) - .or_else(|| { - row.get_opt::, usize>(1) - .and_then(|result| result.ok()) - .map(|b| String::from_utf8_lossy(&b).to_string()) - }) + .or_else(|| row.get_opt::, usize>(1).and_then(|result| result.ok()).map(bytes_to_string_lossy)) .ok_or_else(|| "Failed to read DDL".to_string()) } @@ -3551,11 +3553,7 @@ pub async fn show_create_table_ddl_from( let row = rows.first().ok_or("DDL not found")?; row.get_opt::(1) .and_then(|result| result.ok()) - .or_else(|| { - row.get_opt::, usize>(1) - .and_then(|result| result.ok()) - .map(|b| String::from_utf8_lossy(&b).to_string()) - }) + .or_else(|| row.get_opt::, usize>(1).and_then(|result| result.ok()).map(bytes_to_string_lossy)) .ok_or_else(|| "Failed to read DDL".to_string()) } @@ -3910,6 +3908,15 @@ mod tests { use crate::db::connection_timeout; use mysql_async::consts::ColumnFlags; + #[test] + fn bytes_to_string_reuses_valid_utf8_and_falls_back_lossy() { + assert_eq!(super::bytes_to_string_lossy("héllo 世界".as_bytes().to_vec()), "héllo 世界"); + assert_eq!(super::bytes_to_string_lossy(vec![]), ""); + // 非法 UTF-8 序列退化为替换字符,与 from_utf8_lossy 语义一致 + let invalid = vec![0x66, 0x6f, 0xff, 0x6f]; + assert_eq!(super::bytes_to_string_lossy(invalid.clone()), String::from_utf8_lossy(&invalid)); + } + #[test] fn mysql_column_type_names_map_to_friendly_names() { use mysql_async::consts::ColumnType::*;