Rust in Everything
基于 sqlx 0.9.0

mismatched types; Rust type i32 (as SQL type INT4) is not compatible with SQL type INT8

主线位置:查询主线的最后一步。PgRow 里每一列都带着 RowDescription 给的类型 OID,try_get 解码前先问 T::compatible(列的类型),答「否」就报这句。Type::compatible 的规则见类型的「Type:声明和兼容」,try_get 的三步见 Row 与 FromRow。

flowchart TB
  get["try_get T"] --> isnull{"值是 NULL"}
  isnull -->|是| dec["直接 decode"]
  isnull -->|否| comp{"T compatible"}
  comp -->|否| err["mismatched types"]
  comp -->|是| dec
  dec --> out["Ok 或 decode 错误"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"(chrono feature,没开 bigdecimal / rust_decimal)。表:

create table item (
  id int8 primary key,
  name text not null,
  price numeric(10, 2),
  created_at timestamp not null default '2026-10-24 08:00:00',
  note text
);
-- 三行:price 依次是 9.90、NULL、3.50

现象

src/bin/mismatched.rs 的前半段:

  let r = sqlx::query_scalar::<_, i32>("select count(*) from item").fetch_one(&pool).await;
  println!("count(*) i32  -> {}", r.unwrap_err());
  let r = sqlx::query_scalar::<_, f64>("select price from item where id = 1").fetch_one(&pool).await;
  println!("numeric f64   -> {}", r.unwrap_err());
  let r = sqlx::query_scalar::<_, DateTime<Utc>>("select created_at from item where id = 1").fetch_one(&pool).await;
  println!("timestamp Utc -> {}", r.unwrap_err());
count(*) i32  -> error occurred while decoding column 0: mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `INT8`
numeric f64   -> error occurred while decoding column 0: mismatched types; Rust type `f64` (as SQL type `FLOAT8`) is not compatible with SQL type `NUMERIC`
timestamp Utc -> error occurred while decoding column 0: mismatched types; Rust type `chrono::datetime::DateTime<chrono::offset::utc::Utc>` (as SQL type `TIMESTAMPTZ`) is not compatible with SQL type `TIMESTAMP`

一句话里有三个类型:Rust 类型(std::any::type_name,所以是带完整路径的 chrono::datetime::DateTime<...>)、这个 Rust 类型声明的 SQL 类型(T::type_info())、列实际的 SQL 类型。前两个总是成对出现,对照第三个就知道该换成什么。

更麻烦的是 NULL。同一个程序接着读 price,一次只读 NULL 那一行,一次按 nulls first 读全部:

  let r = sqlx::query_scalar::<_, Option<f64>>("select price from item where id = 2").fetch_one(&pool).await;
  println!("NULL row only -> {:?}", r);
  let mut rows = sqlx::query_scalar::<_, Option<f64>>("select price from item order by price nulls first").fetch(&pool);
  while let Some(r) = rows.next().await {
    println!("stream        -> {}", r.map_or_else(|e| e.to_string(), |v| format!("{v:?}")));
  }
NULL row only -> Ok(None)
stream        -> None
stream        -> error occurred while decoding column 0: mismatched types; Rust type `core::option::Option<f64>` (as SQL type `FLOAT8`) is not compatible with SQL type `NUMERIC`
stream        -> error occurred while decoding column 0: mismatched types; Rust type `core::option::Option<f64>` (as SQL type `FLOAT8`) is not compatible with SQL type `NUMERIC`

类型写错了,NULL 那一行却成功读成了 None。测试数据里这一列恰好全是 NULL 时,测试是绿的;上线遇到第一个有值的行才报错。错误也不说是第几行,只说第几列。

出处

检查在 Row::try_get:

        let value = self.try_get_raw(&index)?;

        if !value.is_null() {
            let ty = value.type_info();

            if !ty.is_null() && !T::compatible(&ty) {
                return Err(Error::ColumnDecode {
                    index: format!("{index:?}"),
                    source: mismatched_types::<Self::Database, T>(&ty),
                });
            }
        }

if !value.is_null() 把 NULL 整个跳过了:NULL 不带值,Option<T> 的 decode 对它直接给 None,不需要知道类型对不对,sqlx 就没去查。文字由 mismatched_types 拼出。query_as、query_scalar、#[derive(FromRow)] 都经过这里。

三组常见的不兼容都是有意的:i32 的 compatible 只认 INT4,因为 INT8 的下一行就可能放不下;f64 只认 FLOAT8,NUMERIC 的二进制格式是一串以 10000 为基的数位,不是 IEEE 754;DateTime<Utc> 认 TIMESTAMPTZ,TIMESTAMP 不带时区,sqlx 不替你假设它是 UTC。两种时间戳在线上的格式见 Postgres 类型。

query! / query_as! 在编译期就做这项检查,报的是 rustc 的错误。src/bin/mismatched_macro.rs:

#[allow(dead_code)]
struct Stats {
  n: i32,
  price: Option<f64>,
}

// ...
  let s = sqlx::query_as!(Stats, r#"select count(*) as "n!", max(price) as price from item"#)
    .fetch_one(&pool)
    .await?;
error: SQLx feature `bigdecimal` required for type NUMERIC of column #2 ("price")
  --> src/bin/mismatched_macro.rs:10:11
   |
10 |   let s = sqlx::query_as!(Stats, r#"select count(*) as "n!", max(price) as price from item"#)
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in the macro `$crate::sqlx_macros::expand_query` which comes from the expansion of the macro `sqlx::query_as` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: the trait bound `i32: From<i64>` is not satisfied
  --> src/bin/mismatched_macro.rs:10:11
   |
10 |   let s = sqlx::query_as!(Stats, r#"select count(*) as "n!", max(price) as price from item"#)
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<i64>` is not implemented for `i32`
   |
...
   = note: required for `i64` to implement `Into<i32>`
...
  • count(*):宏按列类型 INT8 生成 i64,再 .into() 到结构体字段的类型,i64 转不成 i32,于是是一个 From 的报错,而不是 mismatched types。
  • NUMERIC:没有开 bigdecimal 或 rust_decimal feature 时,宏不知道该生成什么类型,直接拒绝。这句来自 get_column_type。

修法

按列的 SQL 类型选 Rust 类型,或者在 SQL 里转换成想要的类型。mismatched.rs 的后半段:

  let n: i64 = sqlx::query_scalar("select count(*) from item").fetch_one(&pool).await?;
  let n32: i32 = sqlx::query_scalar("select count(*)::int4 from item").fetch_one(&pool).await?;
  let p: Vec<Option<f64>> = sqlx::query_scalar("select price::float8 from item order by id").fetch_all(&pool).await?;
  let s: Vec<Option<String>> = sqlx::query_scalar("select price::text from item order by id").fetch_all(&pool).await?;
  let t: NaiveDateTime = sqlx::query_scalar("select created_at from item where id = 1").fetch_one(&pool).await?;
  let tz: DateTime<Utc> = sqlx::query_scalar("select created_at at time zone 'UTC' from item where id = 1").fetch_one(&pool).await?;
  println!("i64 {n} | int4 {n32} | float8 {p:?} | text {s:?}");
  println!("NaiveDateTime {t} | at time zone 'UTC' {tz}");
i64 3 | int4 3 | float8 [Some(9.9), None, Some(3.5)] | text [Some("9.90"), None, Some("3.50")]
NaiveDateTime 2026-10-24 08:00:00 | at time zone 'UTC' 2026-10-24 08:00:00 UTC
列的类型读成或者在 SQL 里
INT8(count(*)、sum(int4))i64::int4 后读 i32
NUMERICbigdecimal / rust_decimal feature 的类型::float8 读 f64(会丢精度),::text 读 String
TIMESTAMPNaiveDateTimeat time zone 'UTC' 变成 TIMESTAMPTZ,读 DateTime<Utc>
TIMESTAMPTZDateTime<Utc>

宏的版本同样在 SQL 里转:count(*)::int4 as "n!"、max(price)::float8 as price,src/bin/mismatched_macro_fix.rs 编译通过,输出 n=3 price=Some(9.9)。长期的修法是改表:金额列确实需要十进制就开 rust_decimal 或 bigdecimal feature;时间列大多应该是 timestamptz。

📝NOTE
没有蠢问题

问: query! 支持 as "n: i32" 这种类型覆盖,能不能用它把 INT8 直接读成 i32、把 NUMERIC 读成 f64?

答: 能编译,但不检查。宏的文档说这种覆盖「dynamic type checking is still done so if the types are incompatible this will be an error at runtime」,实际生成的代码对覆盖过的类型用的是 try_get_unchecked,只有 _ 通配才用 try_get。src/bin/override_type.rs:

  let r = sqlx::query!(r#"select count(*) as "n!: i32" from item"#).fetch_one(&pool).await?;
  println!("count(*) as \"n!: i32\" -> {}", r.n);
  let r = sqlx::query!(r#"select 5000000000::int8 as "n!: i32""#).fetch_one(&pool).await;
  println!("5e9 as \"n!: i32\"      -> {}", r.unwrap_err());
  let r = sqlx::query!(r#"select price as "p: f64" from item where id = 1"#).fetch_one(&pool).await;
  println!("numeric as \"p: f64\"   -> {:?}", r.map(|r| r.p));
count(*) as "n!: i32" -> 3
5e9 as "n!: i32"      -> error occurred while decoding column 0: number too large to fit in target type
numeric as "p: f64"   -> Ok(Some(2.78134232313401e-309))

整数碰巧能用:Postgres 的整数解码接受 1 到 8 字节,再 try_into(),放不下才报错。NUMERIC 读成 f64 没有任何报错,得到的是把 9.90 的二进制表示前 8 个字节当成浮点数读出来的值。0.8.6 的生成代码同一处也是 try_get_unchecked(这里只核对了源码,实验只在 0.9.0 上跑)。没有找到对应的上游 issue。类型覆盖只用在自定义类型这类「底层表示确实一样」的场合;要换类型,在 SQL 里转。

怎么确认没中招

  • 搜 query_as::<_, ( 和 query_scalar::<_, ,对照每一列的 SQL 类型:聚合(count、sum)、NUMERIC、TIMESTAMP 是三个高发处。psql 里 \d 表名 看列类型,表达式列用 select pg_typeof(...) 看。
  • 测试数据里每个可空列至少有一行非 NULL 值,否则类型错误测不出来。
  • query! 里搜 as "...: ,看覆盖的类型和列的 SQL 类型是不是同一种底层表示。

小结

  • 这句报错列出三个类型:Rust 类型、它声明的 SQL 类型、列实际的 SQL 类型。换成和第三个对应的 Rust 类型,或者在 SQL 里 :: 转换。
  • count(*) 是 INT8,读 i64;NUMERIC 不能读 f64,用 decimal 类型或 ::float8;TIMESTAMP 读 NaiveDateTime,TIMESTAMPTZ 读 DateTime<Utc>。
  • NULL 跳过兼容检查,类型错误要到第一个非 NULL 行才暴露,而且不报行号。
  • query_as! 在编译期报这类错(From<i64> 不满足、需要 bigdecimal feature);"col: T" 类型覆盖在 0.9.0 里不做运行期检查,和文档不符。

💬 关联讨论

发起讨论
加载中...