Row 与 FromRow
主线位置:查询主线的最后一步。
PgConnection::run把每条DataRow包成PgRow,fetch_one拿到第一行,query_as再调FromRow::from_row把它变成你的类型,from_row里每个字段是一次Row::try_get。这一页讲try_get怎么找列、在哪一步检查类型、报什么错,以及#[derive(FromRow)]生成什么。Type、Decode本身在 类型:Type、Encode、Decode;query!宏不用FromRow,见生成的代码。
flowchart TB
getc["try_get(index)"] --> raw["try_get_raw"]
raw --> idx{ColumnIndex}
idx -->|列名不存在| cnf[ColumnNotFound]
idx -->|下标越界| oob[ColumnIndexOutOfBounds]
idx -->|找到| nul{值是 NULL}
nul -->|否| compat{"T::compatible"}
compat -->|否| mis["ColumnDecode 类型不匹配"]
compat -->|是| dec["T::decode"]
nul -->|是| dec
dec -->|Err| cd[ColumnDecode]
dec -->|Ok| val[值]
本页实验都在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上跑,依赖 sqlx = "=0.9.0"。表 person 建在自己的 schema 里:
create table person (
id int8 primary key,
name text not null,
email email unique,
mood mood,
created_at timestamp not null default now(),
price numeric(10, 2),
meta jsonb
);
PgRow:一条 DataRow 加一份共享的元数据
PgRow 只有三个字段:
pub struct PgRow {
pub(crate) data: DataRow,
pub(crate) format: PgValueFormat,
pub(crate) metadata: Arc<PgStatementMetadata>,
}
data 是这一行的原始字节和每列的偏移,metadata 是这条语句的列信息(列名、类型),同一次查询的所有行共享一个 Arc。format 是文本还是二进制:扩展查询协议下结果都是二进制,简单查询是文本,见扩展查询协议与语句缓存。
Row trait 只要求驱动实现两个方法:columns() 和 try_get_raw()。其余都是默认实现:
| 方法 | 做什么 | 失败时 |
|---|---|---|
try_get::<T, I>(index) | 找列、检查类型、解码 | 返回 Err |
get::<T, I>(index) | try_get(index).unwrap() | panic |
try_get_unchecked / get_unchecked | 找列、解码,不检查类型 | 同上 |
try_get_raw(index) | 找列,返回 PgValueRef | 返回 Err |
try_column / column、len、is_empty | 列元数据 |
get 带 #[track_caller],panic 报在你调用 get 的那一行。
ColumnIndex:列名还是下标
index 的类型是泛型 I: ColumnIndex<Self>,ColumnIndex 只有一个方法,把自己换算成下标:
pub trait ColumnIndex<T: ?Sized>: Debug {
// ...
fn index(&self, container: &T) -> Result<usize, Error>;
}
usize 的实现由宏 impl_column_index_for_row! 生成,只做越界检查,越界报 ColumnIndexOutOfBounds。&str 的实现在 Postgres 驱动里,查 metadata.column_names 这张 HashMap:
impl ColumnIndex<PgRow> for &'_ str {
fn index(&self, row: &PgRow) -> Result<usize, Error> {
row.metadata
.column_names
.get(*self)
.ok_or_else(|| Error::ColumnNotFound((*self).into()))
.copied()
}
}
列名按原样比较,大小写敏感。同名的两列在 HashMap 里只剩一个下标。src/bin/colnames.rs 打印 select 1 as personId, 1 as a, 2 as a, sum(1::int4) as s 的每一列,再按名字取 a:
personid INT4
a INT4
a INT4
s INT8
a = Ok(2)
没加引号的 personId 被 Postgres 折成了小写;两个 a 按名字只能取到后一个,要么起别名,要么按下标取。sum(int4) 的结果是 INT8,下面会用到。
try_get:检查发生在哪
Row::try_get 是整条解码路径:
fn try_get<'r, T, I>(&'r self, index: I) -> Result<T, Error>
where
I: ColumnIndex<Self>,
T: Decode<'r, Self::Database> + Type<Self::Database>,
{
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),
});
}
}
T::decode(value).map_err(|source| Error::ColumnDecode {
index: format!("{index:?}"),
source,
})
}
三步:
try_get_raw用ColumnIndex找列,错误原样返回(ColumnNotFound/ColumnIndexOutOfBounds),不包成ColumnDecode。- 值不是 NULL 时,用列的类型问
T::compatible。注意顺序:NULL 跳过类型检查。select null::text读成Option<i32>不报类型错,直接得到None,这一行没法告诉你类型写错了,要等到有一行非 NULL 才会暴露。 T::decode,失败包成ColumnDecode。读 NULL 进非Option类型就是在这一步报unexpected null。
index 用 {index:?} 格式化,所以列名带引号(column "n"),下标不带(column 0)。类型不匹配时的那句话由 mismatched_types 拼出来:
pub fn mismatched_types<DB: Database, T: Type<DB>>(ty: &DB::TypeInfo) -> BoxDynError {
// TODO: `#name` only produces `TINYINT` but perhaps we want to show `TINYINT(1)`
format!(
"mismatched types; Rust type `{}` (as SQL type `{}`) is not compatible with SQL type `{}`",
type_name::<T>(),
T::type_info().name(),
ty.name()
)
.into()
}
Rust 类型名来自 std::any::type_name,所以是带完整路径的 chrono::datetime::DateTime<chrono::offset::utc::Utc>。
实验:常见的几种错
src/bin/decode_errors.rs(节选):
let row = sqlx::query("select count(*) as n from person").fetch_one(&pool).await?;
println!("count(*) as i32 -> {}", row.try_get::<i32, _>("n").unwrap_err());
println!("count(*) as i64 -> {:?}", row.try_get::<i64, _>("n"));
println!("count(*) idx 0 -> {}", row.try_get::<i32, _>(0).unwrap_err());
let row = sqlx::query("select price from person where id = 1").fetch_one(&pool).await?;
println!("numeric as f64 -> {}", row.try_get::<f64, _>("price").unwrap_err());
let row = sqlx::query("select created_at from person where id = 1").fetch_one(&pool).await?;
println!("timestamp as Utc -> {}", row.try_get::<DateTime<Utc>, _>("created_at").unwrap_err());
println!("timestamp as Naive-> {:?}", row.try_get::<NaiveDateTime, _>("created_at"));
let row = sqlx::query("select now() as t").fetch_one(&pool).await?;
println!("timestamptz Naive -> {}", row.try_get::<NaiveDateTime, _>("t").unwrap_err());
println!("no column -> {}", row.try_get::<i32, _>("nope").unwrap_err());
println!("bad index -> {}", row.try_get::<i32, _>(3).unwrap_err());
输出(原样):
count(*) as i32 -> error occurred while decoding column "n": mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `INT8`
count(*) as i64 -> Ok(2)
count(*) idx 0 -> error occurred while decoding column 0: mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `INT8`
numeric as f64 -> error occurred while decoding column "price": mismatched types; Rust type `f64` (as SQL type `FLOAT8`) is not compatible with SQL type `NUMERIC`
timestamp as Utc -> error occurred while decoding column "created_at": mismatched types; Rust type `chrono::datetime::DateTime<chrono::offset::utc::Utc>` (as SQL type `TIMESTAMPTZ`) is not compatible with SQL type `TIMESTAMP`
timestamp as Naive-> Ok(2026-10-24T08:00:00)
timestamptz Naive -> error occurred while decoding column "t": mismatched types; Rust type `chrono::naive::datetime::NaiveDateTime` (as SQL type `TIMESTAMP`) is not compatible with SQL type `TIMESTAMPTZ`
no column -> no column found for name: nope
bad index -> column index out of bounds: the len is 1, but the index is 3
query_as 走的是同一条路:元组的 FromRow 按下标调 try_get。
let e = sqlx::query_as::<_, (i32,)>("select count(*) from person").fetch_one(&pool).await.unwrap_err();
println!("query_as (i32,) -> {e}");
println!("Debug -> {e:?}");
query_as (i32,) -> error occurred while decoding column 0: mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `INT8`
Debug -> ColumnDecode { index: "0", source: "mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `INT8`" }
三组类型对应关系值得记住:count(*) 和 sum(int4) 返回 INT8,读成 i64;NUMERIC 要用 rust_decimal / bigdecimal 的类型,或在 SQL 里转成 float8;TIMESTAMP(不带时区)对应 NaiveDateTime,TIMESTAMPTZ 对应 DateTime<Utc>。两种时间戳的二进制格式在 Postgres 类型。
try_get_unchecked:跳过检查会怎样
try_get_unchecked 少了中间那一步:
fn try_get_unchecked<'r, T, I>(&'r self, index: I) -> Result<T, Error>
where
I: ColumnIndex<Self>,
T: Decode<'r, Self::Database>,
{
let value = self.try_get_raw(&index)?;
T::decode(value).map_err(|source| Error::ColumnDecode {
index: format!("{index:?}"),
source,
})
}
T 只要求 Decode,不要求 Type。于是解码完全交给具体类型的 decode,它看到的只是一串字节。
try_get_unchecked 读错类型,会报错吗?M2 Max、macOS、rustc 1.99.0、Postgres 16.14。接着上面的程序:let row = sqlx::query("select 7::int8 as n").fetch_one(&pool).await?;
println!("unchecked 7::int8 -> {:?}", row.try_get_unchecked::<i32, _>("n"));
let row = sqlx::query("select 5000000000::int8 as n").fetch_one(&pool).await?;
println!("unchecked 5e9 -> {}", row.try_get_unchecked::<i32, _>("n").unwrap_err());
let row = sqlx::query("select price from person where id = 1").fetch_one(&pool).await?;
println!("unchecked numeric -> {:?}", row.try_get_unchecked::<f64, _>("price"));
price 那一行存的是 9.90。三行各打印什么?
query! 系列宏生成的代码用的就是 try_get_unchecked:类型在编译期已经对着数据库查过,运行期不再检查(见生成的代码)。手写代码用它,等于放弃了 compatible 这道保护。
FromRow:一行变成一个值
FromRow 只有一个方法:
pub trait FromRow<'r, R: Row>: Sized {
fn from_row(row: &'r R) -> Result<Self, Error>;
}
它对行类型 R 泛型,不对数据库泛型;'r 和 Decode<'r> 是同一个生命周期,理论上可以借行里的数据。sqlx-core 给 () 和 1 到 16 元组实现了它,元组由 impl_from_row_for_tuple! 生成,按下标逐个 try_get:
impl<'r, R, $($T,)+> FromRow<'r, R> for ($($T,)+)
where
R: Row,
usize: crate::column::ColumnIndex<R>,
$($T: crate::decode::Decode<'r, R::Database> + crate::types::Type<R::Database>,)+
{
#[inline]
fn from_row(row: &'r R) -> Result<Self, Error> {
Ok(($(row.try_get($idx as usize)?,)+))
}
}
元组按位置取,列多出来不报错,只是没人读;列少了报 ColumnIndexOutOfBounds。query_as / query_scalar 怎么调 from_row 在 Query、QueryAs 与 SqlSafeStr;query_scalar 其实是 query_as::<(O,)> 取第 0 个。
#[derive(FromRow)]
命名字段的结构体由 expand_derive_from_row_struct 展开:每个字段一条 let,按列名 try_get,最后组装结构体。元组结构体走 expand_derive_from_row_struct_unnamed,按下标取,不看字段属性。
字段属性决定这条 let 怎么写。属性组合由一个 match (attributes.flatten, attributes.try_from, attributes.json) 分派(源码),flatten 和 json 同时出现、try_from 和 json(nullable) 同时出现都会在宏里 panic!:
| 属性 | 生成的表达式 | 要求 |
|---|---|---|
| (无) | __row.try_get("name") | 字段类型 Decode + Type |
rename = "x" | 列名换成 "x" | |
容器上 rename_all = "camelCase" 等 | 列名按 heck 转换 | |
flatten | <Ty as FromRow>::from_row(__row) | 字段类型 FromRow |
try_from = "i64" | 先 try_get::<i64>,再 TryFrom,失败包成 ColumnDecode | 中间类型 Decode + Type |
json | try_get::<Json<_>>(..).map(|x| x.0) | Json<Ty>: Decode + Type |
json(nullable) | try_get::<Option<Json<_>>>,NULL 得 None | 字段是 Option |
skip | Default::default(),不读列 | 字段类型 Default |
字段或容器上 default | 在上面的结果后接 .or_else,只把 ColumnNotFound 换成默认值 | Default |
实验程序 src/bin/from_row.rs 的主角:
#[derive(Debug, FromRow)]
struct Audit {
created_at: chrono::NaiveDateTime,
}
#[derive(Debug, FromRow)]
struct Person {
#[sqlx(try_from = "i64")]
id: u8,
name: String,
#[sqlx(default)]
nickname: Option<String>,
#[sqlx(flatten)]
audit: Audit,
#[sqlx(json(nullable))]
meta: Option<Meta>,
#[sqlx(skip)]
posts: Vec<String>,
}
cargo expand 出来的 from_row(节选,try_from 的错误转换稍后单独讲):
impl<'a, R: ::sqlx::Row> ::sqlx::FromRow<'a, R> for Person
where
&'a ::std::primitive::str: ::sqlx::ColumnIndex<R>,
i64: ::sqlx::decode::Decode<'a, R::Database>,
i64: ::sqlx::types::Type<R::Database>,
String: ::sqlx::decode::Decode<'a, R::Database>,
// ...
Audit: ::sqlx::FromRow<'a, R>,
// ...
{
fn from_row(__row: &'a R) -> ::sqlx::Result<Self> {
let id: u8 = __row
.try_get("id")
.and_then(|v| {
<u8 as ::std::convert::TryFrom<i64>>::try_from(v)
.map_err(|e| {
// ...
})
})?;
let name: String = __row.try_get("name")?;
let nickname: Option<String> = __row
.try_get("nickname")
.or_else(|e| match e {
::sqlx::Error::ColumnNotFound(_) => {
::std::result::Result::Ok(Default::default())
}
e => ::std::result::Result::Err(e),
})?;
let audit: Audit = <Audit as ::sqlx::FromRow<'a, R>>::from_row(__row)?;
let meta: Option<Meta> = __row
.try_get::<::core::option::Option<::sqlx::types::Json<_>>, _>("meta")
.map(|x| x.and_then(|y| y.0))?;
let posts: Vec<String> = Default::default();
::std::result::Result::Ok(Person {
id,
name,
nickname,
audit,
meta,
posts,
})
}
}
生成的是对任意 R: Row 的泛型实现,约束全写在 where 子句里,所以同一个 derive 能用于 PgRow 和 SqliteRow。字段按声明顺序读,第一个出错的字段就让整个 from_row 返回。参数名叫 __row 而不是 row,是为了不和用户的字段名撞上(见下面的冷知识)。
几次查询的输出:
camel Camel { person_id: 1, display_name: "ann" }
snake no column found for name: personId
person Person { id: 1, name: "ann", nickname: None, audit: Audit { created_at: 2026-10-24T08:00:00 }, meta: Some(Meta { lang: "zh", tags: ["a"] }), posts: [] }
person Person { id: 2, name: "bob", nickname: None, audit: Audit { created_at: 2026-10-24T09:00:00 }, meta: None, posts: [] }
try_from error occurred while decoding column id: unprintable error: (unprintable type)
flatten no column found for name: created_at
default WithDefault { mood: "" }
default+null error occurred while decoding column "mood": unexpected null; try decoding as an `Option`
json null error occurred while decoding column "meta": unexpected null; try decoding as an `Option`
json bad error occurred while decoding column "meta": invalid type: integer `1`, expected a string at line 1 column 10
对应的查询和要点:
camel/snake:Camel带#[sqlx(rename_all = "camelCase")],字段person_id读的是列personId。SQL 里要写成带引号的"personId",否则 Postgres 把它折成personid;只给person_id列就报ColumnNotFound。person:select id, name, created_at, meta from person。查询里没有nickname列,default补成None;created_at被flatten的Audit读走;bob的meta是 NULL,json(nullable)给None;posts被skip。try_from:300::int8转u8失败,错误被包成ColumnDecode,列名不带引号(column id),因为这里是#id_s.to_string()而不是{index:?}。原因那一半丢了,下面单独讲。flatten:查询少了created_at,错误是内层Audit::from_row报的ColumnNotFound,外层原样往上传。default/default+null:WithDefault { #[sqlx(default)] mood: String }。列不存在时得到"";列存在但是 NULL 时照样报unexpected null。default只接住ColumnNotFound,不管 NULL。想「NULL 也给默认值」,字段用Option<T>再unwrap_or_default()。json null/json bad:Strict { #[sqlx(json)] meta: Meta }。不带nullable的json读 NULL 报unexpected null;内容和Meta对不上时,source是 serde_json 的错误原文,位置是 JSON 文本里的行列(serde_json 怎么拼这句话,见 serde 卷的错误)。
try_from 失败曾经报 ColumnNotFound0.8.0 之前,try_from 转换失败时生成的是 ::sqlx::Error::ColumnNotFound("FromRow: try_from failed".to_string())。上面那张表里 default 接住的正是 ColumnNotFound,于是带 default 的字段,转换失败会被悄悄换成默认值。
#3356(首次发布于 0.8.0)把它改成 ColumnDecode,提交说明:Breaking because #[sqlx(default)] on an individual field or the struct itself would have previously suppressed the error. This doesn't seem like good behavior as it could result in some potentially very difficult bugs. Instead of using TryFrom for these fields, just implement From and apply the default explicitly. 同一个 PR 把生成代码里的参数名从 row 改成 __row(use a parameter name that's less likely to collide),并加了 __spec_error!,用来把任意错误类型变成 ColumnDecode 的 source。
try_from 的原因为什么是 unprintable
__spec_error! 想做的事是:错误类型实现了 Error 就原样装箱,只实现了 Display 就转字符串,再退一步用 Debug,都没有就打印类型名。实现在 src/spec_error.rs,用的是「自动解引用特化」:给 &&&&SpecErrorWrapper<E>、&&&、&&、&、SpecErrorWrapper<E> 五层各写一个实现,约束从严到宽,方法解析从最外层开始试,第一个满足约束的胜出。可宏是这样调用的(__spec_error!):
macro_rules! __spec_error {
($e:expr) => {{
use $crate::spec_error::{SpecError, SpecErrorWrapper};
let wrapper = SpecErrorWrapper($e);
let wrap_err = wrapper.__sqlx_spec_error();
wrap_err(wrapper)
}};
}
接收者是 SpecErrorWrapper<E> 本身,不是 &&&&SpecErrorWrapper<E>。方法解析从 SpecErrorWrapper<E> 起步,自动加一层引用就匹配上了「没有约束」的那个实现,返回固定的 "unprintable error: (unprintable type)"。实验(src/bin/spec_error.rs):
let io = std::io::Error::other("boom");
println!("io::Error -> {}", sqlx::__spec_error!(io));
println!("&str -> {}", sqlx::__spec_error!("displayable"));
let e = u8::try_from(300_i64).unwrap_err();
println!("TryFromIntError -> {}", sqlx::__spec_error!(e));
// 同样的 trait,手动从 &&&& 开始探测
use sqlx::spec_error::{SpecError, SpecErrorWrapper};
let e = u8::try_from(300_i64).unwrap_err();
let wrapper = SpecErrorWrapper(e);
let f = (&&&&wrapper).__sqlx_spec_error();
println!("&&&& by hand -> {}", f(wrapper));
io::Error -> unprintable error: (unprintable type)
&str -> unprintable error: (unprintable type)
TryFromIntError -> unprintable error: (unprintable type)
&&&& by hand -> number too large to fit in target type
同一段代码在 0.8.6 下前三行输出完全一样。仓库里的 test_spec_error 只检查返回类型是 Box<dyn Error + Send + Sync>,不检查内容,所以没测出来。实际影响:try_from 失败时你只知道是哪一列,不知道原因。要看原因,自己实现 Decode,或者字段用中间类型、在 FromRow 之外转换。
动脑筋答案
src/bin/decode_errors.rs 里那三行的输出:
unchecked 7::int8 -> Ok(7)
unchecked 5e9 -> error occurred while decoding column "n": number too large to fit in target type
unchecked numeric -> Ok(2.78134232313401e-309)
Ok(7):Postgres 驱动的整数解码int_decode接受 1 到 8 字节,用BigEndian::read_int(buf, buf.len())读成i64,i32的Decode再try_into()。8 字节的7放得进i32,于是成功。try_get会在这之前就因为INT4和INT8不兼容而拒绝。number too large ...:同一条路,try_into()失败,消息是标准库TryFromIntError的Display。Ok(2.78134232313401e-309):f64的Decode在二进制格式下就是BigEndian::read_f64(value.as_bytes()?),把NUMERIC二进制表示的前 8 个字节当成 IEEE 754 读出来,得到一个没有意义的数,没有任何报错。compatible检查拦的就是这种情况。
小结
PgRow是一条DataRow加一份共享的Arc<PgStatementMetadata>;驱动只实现columns和try_get_raw,其余是Row的默认方法,get就是try_get加unwrap。- 列名经
column_names查下标,查不到报no column found for name: ...;下标越界报column index out of bounds。两者都不包成ColumnDecode。 try_get在值非 NULL 时用T::compatible检查列类型,不兼容报mismatched types; Rust type ... is not compatible with SQL type ...;NULL 跳过检查,交给decode报unexpected null。try_get_unchecked不检查类型,读错类型可能成功、可能报错、也可能静默得到垃圾值;query!宏用它,因为类型在编译期查过。#[derive(FromRow)]每个字段生成一次try_get;default只接住ColumnNotFound,不管 NULL;try_from失败报ColumnDecode,但 0.9.0 里原因总是unprintable error: (unprintable type)。