Appearance
第7章 Row 与 Column:动态结果集的最小稳定面
"A row is a contract: give me an index, I give you a value— but neither of us ever forgets the lifetimes of those values." —— 任何一次 fetch 后的真实约束
本章要点
Rowtrait(sqlx-core/src/row.rs:14)是整个 fetch 路径的交付面——fetch_many流里流出的就是DB::Row,之后所有解码都从 Row 上try_get。try_get_raw+columns是 Row 的两个必实现方法——其它 8 个方法(is_empty/len/column/try_column/get/get_unchecked/try_get/try_get_unchecked)都是基于这两个原语的默认实现。trait 用双原语表达丰富接口,和第 4 章 Executor 同一套设计手法。try_get的三层路径(row.rs:93-134):try_get_raw → value.type_info → T::compatible → T::decode——每一步失败都返回对应的Error::ColumnDecode/ColumnIndexOutOfBounds。ColumnIndextrait 让try_get(0)和try_get("name")共用同一个方法签名——usize和&str各自实现ColumnIndex<Row>,index()返回Result<usize, Error>。- PgRow 的存储(
sqlx-postgres/src/row.rs:14-18)——DataRow(借用服务端返回的字节)+PgValueFormat(Binary 或 Text)+Arc<PgStatementMetadata>(列信息共享)。 - MySqlRow 结构类似但字段名不同;SqliteRow 不借用 statement——它把每个值单独
SqliteValue::new拷贝出来(因为 SQLite 的sqlite3_column_*指针在下次step时失效)。 - SqliteRow 的
unsafe impl Send + Sync(sqlx-sqlite/src/row.rs:28-29)——手动保证线程安全,前提是"构造后不再触碰原 statement"。 Row: 'static(row.rs:14)—— Row 本身不借用外部数据,能跨 await 安全存活。但try_get_raw返回ValueRef<'_>——借用 row 的内部 buffer,生命周期绑到&'r self。
7.1 问题引入:fetch 后的世界
第 4 章讲完了 Executor::fetch_many 返回 BoxStream<Either<QueryResult, Row>>;第 5-6 章讲完了参数如何进、值如何解码。但中间有一个类型从头到尾伴随着每一次查询——Row。
考虑下面这段典型用法:
rust
let row = sqlx::query("SELECT id, name, email, created FROM users WHERE id = $1")
.bind(42_i32)
.fetch_one(&pool).await?;
let id: i32 = row.try_get("id")?;
let name: String = row.try_get("name")?;
let email: Option<String> = row.try_get("email")?;
let created: OffsetDateTime = row.try_get(3)?; // 按位置这里用户代码做了几件事:
fetch_one返回一个 Row。- 对 Row 做四次
try_get——三次按列名、一次按位置。 - 每个
try_get返回不同的 Rust 类型——i32/String/Option<String>/OffsetDateTime——类型由用户声明决定。
这几条能成立的前提是 Row 类型能做到三件事:
- 保存足够的信息(列名、类型、字节)以供后续 try_get。
- 按字符串或整数索引定位到一列。
- 返回一个 ValueRef,交给 Decode 做类型转换。
这章讲 Row 怎么做这三件事——以及为什么三家 DB 的 Row 内部存储完全不同。
7.2 Row trait 的形态
sqlx-core/src/row.rs:14-177 的 Row trait 一共定义了 10 个方法,其中只有两个必实现——columns(line 54)和 try_get_raw(line 175)。其它 8 个(is_empty / len / column / try_column / get / get_unchecked / try_get / try_get_unchecked)都是基于这两个的默认实现。
rust
pub trait Row: Unpin + Send + Sync + 'static {
type Database: Database<Row = Self>;
// 必实现
fn columns(&self) -> &[<Self::Database as Database>::Column];
fn try_get_raw<I>(&self, index: I) -> Result<DB::ValueRef<'_>, Error>
where I: ColumnIndex<Self>;
// 默认方法
fn is_empty(&self) -> bool { self.len() == 0 }
fn len(&self) -> usize { self.columns().len() }
fn column<I>(&self, index: I) -> &DB::Column where I: ColumnIndex<Self> { ... }
fn try_column<I>(&self, index: I) -> Result<&DB::Column, Error> where I: ColumnIndex<Self> { ... }
fn get<'r, T, I>(&'r self, index: I) -> T
where I: ColumnIndex<Self>, T: Decode<'r, DB> + Type<DB>;
fn try_get<'r, T, I>(&'r self, index: I) -> Result<T, Error>
where I: ColumnIndex<Self>, T: Decode<'r, DB> + Type<DB>;
fn try_get_unchecked<'r, T, I>(&'r self, index: I) -> Result<T, Error>
where I: ColumnIndex<Self>, T: Decode<'r, DB>;
fn get_unchecked<'r, T, I>(&'r self, index: I) -> T
where I: ColumnIndex<Self>, T: Decode<'r, DB>;
}必实现方法只有 columns 和 try_get_raw 这件事值得单独强调——这是和第 4 章 Executor 完全一致的设计手法:用一对最小原语表达丰富接口。try_get_raw 负责"按索引取 ValueRef",columns 负责"列出所有列"。其它方法全是组合——try_get 是 try_get_raw + compatible + decode、try_column 是 columns()[index]、len 是 columns().len()、get* 是 try_get*().unwrap()。
super-trait bound Unpin + Send + Sync + 'static 是第 3 章 §3.4.1 讨论过的:
Unpin:可以在 BoxStream 里自由移动。Send + Sync:可以跨 tokio task 共享。'static:Row 不能借用外部数据——所有字节必须 owned 或来自Arc共享。
这条 'static bound 是 Row 整个设计的基石——它让 Row 可以随便 Vec::collect、放进 channel、跨 await。但它也约束 Row 必须自持数据——Postgres 的 DataRow 内部不是 &[u8](借用 TCP buffer)而是 Bytes(Arc<[u8]> 的 cheap-clone 版本),SQLite 的 row 是 Box<[SqliteValue]>(独立分配)。
7.3 try_get 的三层路径
try_get 是用户最常用的方法,也是最值得精读的。sqlx-core/src/row.rs:111-133:
rust
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(&index):把 I 转 usize 下标(通过 ColumnIndex::index),从 row 里取一个 ValueRef<'_>。失败返回 Error::ColumnNotFound 或 ColumnIndexOutOfBounds。
第二层 — 类型兼容检查:如果 value 非 NULL,取它的 type_info(),调用 T::compatible(&ty)。不兼容返回 Error::ColumnDecode 带 mismatched_types 描述。
第三层 — T::decode(value):调 Decode 实现做字节解码。失败返回 Error::ColumnDecode 带源错误。
关键细节 1:NULL 不触发兼容检查。if !value.is_null() 让 NULL 列直接跳到 decode——让 try_get::<Option<i32>, _>(null_column) 成功返回 None,而不是因为 "NULL 和 INT4 不兼容" 报错。Decode 实现里 Option<T> 的 blanket(第 5 章 §5.6)会接住 is_null 的 ValueRef 返回 Ok(None)。
关键细节 2:ty.is_null() && ... 这个短路判断——如果 type_info() 自己是 NULL(表示服务端说"这列是未知类型"),也跳过兼容检查。这是对 Postgres 的 UNKNOWN 类型的宽容——SELECT 'hello' 返回列类型是 UNKNOWN,try_get::<String, _> 能兼容因为 String::compatible 也接受 UNKNOWN(第 5 章 §5.3.1)。
关键细节 3:错误信息包含 index——format!("{index:?}") 把用户传入的索引(整数或字符串)格式化到错误里。这让 try_get::<i32, _>("foo") 失败时你能看到 ColumnDecode { index: "\"foo\"", ... }——精确定位是哪列出问题。
7.3.0 try_get 能报哪些 Error
try_get 可能产生四类错误,了解它们对调试很有帮助:
| 错误 | 触发场景 | 来源 |
|---|---|---|
Error::ColumnNotFound(name) | try_get("foo") 列不存在 | ColumnIndex::<&str> 返回 |
Error::ColumnIndexOutOfBounds | try_get(99) 超出列数 | ColumnIndex::<usize> 返回 |
Error::ColumnDecode { mismatched } | try_get::<i32>("name") 但 name 是 TEXT | compatible 检查失败 |
Error::ColumnDecode { source } | try_get::<i32>("maybe_overflow") 字节是 i64 且超 i32 范围 | Decode::decode 返回 Err |
所有 ColumnDecode 错误都带 index 字段——format!("{index:?}") 把用户传入的索引(整数或带引号的字符串)格式化进去。一个常见困扰是字符串索引在错误里有引号:"\"foo\"" 而不是 "foo"——这是 {:?} 的 Debug 格式特性,用 Display 会少引号但少了"用户明显传了字符串"的信息。
实际排查 decode 错误的标准步骤:
- 看错误类型——ColumnNotFound 是列名拼错,OutOfBounds 是位置越界,ColumnDecode 是解码问题。
- ColumnDecode 里再看
source——如果是mismatched_types,Rust 类型和列类型不对;如果是其它,decode 内部失败(字符串 UTF-8、整数溢出等)。 - 用
row.column(index).type_info()确认列实际类型——和你期望的对不对。 - 看
row.columns().iter().map(|c| c.name()).collect::<Vec<_>>()确认有哪些列——用来定位拼写错误。
7.3.1 try_get_unchecked 跳过兼容检查
对比 try_get,try_get_unchecked(row.rs:146-154)少了中间一层:
rust
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::compatible检查——直接 decode。 - trait bound 少
Type<DB>——不需要类型声明。
谁用 unchecked?query! 宏生成的代码。第 1 章 §1.5.1 讨论过——宏在编译期已经通过 describe 验证了 SQL 列类型和 Rust 类型兼容,运行时再检查一遍是重复劳动。所以生成代码用 try_get_unchecked 跳过 compatible。
直接调用 try_get_unchecked 的代价是解码错误的信息可能不清晰——compatible 检查失败给"类型不匹配",decode 里失败给"字节解析失败"。但对宏生成代码,这个代价不存在(编译期保证过了)。对手写代码,优先用 try_get 除非你确定要跳过。
7.4 ColumnIndex:统一字符串和整数索引
try_get<T, I: ColumnIndex<Self>> 的 I 参数是统一索引类型。ColumnIndex 让 try_get(0) 和 try_get("name") 共用同一个方法。
sqlx-core/src/column.rs:35-44:
rust
pub trait ColumnIndex<T: ?Sized>: Debug {
fn index(&self, container: &T) -> Result<usize, Error>;
}一个方法:接受对容器(Row 或 Statement)的引用、返回有效的 usize 下标或错误。
三个基本实现分布在三个地方:
usize 的实现(column.rs:57-68,通过 impl_column_index_for_row! 宏):
rust
impl ColumnIndex<$R> for usize {
fn index(&self, row: &$R) -> Result<usize, Error> {
let len = Row::len(row);
if *self >= len {
return Err(Error::ColumnIndexOutOfBounds { len, index: *self });
}
Ok(*self)
}
}usize 就是自己——只做越界检查。
&str 的实现(每个驱动各自,例如 sqlx-postgres/src/row.rs:45-52):
rust
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()
}
}字符串索引查 column_names: HashMap<UStr, usize>——O(1) lookup。找不到返回 ColumnNotFound。
&I where I: ColumnIndex<T>(column.rs:47-52)的 blanket:
rust
impl<T: ?Sized, I: ColumnIndex<T> + ?Sized> ColumnIndex<T> for &'_ I {
fn index(&self, row: &T) -> Result<usize, Error> {
(**self).index(row)
}
}让 try_get(&"foo") 和 try_get("foo") 都合法——引用和值都可以。这对用户代码里"变量传递"场景很友好。
column_names 的预构建是 Row 类型性能的关键——PgRow 的 metadata 是 Arc<PgStatementMetadata>,statement metadata 在 PREPARE 时就已经把列名哈希表建好,row 直接共享。每行查字符串都是 O(1)——没有 O(n) 的列表线性扫描。
7.5 Column trait 的极简
sqlx-core/src/column.rs:6-22:
rust
pub trait Column: 'static + Send + Sync + Debug {
type Database: Database<Column = Self>;
fn ordinal(&self) -> usize;
fn name(&self) -> &str;
fn type_info(&self) -> &<Self::Database as Database>::TypeInfo;
}三个方法——ordinal(列序号)、name(列名或别名)、type_info(列类型)。就这么简单。
为什么只有三个方法? 因为 Column 的使命是"列元数据"——不是"列数据"。它告诉你这列叫什么、是第几列、类型是什么,但不持有任何值。值在 Row 的 ValueRef 里。
super-trait bound 'static + Send + Sync + Debug——Column 独立生存、跨线程共享、可打印。实际三家驱动的 PgColumn / MySqlColumn / SqliteColumn 都是 small struct,实现 Clone,经常通过 Arc 共享。
每家驱动的 Column 还有扩展字段——比如 PgColumn(sqlx-postgres/src/column.rs:7-15):
rust
pub struct PgColumn {
pub(crate) ordinal: usize,
pub(crate) name: UStr,
pub(crate) type_info: PgTypeInfo,
pub(crate) relation_id: Option<Oid>, // 表的 OID
pub(crate) relation_attribute_no: Option<i16>, // 列在表里的 1-based 位置
}后两个字段是 Postgres 独有的——描述这列"来自哪张表的第几列"。表达式列(例如 SELECT a + b FROM t)的两个字段都是 None。这些元数据用于编译期 nullability 推断(query! 宏的 pg_attribute 查询会用到)——第 11 章详细。
MySQL 和 SQLite 的 Column 没有这些字段——各自协议不提供同级信息。
7.6 PgRow:借用 DataRow 字节
sqlx-postgres/src/row.rs:14-18:
rust
pub struct PgRow {
pub(crate) data: DataRow,
pub(crate) format: PgValueFormat,
pub(crate) metadata: Arc<PgStatementMetadata>,
}三个字段:
data: DataRow——Postgres 协议返回的原始行数据。DataRow内部是storage: Bytes(bytescrate,cheap-clone 的 Arc<[u8]>)+ 一个列偏移表。format: PgValueFormat——Binary 或 Text。整行统一,不按列变。metadata: Arc<PgStatementMetadata>——列信息、列名哈希表。多行共享(同一个 statement 执行出的所有 row 共享同一 metadata)。
try_get_raw 实现(row.rs:27-42):
rust
fn try_get_raw<I>(&self, index: I) -> Result<PgValueRef<'_>, Error>
where I: ColumnIndex<Self>,
{
let index = index.index(self)?;
let column = &self.metadata.columns[index];
let value = self.data.get(index);
Ok(PgValueRef {
format: self.format,
row: Some(&self.data.storage),
type_info: column.type_info.clone(),
value,
})
}步骤:
- 通过
ColumnIndex::index把I转成 usize。 - 从 metadata.columns 取
PgColumn。 - 从
data.get(index)取Option<&[u8]>——列的字节切片(或 None 表示 NULL)。 - 构造
PgValueRef打包返回。
PgValueRef::row: Some(&self.data.storage) 这个字段是什么?它是对整行 Bytes 的借用——让 PgValueRef::as_bytes() 能返回零拷贝的 &[u8]。
为什么要借用整行而不是单列?因为 PgValueRef::to_owned() 在 §3.4.3 讨论过——"Postgres 的 to_owned 是 O(1) 引用计数递增"——PgValue 内部的 Bytes 也 clone 自 data.storage。多个 PgValue 可以同时存在、共享同一份 DataRow 的底层字节、整体 O(1)——这条优化依赖整行 Bytes 的共享引用。
7.6.0 DataRow 的内部:存储 + 偏移表
PgRow::data 的类型 DataRow 在 sqlx-postgres/src/message/data_row.rs:10-16:
rust
pub struct DataRow {
pub(crate) storage: Bytes,
pub(crate) values: Vec<Option<Range<u32>>>,
}两个字段:
storage: Bytes——整条 Postgres DataRow 消息的原始字节缓冲。values: Vec<Option<Range<u32>>>——每列一个Option<Range<u32>>:None表示 NULL,Some(start..end)表示该列在 storage 里的字节偏移范围。
用 Range<u32> 而不是 Range<usize> 是省内存的刻意选择——源码注释写得很清楚:"Values cannot be larger than i32 in postgres"——Postgres 协议的单值最大 i32::MAX 字节,所以 u32 够用。30 列的 row,values 向量本身 30 × 8 = 240 字节(如果 usize 要 480 字节)。这种优化在批量 fetch 百万行时有可观收益。
DataRow::get(data_row.rs:21-27):
rust
pub(crate) fn get(&self, index: usize) -> Option<&'_ [u8]> {
self.values[index]
.as_ref()
.map(|col| &self.storage[(col.start as usize)..(col.end as usize)])
}get(index) 按 index 索引 values 向量、读 Range、从 storage 切片出字节。零拷贝——切片 &[u8] 的生命周期绑到 &self(即 DataRow)。
DataRow::decode_body(data_row.rs:31- 下方)是协议解析——从 Postgres 发来的原始字节按"列数 u16 + 每列 (长度 i32 + N 字节数据)"格式逐列解析,构建 values 向量。这个解析本身不复制数据,只记录 Range。真正持有字节的是 storage: Bytes——由 hyper-util 的网络层 deliver 过来。
7.6.1 PgValueFormat 的一行决定
format: PgValueFormat 这一字段看似小但影响巨大:
- Binary:每列字节按 Postgres 二进制格式(整数大端、浮点 IEEE 754、字符串 UTF-8 直接存、时间戳按 microseconds 相对 2000-01-01)。
- Text:每列字节是 UTF-8 文本(整数用 ASCII、时间戳用
YYYY-MM-DD HH:MM:SS.mmmuuu±TZ格式)。
整个 Row 所有列共享一个 format——由查询使用的协议模式决定:
- Extended Query(默认,用 prepared statement)→ Binary。
- Simple Query(
SELECT ...裸字符串发过去)→ Text。
sqlx 默认用 Extended Query(走 Bind + Execute),所以大多数场景是 Binary。Text 模式出现在 raw_sql! 或多语句拼接的 simple query 场景。
这条差异让每个 Decode 实现必须处理两种格式——第 5 章 §5.5.1 的 bool Decode 就是例子,要 match 两种 format 分别解析。对实现者来说多一倍工作量,但对用户透明。
7.7 MySqlRow:和 Postgres 神似
sqlx-mysql/src/row.rs:13-18:
rust
pub struct MySqlRow {
pub(crate) row: protocol::Row,
pub(crate) format: MySqlValueFormat,
pub(crate) columns: Arc<Vec<MySqlColumn>>,
pub(crate) column_names: Arc<HashMap<UStr, usize>>,
}结构和 PgRow 几乎一样——row(协议字节)+ format(Binary / Text)+ columns(Arc 共享的列信息)+ column_names(Arc 共享的哈希表)。
唯一结构差异是 columns 和 column_names 分开存——PgRow 用 Arc<PgStatementMetadata> 把它们装一起。这只是 bookkeeping 差异,语义一致。
try_get_raw(sqlx-mysql/src/row.rs:27-39)几乎逐行对应 PgRow 的实现——拿 index、拿 column、拿 value 字节、打包成 MySqlValueRef。
MySQL 的 format 同样有两种:
- Binary:prepared statement 的 COM_STMT_EXECUTE 结果。
- Text:COM_QUERY 的裸 SQL 结果。
这组二元差异是 sqlx Postgres / MySQL 驱动共同面对的复杂度——每个 Decode 实现都要 match 两种格式。SQLite 没有这个维度(§7.8)。
7.8 SqliteRow:为什么必须独立拷贝
sqlx-sqlite/src/row.rs:15-20:
rust
pub struct SqliteRow {
pub(crate) values: Box<[SqliteValue]>,
pub(crate) columns: Arc<Vec<SqliteColumn>>,
pub(crate) column_names: Arc<HashMap<UStr, usize>>,
}values: Box<[SqliteValue]> ——独立持有每列的 value。不是借用外部 buffer。
为什么和 Postgres/MySQL 截然不同?因为 SQLite 的 sqlite3_column_* 函数返回的指针在下次 sqlite3_step 时失效。SQLite 不是流式协议——每次 step 覆盖上一行的列数据,想在两行之间同时持有两行数据必须拷贝。
SqliteRow::current(row.rs:34-52)的构造:
rust
pub(crate) fn current(
statement: &StatementHandle,
columns: &Arc<Vec<SqliteColumn>>,
column_names: &Arc<HashMap<UStr, usize>>,
) -> Self {
let size = statement.column_count();
let mut values = Vec::with_capacity(size);
for i in 0..size {
values.push(unsafe {
let raw = statement.column_value(i);
SqliteValue::new(raw, columns[i].type_info.clone())
});
}
Self {
values: values.into_boxed_slice(),
columns: Arc::clone(columns),
column_names: Arc::clone(column_names),
}
}每个列都 SqliteValue::new ——这个 new 内部对文本 / blob 做 Vec::from(slice) 拷贝。每次 step 产出一行都复制一次所有列。
代价:SQLite 的 fetch 比 Postgres / MySQL 多一次 memcpy。批量 fetch 大量 blob 时这个代价可观。
收益:SqliteRow 可以独立生存——你可以 fetch_all 收集 1000 行,它们之间互不影响。如果 sqlx 选择不拷贝(让 SqliteRow 借用 statement),用户就不能收集——每拿一行就失效。这个设计是保用户 API 一致性的权衡。
7.8.1 手动 unsafe impl Send + Sync
row.rs:28-29:
rust
unsafe impl Send for SqliteRow {}
unsafe impl Sync for SqliteRow {}这两行是 sqlx-sqlite 里少数的手动 unsafe。正上方有一段解释注释(row.rs:22-26):
Accessing values from the statement object is
safe across threads as long as we don't call [sqlite3_step]
we block ourselves from doing that by only exposing
a set interface on [StatementHandle]意思是:SQLite 的 sqlite3_value_* API 一旦脱离 statement 后是线程安全的(只读)——但调 sqlite3_step 时不能有其他线程访问。sqlx 通过不暴露 step 方法到 SqliteRow 上来保证这条不变量——用户拿到 SqliteRow 后只能 try_get,不能触发 step。
这是 Rust "unsafe impl 换来 API 对称" 的典型做法——编译器不会自动推 Send/Sync(因为内部有 *mut sqlite3_value),但库作者可以手动承诺"满足不变量"。前提是这个不变量必须在 API 设计里真正可维护。
7.9 生命周期骨架:Row 的 'static vs ValueRef 的 'r
本章开头提到 Row: 'static——但 try_get_raw 返回的 ValueRef<'_> 借用 row 的内部。这两条怎么协同?
关键是**ValueRef<'_> 里的 _ 是 &'r self 的生命周期**——不是 'static。翻看 try_get_raw 的签名:
rust
fn try_get_raw<I>(&self, index: I) -> Result<<Self::Database as Database>::ValueRef<'_>, Error>
where I: ColumnIndex<Self>;ValueRef<'_> 的 '_ 因为 Rust 生命周期省略规则,等价于 ValueRef<'r> where 'r = lifetime of &self——borrower 的生命周期绑到 &self 上。也就是说:
- Row 自身是 'static 生命的值(可以放进 channel、Vec、tokio::spawn)。
- 从 Row 借出来的 ValueRef 只能在
&row借用的作用域内活。 - decode 出
String/i32等 owned 类型后,ValueRef 可以丢弃,Row 仍然活着。 - decode 出
&'r str等借用类型,那个&str的生命周期 tie 到 row——row drop 就失效。
这条生命周期架构让 Row 用起来既灵活(能跨 await)、又零拷贝(按需借用)。用户可以选:
rust
// 独立生存的 String——能跨 await 返回
async fn get_name(pool: &PgPool) -> Result<String> {
let row = query("SELECT name FROM users").fetch_one(pool).await?;
row.try_get::<String, _>(0) // Row drop 不影响 String
}
// 借用 row 的 &str——不能跨越 row 的作用域
let row = query(...).fetch_one(&pool).await?;
let name: &str = row.try_get(0)?;
log::info!("{name}"); // OK
drop(row);
// log::info!("{name}"); // ERROR: name 已失效两种都合法、都有代价——独立 String 多一次 memcpy、&str 受 row 生命周期约束。用户按场景选。
7.10 三家对照表
把三家 Row 的内部结构并排:
| 字段 / 行为 | PgRow | MySqlRow | SqliteRow |
|---|---|---|---|
| 核心数据 | data: DataRow(Bytes 共享) | row: protocol::Row(Bytes 共享) | values: Box<[SqliteValue]>(独立拷贝) |
| 列元数据 | metadata: Arc<PgStatementMetadata> | columns: Arc<Vec<MySqlColumn>> | columns: Arc<Vec<SqliteColumn>> |
| 列名哈希 | metadata.column_names 内 | column_names: Arc<HashMap> | column_names: Arc<HashMap> |
| Binary/Text 格式 | format: PgValueFormat | format: MySqlValueFormat | 无(C API 直接按类型访问) |
| to_owned 代价 | O(1) Bytes clone | O(1) Bytes clone | O(n) memcpy(value 已在构造时拷贝) |
| fetch 时开销 | 几乎零 | 几乎零 | 每行每列一次拷贝 |
| unsafe impl Send/Sync | 不需要(结构自动 Send/Sync) | 不需要 | 需要(因为内部含 *mut sqlite3_value) |
| 代码量 | ~75 行 | ~51 行 | ~89 行 |
几条读表观察:
- PgRow / MySqlRow 几乎同构——借用协议返回的共享字节。差异只在字段命名和 MySQL 多一个独立 column_names。
- SqliteRow 形态本质不同——独立持有 value,构造时拷贝。这是 SQLite C API 的约束倒逼的设计。
- unsafe 只出现在 SQLite——因为它是唯一一家需要跨 step 保留数据、结构里含裸指针的驱动。
- 代码量 PgRow 反而不是最多——SqliteRow 更长是因为
current方法要手动拷贝构造。
7.11 column_names 的哈希表:O(1) 名字查找
三家都用同一个数据结构表达"列名 → 下标" 查询:HashMap<UStr, usize>(或等价)。
UStr(sqlx-core/src/ext/ustr.rs)是 sqlx 内部的可哈希共享字符串——Arc<str> 的包装。相比 String:
- 字符串内容通过 Arc 共享——多个 Row 引用同一列名不重复拷贝。
- 可以作为 HashMap 的 key(实现 Hash + Eq)。
- Clone 是 O(1)(增加 refcount)。
HashMap 的构造发生在 prepare 阶段(第 12 章 Connection::prepare_with)——一条 SQL 的列信息在准备完成时就已经建好 column_names,后续所有 row 共享同一份 Arc。fetch_all 出 1000 行时,这 1000 行的 column_names 都指向同一个 HashMap 实例——只增加 Arc 的 refcount,不重新构建。
O(1) lookup 对高频按名访问至关重要。考虑 #[derive(FromRow)] 的展开(第 8 章):
rust
// #[derive(FromRow)] struct User { id: i32, name: String } 展开大致为:
User {
id: row.try_get("id")?,
name: row.try_get("name")?,
}每次 try_get("id") 和 try_get("name") 都做一次 HashMap 查询——如果是 O(n) 线性扫描列名列表,对 30 列宽表做一次 FromRow 解码是 O(n²)。HashMap 把这个降到 O(n)。实际生产里这条优化在长列表场景下不可或缺。
7.11.1 UStr 的内部与替代选择
为什么不直接用 String 做 HashMap 的 key?UStr 解决的是三件事:
1. 多行共享同一份字符串。一条 SQL SELECT id, name, email FROM users fetch 1000 行,每行的 column_names 都引用同一个 key 集合——UStr 是 Arc<str> 的包装,clone 是 O(1) 增 refcount,不分配新堆内存。如果用 String,1000 行 × 3 列 = 3000 次字符串拷贝。
2. 字符串内容不可变。HashMap 的 key 按 Rust 语义不能 mut borrow,但 String 的 capacity 可变——Arc<str> 是不可变字符串的标准表达,语义更贴合 key 场景。
3. 实现 Hash + Eq + Borrow<str>。UStr 实现 Borrow<str>,让 hashmap.get("foo") 能直接用 &str 查询不需要 String 分配——和 HashMap<String, V> 相比省一次 &String → &str 的类型转换。
替代方案有:
Cow<'static, str>——适合"混合字面量和运行时字符串"的场景,但共享语义不如 Arc 明确。smartstring::SmartString——短字符串内联优化(≤23 字节不堆分配)。适合列名大多很短的场景,但 sqlx 没引入这个依赖。compact_str::CompactString——类似 smartstring。
sqlx 选了最简单的 Arc<str> 包装——没 inline 优化、但代码量最小、行为最清晰。对列名 lookup 这种"短字符串 × 高频查"场景,Arc<str> 的性能和专用短字符串库相差不大(L1 cache 命中率主导)。
7.12 PgRow 的 Debug:为什么 Debug 输出值而不是字节
sqlx-postgres/src/row.rs:58-73 有一个有意思的 Debug 实现:
rust
impl Debug for PgRow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PgRow ")?;
let mut debug_map = f.debug_map();
for (index, column) in self.columns().iter().enumerate() {
match self.try_get_raw(index) {
Ok(value) => {
debug_map.entry(
&column.name,
&Postgres::fmt_value_debug(&<PgValueRef as ValueRef>::to_owned(&value)),
);
}
Err(error) => {
debug_map.entry(&column.name, &format!("decode error: {error:?}"));
}
}
}
debug_map.finish()
}
}println!("{row:?}") 会输出 PgRow { "id": "42", "name": "Alice", "email": "alice@example.com" }——实际解码后的值而不是原始字节。
这个 Debug 实现的代价是"每次 format 都做一次 decode"——大行(比如一列 10MB 的 JSONB)的 Debug 会重新分配并解码。实际生产里几乎只在 tracing::debug!("{row:?}") 打印行时会触发——但那条日志本身已经在诊断场景了,多付点 Debug 开销可以接受。
更精妙的是 Postgres::fmt_value_debug——它来自 TypeChecking trait(第 5 章 §5.13),根据列的 TypeInfo 选择合适的展示方式。比如 bytea 列显示为 \x48656c6c6f 十六进制、timestamptz 列显示为 ISO 8601 字符串——比 Debug 原始字节友好得多。
MySQL 和 SQLite 的 Row 没有类似的 Debug 实现——它们只做 #[derive(Debug)] 默认的字段级打印,不递归 decode。这是 Postgres Row 相对"友好"的一处——也是它稍复杂的代价。
7.13 本章小结
本章把 Row 和 Column 从 trait 定义到三家具体实现完整打开:
- Row trait 的最小原语(§7.2)——
columns+try_get_raw是必实现的两个方法,其它 8 个方法都基于它们派生。双原语表达丰富接口是 Executor trait 和 Row trait 共同的设计手法。 - try_get 的三层路径(§7.3)——raw → compatible → decode,每一层有明确的失败分支。NULL 和 UNKNOWN 跳过兼容检查让
Option<T>和未知类型友好处理。 - try_get_unchecked 跳过 compatible(§7.3.1)——给
query!宏用,因为编译期已验证过。手写代码优先用 checked 版本。 - ColumnIndex 统一索引(§7.4)—— usize 和 &str 实现同一 trait,让
try_get(0)和try_get("name")同一签名。blanket impl 让&I也合法。 - Column trait 只有三方法(§7.5)——ordinal / name / type_info。每家驱动有扩展字段(PgColumn 的 relation_id / relation_attribute_no 用于 pg_attribute 查询)。
- PgRow 借用 DataRow 的 Bytes(§7.6)——
data.storage是 Arc<[u8]>,多个 PgValueRef 可以共享同一份字节。 - PgValueFormat Binary/Text 分发(§7.6.1)——整行统一 format,由协议模式决定。Extended Query → Binary,Simple Query → Text。
- MySqlRow 结构近似(§7.7)——字段组织略有不同但语义一致。
- SqliteRow 独立拷贝(§7.8)——C API 约束导致每行构造时拷贝所有 value,换来跨步存活能力。
- unsafe impl Send + Sync(§7.8.1)——SQLite 唯一手动标注的线程安全,依赖"不暴露 step" 的 API 不变量。
- Row 'static / ValueRef 'r(§7.9)——Row 独立生存可跨 await,ValueRef 借用 Row。decode 出 owned 类型跳出借用链,decode 出
&str被 row 生命周期约束。 - column_names O(1) lookup(§7.11)——HashMap
<UStr, usize>通过 Arc 共享,prepare 时一次构建,每次 fetch_all 零重复。对 FromRow 解码性能至关重要。 - PgRow 的 Debug 实现解码值(§7.12)——
{row:?}输出可读字符串而不是原字节,靠TypeChecking::fmt_value_debug做类型感知格式化。
7.13.1 三家 fetch 的性能差异
把三家的单行 fetch 路径开销放一起对比:
| 阶段 | PgRow | MySqlRow | SqliteRow |
|---|---|---|---|
| 读网络 / C API | async read from TCP → Bytes | async read from TCP → Bytes | sqlite3_step (同步 C call) |
| 协议解析 / 构造 values | 计算 Vec<Option<Range<u32>>> | 类似 | 循环 sqlite3_column_value + SqliteValue::new |
| 构造 Row | 三指针(data / format / metadata) | 四指针(row / format / columns / column_names) | 三指针(values / columns / column_names) |
| try_get_raw(i) | ValueRef 构造:O(1) index + borrow slice | 类似 | ValueRef 构造:O(1) from pre-built SqliteValue |
| try_get_raw("name") | HashMap O(1) + 以上 | 同 | 同 |
| decode i32 | 4 字节 BigEndian read | 4 字节 LittleEndian read | 直接从 SqliteValue::Int 变体读 |
| decode String | memcpy 字节 → String | memcpy 字节 → String | clone Cow 到 String |
实际性能差异几何? 本书没有 benchmark 数字(不编造),但从源码结构能判断:
- PgRow / MySqlRow 的 try_get 主导于 HashMap lookup + byte slice——几十纳秒级。
- SqliteRow 的 try_get 主导于 variant match + value clone——同样几十纳秒级。
- 三家 fetch 整行成本几乎等同——差异在网络 RTT(TCP 1-5ms vs SQLite 0ms)远大于本地解码。
SQLite 的真正优势不是解码快,是完全没有网络往返。Postgres 和 MySQL 的 fetch 95% 时间花在等 TCP 响应上;SQLite 是同步进程内 C 调用,一行 fetch 典型 10-50μs(从 step 返回到 row 构造完毕)。
这也回答了一个常见问题"为什么 sqlx 的 Postgres fetch 比 SQLite 慢"——不是 sqlx 慢,是网络慢。换 tokio-postgres 结果一样,甚至 sqlx 的额外 compatible 检查只贡献 20-50ns(相对于 1ms 级 RTT 完全可以忽略)。
7.14 实战:读一行宽表的完整路径
把本章所有内容放进一次真实查询里。假设你有一张 users 表 7 列,查询:
rust
let row = sqlx::query(
"SELECT id, email, created_at, is_active, settings, last_login, avatar_url
FROM users WHERE id = $1"
).bind(42).fetch_one(&pool).await?;
let user = User {
id: row.try_get("id")?,
email: row.try_get::<String, _>("email")?,
created_at: row.try_get::<OffsetDateTime, _>("created_at")?,
is_active: row.try_get::<bool, _>("is_active")?,
settings: row.try_get::<Json<Settings>, _>("settings")?,
last_login: row.try_get::<Option<OffsetDateTime>, _>("last_login")?,
avatar_url: row.try_get::<Option<String>, _>("avatar_url")?,
};这段代码在 Postgres 下完整路径(一次 fetch + 七次 try_get):
fetch 阶段:
- 服务端返回
DataRow消息,约 300 字节(假设 email 50 字节、json settings 100 字节、其他 150 字节)。 - sqlx 的 Postgres 驱动把字节装进
Bytes(Arc<[u8]>,cheap-clone)。 - 按列解析出
values: Vec<Option<Range<u32>>>——7 个 Range,约 56 字节 overhead。 - 构造
PgRow { data: DataRow, format: Binary, metadata: Arc<...> }——总共 3 个指针大小 + DataRow 自身。
try_get 阶段(按 "id" 为例):
"id".index(&row)→ HashMap lookup →Ok(0)。try_get_raw(0)→PgValueRef { row: Some(&storage), value: Some(&bytes[0..4]), ... }。Type::<i32, Postgres>::compatible(&INT4)→ true。Decode::<i32, Postgres>::decode(value)→BigEndian::read_i32(&bytes[0..4])→ 42。
每次 try_get 约 50ns 的纯 CPU 开销(HashMap lookup 20ns + decode 30ns),零堆分配。
整行七次 try_get 的总代价:
- 4 次基本类型(id/is_active/created_at/last_login)——每次约 50ns,都是栈值。
- 2 次 String 类型(email/avatar_url)——每次约 100ns,包含 memcpy 字符串字节到新 String。
- 1 次 Json 类型(settings)——约 500ns(含 serde_json::from_slice)。
总和约 1μs/行。这是 sqlx 的"标准 fetch 单行成本"基线——如果你看到比这个高很多(比如 100μs),基本可以排除 sqlx 自身问题,问题在网络或服务端。
和直接用 tokio-postgres 对比:tokio-postgres 的 Row::get 也是类似路径,但没有 compatible 检查——稍快 10-20%。这条差距就是"运行时类型安全 vs 极致性能"的具体数字。如果你的业务热点在 row 解码上(比如做 ETL 每秒处理百万行),可以考虑 try_get_unchecked 跳过 compatible,或直接用 tokio-postgres。对大多数业务代码,多的这 10% 不构成瓶颈。
7.14.1 常见 Row 使用陷阱及调试路径
在生产里遇到 Row 相关问题时,下面这几种情况最常见:
陷阱 1:Error::ColumnNotFound("foo"),但表里明明有 foo 列
排查方向:
- 列名是否区分大小写?Postgres 非引号列名被 lowercase 化,
SELECT FOO返回列名是foo;SELECT "Foo"返回Foo。MySQL/SQLite 略有差异。 SELECT *下列名是否有 schema 前缀?某些 JOIN 查询列名会是table.foo。- 是否用了别名?
SELECT foo AS bar——那就得try_get("bar")。
debug 技巧:先 row.columns().iter().map(|c| c.name()).collect::<Vec<_>>() 看实际列名。
陷阱 2:ColumnDecode { mismatched },列类型和 Rust 类型看起来一致
排查方向:
- Postgres 的
NUMERIC(10,2)对应 Rust 的BigDecimal(需要bigdecimalfeature)或Decimal(需要rust_decimalfeature)——不是f64。 - Postgres 的
TIMESTAMP和TIMESTAMPTZ对应不同 Rust 类型——PrimitiveDateTimevsOffsetDateTime。 - MySQL 的
TINYINT(1)默认对应i8,但常被语义当 bool——需要booldecode 支持。
debug 技巧:row.column(i).type_info().name() 看服务端声明的类型名。
陷阱 3:try_get("x") 返回 Option::None,但业务希望获取默认值
排查方向:
- 期望 "列不存在时用默认值"?——
try_get不能做这个;FromRow的#[sqlx(default)]attribute 可以(第 8 章)。 - 期望 "列为 NULL 时返回默认值"?——
unwrap_or_default()或unwrap_or(fallback)自己处理。 - 期望 "列为空字符串时用默认值"?——SQL 层面先
COALESCE。
陷阱 4:fetch 返回的行数和期望不一致
和 Row 本身无关,但经常和 Row 一起误诊。检查:
fetch_one期望恰好一行,多一行或少一行都 Error——用fetch_optional容忍少、用fetch_all容忍多。LIMIT 1服务端限制 vs.take(1)客户端——前者少传数据,后者可能多拉。- 分页——
OFFSET对大表慢,改用 keyset pagination。
这些陷阱多数都不是 sqlx 本身的问题——是 SQL / 协议 / 类型系统在"边界条件"上的表达差异。Row 作为"DB 到 Rust 的交付面",所有这些差异都最终落到 try_get 的返回上。学会读 Error 和用 columns() 探查实际情况,是成为 sqlx 熟手的必经之路。
7.14.2 relation_id 与编译期 nullability 推断
PgColumn::relation_id 和 relation_attribute_no 是 sqlx 的编译期 nullability 推断(第 11 章的主题之一)的数据源——这两个字段值得单独讲一下。
Postgres 的 Describe 消息不直接告诉你"这列是否可空"——Describe::Statement 只返回 Parameters 和 RowDescription,后者里每列有 table_oid 和 attribute_number 但不含 NOT NULL 约束。如果想知道可空性,要另查 pg_catalog.pg_attribute:
sql
SELECT attnotnull FROM pg_attribute
WHERE attrelid = $1 AND attnum = $2;sqlx-macros-core 在 query! 展开时拿到 PgColumn::relation_id(attrelid)和 relation_attribute_no(attnum)后,自动发这条查询批量取回每列的 notnull 标志。然后根据这个标志决定:
- notnull 且列出现在 SELECT——Rust 类型是
i32/String等。 - notnull 但列被 LEFT JOIN 后可能变 NULL(沿 join tree 推断)——Rust 类型升为
Option<i32>。 - 列本身 nullable——Rust 类型是
Option<T>。
这条自动 nullability 推断是 query! 宏的关键便利——你写 SELECT u.id, u.email, p.title FROM users u LEFT JOIN posts p ON ...,宏会正确判断 title: Option<String>(因为 LEFT JOIN 右侧行可能缺失),即便 posts.title 本身 NOT NULL。
表达式列(比如 SELECT COUNT(*)、SELECT a + b)的 relation_id 是 None——此时 sqlx 无法推断可空,默认标记为 nullable(即 Option<T>),用户可以用 SELECT COUNT(*) AS "count!: i64" 这种 override 语法强制非空。
MySQL 和 SQLite 没有同级的元信息——MySQL 的 FieldFlag::NOT_NULL 不可靠(对 JOIN 结果不正确),SQLite 完全没有。所以 sqlx 的编译期 nullability 推断只对 Postgres 真正有效。这是 Postgres 用户在 sqlx 上享有的独家优待,也是为什么 sqlx 用户里 Postgres 占比远超其它 DB 的技术原因。
7.15 Row trait 的版本演进
Row trait 在 sqlx 历史里演进过几次:
- 0.3 以前:
Row还是RawRow——只有try_get_raw,所有解码由用户手写。不同 DB 的 Row 类型不统一。 - 0.3:引入现代
Rowtrait(带 compatible 检查),成为所有驱动共用的接口。 - 0.5:
get和try_get的track_caller属性加入——让 panic 时错误信息指向用户的调用处而不是 sqlx 内部。 - 0.7:GAT 之后
try_get_raw的返回类型从HasValueRef<'_, DB>::ValueRef变成DB::ValueRef<'_>——签名清爽很多。 - 0.8(本书版本):错误类型进一步细化,
ColumnDecode的source支持BoxDynError装任何底层错误。
每一步都是"保持 trait 接口极简,把能力通过方法默认实现和错误类型扩展进去"。try_get_raw 作为必实现原语从 0.3 到 0.8 几乎没变——这是 trait 设计稳定性的标志。
7.16 三道判断题
Q1:为什么 Row::try_get 不能直接返回 &'r T 而必须返回 owned T?
A:Decode 契约决定的。Decode::decode 返回 Result<Self, Error>——Self 是具体类型,可以是 owned(String)也可以是 borrowed(&'r str)。try_get<T> 的 T 由用户声明——用户写 try_get::<String, _> 就拿 owned,写 try_get::<&str, _> 就拿 borrowed。trait 不强制方向,由 Decode impl 决定。
Q2:Row::columns 为什么返回 &[Column] 而不是 &[&Column] 或 Vec<Column>?
A:零拷贝访问。columns() 调用极频繁(每个 try_get 字符串索引都要一次),返回 &[Column] 让调用方零分配且能用索引访问。&[&Column] 多一层引用、Vec<Column> 要 clone——都比 &[Column] 昂贵。
Q3:为什么 PgRow 的 Debug 实现要解码值、而不是打印原始字节?
A:实用性优先。tracing::debug!("{row:?}") 的典型用户是在诊断问题——原始字节对应不上 SQL 查询的直观表达("12 34 56 78" vs "12345678")。解码后用户能立刻识别数据。代价是 Debug 慢——但 Debug 本来就不追求高性能(也没人对着 Debug 做基准测试)。这是"正确优先于快"的 API 设计。
7.17 Row 设计的整体价值
跳出方法细节,看 Row trait 在 sqlx 整体架构里的角色。
Row 承担了 "ORM-free 数据映射"的全部重量。sqlx 不做 ORM——它不自动把 Row 变成 User 对象。但 sqlx 提供了一个非常稳定的中间接口(Row + try_get),让用户可以:
- 手写映射——每字段
try_get然后构造 struct。显式、可控、零开销。 - 用 FromRow 派生——宏生成同样的 try_get 链,少几行代码。
- 用 query_as! 宏——编译期生成带类型的 try_get_unchecked 链,跳过运行时检查。
- 直接丢进其它库——传给 sea-orm、diesel(作为 raw row)、或者自定义的 ORM。
这条设计让 sqlx 成为"生态基础设施"——SeaORM 建在 sqlx 的 Row 上、各种业务特化的类型映射库都可以在 Row 之上造轮子、甚至 sqlx-pgfs 这种"Postgres 作为文件系统"的创意项目也借 Row 的稳定接口运行。Row 是一层零漏抽象:底下的字节 / 协议 / 格式全部封装,上层只需"给我一个索引我给你一个 ValueRef"。
对比 diesel 的 Row——diesel 的 Row 和 Queryable trait 绑定极紧,你必须通过 derive 或 manual impl 把 Row 变成具体的 struct;没有"直接访问列值"的稳定接口。这也是为什么 diesel 之上很难长出平行的扩展库——Row 不是"公共基座",是 Queryable trait 的内部辅助。sqlx 用相反的路线——Row 公开、稳定、简洁——换来整个生态的可组合性。
这条设计哲学值得记住:当你设计一个基础库时,问一句"我的核心抽象是不是稳定到让别人能在其上造同级库"——如果是,你就有生态;如果不是,你就是个工具。sqlx 的 Row 是前者的例子。
7.17.1 Row 与 tokio::spawn 的交互
Row 的 'static bound 让它能跨 async task 传递——这条能力在实战里非常好用。一个典型模式:
rust
let rows: Vec<PgRow> = query("SELECT ...").fetch_all(&pool).await?;
let handles: Vec<_> = rows.into_iter().map(|row| {
tokio::spawn(async move {
let id: i32 = row.try_get("id").unwrap();
let data = fetch_external(id).await.unwrap();
(id, data)
})
}).collect();
let results = futures::future::join_all(handles).await;每个 Row 被 move 进独立的 spawn task——这要求 Row 是 Send + 'static(tokio::spawn 的约束)。PgRow / MySqlRow / SqliteRow 都满足。
这条能力让"fetch 一批行后并发处理"成为零样板代码的常见模式。如果 Row 不是 'static——比如借用了 connection 的 buffer——你就必须先 decode 再 spawn,因为 Row 里的 &conn 不能跨 task。sqlx 用 Arc<Bytes> 和 Arc<Metadata> 把外部依赖全部 owned 化,换来了这条 "fetch + 并发处理" 的便利。
代价是每个 Row 占的内存比 "借用 connection" 版本略大——但占用的内存本来就是 Arc 的 refcount 不是字节本身,多一份 Arc 多 8 字节 pointer,相对 Row 里几十上百字节字节数据可以忽略。
7.18 本章小结
本章把 Row 和 Column 这一对"查询结果交付面"完整打开:
- Row trait 最小原语——只有
columns和try_get_raw必实现;其他 8 个方法(is_empty/len/column/try_column/get/get_unchecked/try_get/try_get_unchecked)都是默认实现。两原语 + 默认方法派生 是 sqlx trait 家族的一贯风格。 - try_get 的三层路径——raw → compatible → decode,每层失败有对应的
Error::ColumnDecode变体。NULL 和 UNKNOWN 自动跳过兼容检查。 - try_get_unchecked 跳过 compatible——供
query!宏生成代码使用,手写代码优先用 checked。 - ColumnIndex 统一索引—— usize 和 &str 共用同一方法签名,
&I的 blanket 让引用也合法。 - Column 的三字段极简——ordinal / name / type_info。PgColumn 额外有 relation_id / relation_attribute_no,用于 query! 的 nullability 推断。
- PgRow / MySqlRow 借用 Bytes——零拷贝访问 DataRow / protocol::Row 的底层字节。
- SqliteRow 独立拷贝——C API 约束下每行构造时拷贝所有 value;手动 unsafe impl Send/Sync。
- Row
'static+ ValueRef'r—— Row 能跨 await 传递(甚至tokio::spawn),ValueRef 借用 row。 - column_names O(1) lookup + Arc 共享——用 UStr 做 key、多行共享同一 HashMap。
- 生态基座——Row 是 sqlx 最稳定的公共接口,SeaORM 等二层库建在它之上。
下一章(第 8 章)我们看 FromRow 派生宏——它怎么把 Row 一次性解码成 Rust 结构体、#[sqlx(rename)] / #[sqlx(flatten)] / #[sqlx(default)] 这些 attribute 怎么展开、以及为什么 sqlx 的 FromRow 和 serde 的 Deserialize 在设计上是平行的但实现完全不同。从本章的基石往上走一层:Row 是"按索引取值"的底层接口,FromRow 是"按结构整体映射"的上层接口,两者协作完成 SQL 到 Rust 的结构化转换。