From 564c5beeff6e80c0ab9b4332945e2d3cd0d841c7 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 6 Jul 2026 13:56:05 +0800 Subject: [PATCH] fix(sqlite): decode text-affinity blobs --- crates/dbx-core/Cargo.toml | 2 +- crates/dbx-core/src/db/sqlite.rs | 57 ++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 4a334ddc5..e2f4404d5 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -34,7 +34,7 @@ tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1" deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] } tokio-postgres-rustls = "0.13" webpki-roots = "0.26" -rusqlite = { version = "0.32", features = ["bundled", "load_extension", "backup", "functions"] } +rusqlite = { version = "0.32", features = ["bundled", "load_extension", "backup", "functions", "column_decltype"] } mysql_async = { version = "0.36", default-features = false, features = ["default-rustls", "client_ed25519", "chrono", "rust_decimal"] } sqlparser = { version = "0.62.0", features = ["visitor"] } redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tls-rustls-insecure", "tokio-rustls-comp", "sentinel", "cluster-async"] } diff --git a/crates/dbx-core/src/db/sqlite.rs b/crates/dbx-core/src/db/sqlite.rs index 2eda34503..0801fb771 100644 --- a/crates/dbx-core/src/db/sqlite.rs +++ b/crates/dbx-core/src/db/sqlite.rs @@ -391,6 +391,36 @@ mod tests { assert_eq!(result.rows[0][0], serde_json::json!("Ada")); } + #[tokio::test] + async fn text_affinity_blob_bytes_display_as_utf8_text() { + let pool = connect_path(":memory:").await.expect("connect in-memory SQLite"); + + execute_query( + &pool, + "CREATE TABLE goods (data TEXT); INSERT INTO goods (data) VALUES (X'7b227469746c65223a22e4b8ade69687227d');", + ) + .await + .expect("insert blob-backed JSON into TEXT column"); + let result = execute_query(&pool, "SELECT data FROM goods").await.expect("select data"); + + assert_eq!(result.rows[0][0], serde_json::json!(r#"{"title":"中文"}"#)); + } + + #[tokio::test] + async fn blob_declared_columns_stay_hex_encoded() { + let pool = connect_path(":memory:").await.expect("connect in-memory SQLite"); + + execute_query( + &pool, + "CREATE TABLE goods (data BLOB); INSERT INTO goods (data) VALUES (X'7b227469746c65223a22e4b8ade69687227d');", + ) + .await + .expect("insert blob-backed JSON into BLOB column"); + let result = execute_query(&pool, "SELECT data FROM goods").await.expect("select data"); + + assert_eq!(result.rows[0][0], serde_json::json!("0x7b227469746c65223a22e4b8ade69687227d")); + } + #[tokio::test] async fn create_if_missing_rejects_existing_non_sqlite_file() { let path = std::env::temp_dir().join(format!("dbx-not-sqlite-{}.png", uuid::Uuid::new_v4())); @@ -1925,13 +1955,18 @@ fn execute_query_blocking(pool: &SqliteHandle, sql: &str, max_rows: Option>(); + let column_decl_types = + stmt.columns().iter().map(|column| column.decl_type().map(str::to_string)).collect::>(); let mut rows = stmt.query([]).map_err(|e| e.to_string())?; let mut result_rows = Vec::new(); while let Some(row) = rows.next().map_err(|e| e.to_string())? { let mut values = Vec::with_capacity(columns.len()); for i in 0..columns.len() { - values.push(value_ref_to_json(row.get_ref(i).map_err(|e| e.to_string())?)); + values.push(value_ref_to_json( + row.get_ref(i).map_err(|e| e.to_string())?, + column_decl_types.get(i).and_then(Option::as_deref), + )); } result_rows.push(values); if result_rows.len() > row_limit { @@ -1972,7 +2007,7 @@ fn execute_query_blocking(pool: &SqliteHandle, sql: &str, max_rows: Option) -> serde_json::Value { +fn value_ref_to_json(value: ValueRef<'_>, column_decl_type: Option<&str>) -> serde_json::Value { match value { ValueRef::Null => serde_json::Value::Null, ValueRef::Integer(v) => super::safe_i64_to_json(v), @@ -1980,6 +2015,22 @@ fn value_ref_to_json(value: ValueRef<'_>) -> serde_json::Value { serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null) } ValueRef::Text(v) => serde_json::Value::String(String::from_utf8_lossy(v).to_string()), - ValueRef::Blob(v) => super::binary_value_to_json(v), + ValueRef::Blob(v) => sqlite_blob_value_to_json(v, column_decl_type), } } + +fn sqlite_blob_value_to_json(bytes: &[u8], column_decl_type: Option<&str>) -> serde_json::Value { + if is_sqlite_text_affinity(column_decl_type) { + // SQLite columns can hold BLOB values even when declared as TEXT. + // Match common clients by showing valid UTF-8 bytes as text for text-affinity columns. + if let Ok(text) = std::str::from_utf8(bytes) { + return serde_json::Value::String(text.to_string()); + } + } + super::binary_value_to_json(bytes) +} + +fn is_sqlite_text_affinity(column_decl_type: Option<&str>) -> bool { + let upper = column_decl_type.unwrap_or("").to_ascii_uppercase(); + upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") +}