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_decimalfeature 时,宏不知道该生成什么类型,直接拒绝。这句来自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 |
NUMERIC | bigdecimal / rust_decimal feature 的类型 | ::float8 读 f64(会丢精度),::text 读 String |
TIMESTAMP | NaiveDateTime | at time zone 'UTC' 变成 TIMESTAMPTZ,读 DateTime<Utc> |
TIMESTAMPTZ | DateTime<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。
问: 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>不满足、需要bigdecimalfeature);"col: T"类型覆盖在 0.9.0 里不做运行期检查,和文档不符。