fix(sqlite): decode text-affinity blobs

This commit is contained in:
t8y2 2026-07-06 13:56:05 +08:00
parent dd8d1d3556
commit 564c5beeff
2 changed files with 55 additions and 4 deletions

View File

@ -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"] }

View File

@ -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<usize
if starts_with_executable_sql_keyword(sql, &["SELECT", "PRAGMA", "EXPLAIN", "WITH"]) {
let mut stmt = conn.prepare(sql).map_err(|e| e.to_string())?;
let columns = stmt.column_names().iter().map(|name| name.to_string()).collect::<Vec<_>>();
let column_decl_types =
stmt.columns().iter().map(|column| column.decl_type().map(str::to_string)).collect::<Vec<_>>();
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<usize
})
}
fn value_ref_to_json(value: ValueRef<'_>) -> 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")
}