I have found these related issues/pull requests
This is a follow-up to #3221 (which is now closed) rather than a different root cause.
Issue #3221 showed the bool case (NULL -> false). This report broadens the scope: the same SQLite decode behavior also affects String, Vec<u8>, i64, and f64, and the text/blob path appears to conflate SQL NULL with legitimate zero-length TEXT/BLOB values.
Description
When decoding SQLite rows into non-Option Rust types, SQL NULL is silently converted into default-looking values instead of returning a decode error.
Confirmed examples:
String -> ""
Vec<u8> -> []
i64 -> 0
f64 -> 0.0
bool -> false
Option<T> still behaves as expected (NULL -> None), so the difference is silent and easy to miss.
This also affects higher-level APIs like query_as() / FromRow, since they go through the same row decode path.
Reproduction steps
use sqlx::{Connection, Row};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut conn = sqlx::SqliteConnection::connect("sqlite::memory:").await?;
let row = sqlx::query(
"SELECT CAST(NULL AS TEXT) AS t,
CAST(NULL AS BLOB) AS b,
CAST(NULL AS INTEGER) AS i,
CAST(NULL AS REAL) AS f,
CAST(NULL AS INTEGER) AS bo"
)
.fetch_one(&mut conn)
.await?;
println!("String={:?}", row.try_get::<String, _>("t"));
println!("Vec<u8>={:?}", row.try_get::<Vec<u8>, _>("b"));
println!("i64={:?}", row.try_get::<i64, _>("i"));
println!("f64={:?}", row.try_get::<f64, _>("f"));
println!("bool={:?}", row.try_get::<bool, _>("bo"));
println!("Option<String>={:?}", row.try_get::<Option<String>, _>("t"));
Ok(())
}
This will output
String=Ok("")
Vec<u8>=Ok([])
i64=Ok(0)
f64=Ok(0.0)
bool=Ok(false)
Option<String>=Ok(None)
SQLx version
main branch
Enabled SQLx features
default features
Database server and version
SQLite
Operating system
MacOS
Rust version
1.94.0 (4a4ef493e 2026-03-02)