Skip to content

第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 后的真实约束

本章要点

  • Row trait(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
  • ColumnIndex traittry_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 + Syncsqlx-sqlite/src/row.rs:28-29)——手动保证线程安全,前提是"构造后不再触碰原 statement"。
  • Row: 'staticrow.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)?;  // 按位置

这里用户代码做了几件事:

  1. fetch_one 返回一个 Row。
  2. 对 Row 做四次 try_get——三次按列名、一次按位置。
  3. 每个 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_columncolumns()[index]lencolumns().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)而是 BytesArc<[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):把 Iusize 下标(通过 ColumnIndex::index),从 row 里取一个 ValueRef<'_>。失败返回 Error::ColumnNotFoundColumnIndexOutOfBounds

第二层 — 类型兼容检查:如果 value 非 NULL,取它的 type_info(),调用 T::compatible(&ty)。不兼容返回 Error::ColumnDecodemismatched_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::ColumnIndexOutOfBoundstry_get(99) 超出列数ColumnIndex::<usize> 返回
Error::ColumnDecode { mismatched }try_get::<i32>("name") 但 name 是 TEXTcompatible 检查失败
Error::ColumnDecode { source }try_get::<i32>("maybe_overflow") 字节是 i64 且超 i32 范围Decode::decode 返回 Err

所有 ColumnDecode 错误都带 index 字段——format!("{index:?}") 把用户传入的索引(整数或带引号的字符串)格式化进去。一个常见困扰是字符串索引在错误里有引号"\"foo\"" 而不是 "foo"——这是 {:?} 的 Debug 格式特性,用 Display 会少引号但少了"用户明显传了字符串"的信息。

实际排查 decode 错误的标准步骤:

  1. 看错误类型——ColumnNotFound 是列名拼错,OutOfBounds 是位置越界,ColumnDecode 是解码问题。
  2. ColumnDecode 里再看 source——如果是 mismatched_types,Rust 类型和列类型不对;如果是其它,decode 内部失败(字符串 UTF-8、整数溢出等)。
  3. row.column(index).type_info() 确认列实际类型——和你期望的对不对。
  4. row.columns().iter().map(|c| c.name()).collect::<Vec<_>>() 确认有哪些列——用来定位拼写错误。

7.3.1 try_get_unchecked 跳过兼容检查

对比 try_gettry_get_uncheckedrow.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 参数是统一索引类型ColumnIndextry_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 还有扩展字段——比如 PgColumnsqlx-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: Bytesbytes crate,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,
    })
}

步骤:

  1. 通过 ColumnIndex::indexI 转成 usize。
  2. 从 metadata.columns 取 PgColumn
  3. data.get(index)Option<&[u8]>——列的字节切片(或 None 表示 NULL)。
  4. 构造 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 的类型 DataRowsqlx-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::getdata_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_bodydata_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 QuerySELECT ... 裸字符串发过去)→ 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_rawsqlx-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::currentrow.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 的内部结构并排:

字段 / 行为PgRowMySqlRowSqliteRow
核心数据data: DataRow(Bytes 共享)row: protocol::Row(Bytes 共享)values: Box<[SqliteValue]>(独立拷贝)
列元数据metadata: Arc<PgStatementMetadata>columns: Arc<Vec<MySqlColumn>>columns: Arc<Vec<SqliteColumn>>
列名哈希metadata.column_namescolumn_names: Arc<HashMap>column_names: Arc<HashMap>
Binary/Text 格式format: PgValueFormatformat: MySqlValueFormat无(C API 直接按类型访问)
to_owned 代价O(1) Bytes cloneO(1) Bytes cloneO(n) memcpy(value 已在构造时拷贝)
fetch 时开销几乎零几乎零每行每列一次拷贝
unsafe impl Send/Sync不需要(结构自动 Send/Sync)不需要需要(因为内部含 *mut sqlite3_value
代码量~75 行~51 行~89 行

几条读表观察:

  1. PgRow / MySqlRow 几乎同构——借用协议返回的共享字节。差异只在字段命名和 MySQL 多一个独立 column_names。
  2. SqliteRow 形态本质不同——独立持有 value,构造时拷贝。这是 SQLite C API 的约束倒逼的设计。
  3. unsafe 只出现在 SQLite——因为它是唯一一家需要跨 step 保留数据、结构里含裸指针的驱动。
  4. 代码量 PgRow 反而不是最多——SqliteRow 更长是因为 current 方法要手动拷贝构造。

7.11 column_names 的哈希表:O(1) 名字查找

三家都用同一个数据结构表达"列名 → 下标" 查询:HashMap<UStr, usize>(或等价)。

UStrsqlx-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 集合——UStrArc<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 定义到三家具体实现完整打开:

  1. Row trait 的最小原语(§7.2)——columns + try_get_raw 是必实现的两个方法,其它 8 个方法都基于它们派生。双原语表达丰富接口是 Executor trait 和 Row trait 共同的设计手法。
  2. try_get 的三层路径(§7.3)——raw → compatible → decode,每一层有明确的失败分支。NULL 和 UNKNOWN 跳过兼容检查让 Option<T> 和未知类型友好处理。
  3. try_get_unchecked 跳过 compatible(§7.3.1)——给 query! 宏用,因为编译期已验证过。手写代码优先用 checked 版本。
  4. ColumnIndex 统一索引(§7.4)—— usize 和 &str 实现同一 trait,让 try_get(0)try_get("name") 同一签名。blanket impl 让 &I 也合法。
  5. Column trait 只有三方法(§7.5)——ordinal / name / type_info。每家驱动有扩展字段(PgColumn 的 relation_id / relation_attribute_no 用于 pg_attribute 查询)。
  6. PgRow 借用 DataRow 的 Bytes(§7.6)——data.storage 是 Arc<[u8]>,多个 PgValueRef 可以共享同一份字节。
  7. PgValueFormat Binary/Text 分发(§7.6.1)——整行统一 format,由协议模式决定。Extended Query → Binary,Simple Query → Text。
  8. MySqlRow 结构近似(§7.7)——字段组织略有不同但语义一致。
  9. SqliteRow 独立拷贝(§7.8)——C API 约束导致每行构造时拷贝所有 value,换来跨步存活能力。
  10. unsafe impl Send + Sync(§7.8.1)——SQLite 唯一手动标注的线程安全,依赖"不暴露 step" 的 API 不变量。
  11. Row 'static / ValueRef 'r(§7.9)——Row 独立生存可跨 await,ValueRef 借用 Row。decode 出 owned 类型跳出借用链,decode 出 &str 被 row 生命周期约束。
  12. column_names O(1) lookup(§7.11)——HashMap<UStr, usize> 通过 Arc 共享,prepare 时一次构建,每次 fetch_all 零重复。对 FromRow 解码性能至关重要。
  13. PgRow 的 Debug 实现解码值(§7.12)——{row:?} 输出可读字符串而不是原字节,靠 TypeChecking::fmt_value_debug 做类型感知格式化。

7.13.1 三家 fetch 的性能差异

把三家的单行 fetch 路径开销放一起对比:

阶段PgRowMySqlRowSqliteRow
读网络 / C APIasync read from TCP → Bytesasync read from TCP → Bytessqlite3_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 i324 字节 BigEndian read4 字节 LittleEndian read直接从 SqliteValue::Int 变体读
decode Stringmemcpy 字节 → Stringmemcpy 字节 → Stringclone 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 阶段

  1. 服务端返回 DataRow 消息,约 300 字节(假设 email 50 字节、json settings 100 字节、其他 150 字节)。
  2. sqlx 的 Postgres 驱动把字节装进 BytesArc<[u8]>,cheap-clone)。
  3. 按列解析出 values: Vec<Option<Range<u32>>>——7 个 Range,约 56 字节 overhead。
  4. 构造 PgRow { data: DataRow, format: Binary, metadata: Arc<...> }——总共 3 个指针大小 + DataRow 自身。

try_get 阶段(按 "id" 为例):

  1. "id".index(&row) → HashMap lookup → Ok(0)
  2. try_get_raw(0)PgValueRef { row: Some(&storage), value: Some(&bytes[0..4]), ... }
  3. Type::<i32, Postgres>::compatible(&INT4) → true。
  4. 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 列

排查方向:

  1. 列名是否区分大小写?Postgres 非引号列名被 lowercase 化,SELECT FOO 返回列名是 fooSELECT "Foo" 返回 Foo。MySQL/SQLite 略有差异。
  2. SELECT * 下列名是否有 schema 前缀?某些 JOIN 查询列名会是 table.foo
  3. 是否用了别名?SELECT foo AS bar——那就得 try_get("bar")

debug 技巧:先 row.columns().iter().map(|c| c.name()).collect::<Vec<_>>() 看实际列名。

陷阱 2:ColumnDecode { mismatched },列类型和 Rust 类型看起来一致

排查方向:

  1. Postgres 的 NUMERIC(10,2) 对应 Rust 的 BigDecimal(需要 bigdecimal feature)或 Decimal(需要 rust_decimal feature)——不是 f64
  2. Postgres 的 TIMESTAMPTIMESTAMPTZ 对应不同 Rust 类型——PrimitiveDateTime vs OffsetDateTime
  3. MySQL 的 TINYINT(1) 默认对应 i8,但常被语义当 bool——需要 bool decode 支持。

debug 技巧:row.column(i).type_info().name() 看服务端声明的类型名。

陷阱 3:try_get("x") 返回 Option::None,但业务希望获取默认值

排查方向:

  1. 期望 "列不存在时用默认值"?—— try_get 不能做这个;FromRow#[sqlx(default)] attribute 可以(第 8 章)。
  2. 期望 "列为 NULL 时返回默认值"?——unwrap_or_default()unwrap_or(fallback) 自己处理。
  3. 期望 "列为空字符串时用默认值"?——SQL 层面先 COALESCE

陷阱 4:fetch 返回的行数和期望不一致

和 Row 本身无关,但经常和 Row 一起误诊。检查:

  1. fetch_one 期望恰好一行,多一行或少一行都 Error——用 fetch_optional 容忍少、用 fetch_all 容忍多。
  2. LIMIT 1 服务端限制 vs .take(1) 客户端——前者少传数据,后者可能多拉。
  3. 分页——OFFSET 对大表慢,改用 keyset pagination。

这些陷阱多数都不是 sqlx 本身的问题——是 SQL / 协议 / 类型系统在"边界条件"上的表达差异。Row 作为"DB 到 Rust 的交付面",所有这些差异都最终落到 try_get 的返回上。学会读 Error 和用 columns() 探查实际情况,是成为 sqlx 熟手的必经之路。

7.14.2 relation_id 与编译期 nullability 推断

PgColumn::relation_idrelation_attribute_no 是 sqlx 的编译期 nullability 推断(第 11 章的主题之一)的数据源——这两个字段值得单独讲一下。

Postgres 的 Describe 消息不直接告诉你"这列是否可空"——Describe::Statement 只返回 ParametersRowDescription,后者里每列有 table_oidattribute_number 但不含 NOT NULL 约束。如果想知道可空性,要另查 pg_catalog.pg_attribute

sql
SELECT attnotnull FROM pg_attribute
WHERE attrelid = $1 AND attnum = $2;

sqlx-macros-corequery! 展开时拿到 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:引入现代 Row trait(带 compatible 检查),成为所有驱动共用的接口。
  • 0.5gettry_gettrack_caller 属性加入——让 panic 时错误信息指向用户的调用处而不是 sqlx 内部。
  • 0.7:GAT 之后 try_get_raw 的返回类型从 HasValueRef<'_, DB>::ValueRef 变成 DB::ValueRef<'_>——签名清爽很多。
  • 0.8(本书版本):错误类型进一步细化,ColumnDecodesource 支持 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 + 'statictokio::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 这一对"查询结果交付面"完整打开:

  1. Row trait 最小原语——只有 columnstry_get_raw 必实现;其他 8 个方法(is_empty/len/column/try_column/get/get_unchecked/try_get/try_get_unchecked)都是默认实现。两原语 + 默认方法派生 是 sqlx trait 家族的一贯风格。
  2. try_get 的三层路径——raw → compatible → decode,每层失败有对应的 Error::ColumnDecode 变体。NULL 和 UNKNOWN 自动跳过兼容检查。
  3. try_get_unchecked 跳过 compatible——供 query! 宏生成代码使用,手写代码优先用 checked。
  4. ColumnIndex 统一索引—— usize 和 &str 共用同一方法签名,&I 的 blanket 让引用也合法。
  5. Column 的三字段极简——ordinal / name / type_info。PgColumn 额外有 relation_id / relation_attribute_no,用于 query! 的 nullability 推断。
  6. PgRow / MySqlRow 借用 Bytes——零拷贝访问 DataRow / protocol::Row 的底层字节。
  7. SqliteRow 独立拷贝——C API 约束下每行构造时拷贝所有 value;手动 unsafe impl Send/Sync。
  8. Row 'static + ValueRef 'r—— Row 能跨 await 传递(甚至 tokio::spawn),ValueRef 借用 row。
  9. column_names O(1) lookup + Arc 共享——用 UStr 做 key、多行共享同一 HashMap。
  10. 生态基座——Row 是 sqlx 最稳定的公共接口,SeaORM 等二层库建在它之上。

下一章(第 8 章)我们看 FromRow 派生宏——它怎么把 Row 一次性解码成 Rust 结构体、#[sqlx(rename)] / #[sqlx(flatten)] / #[sqlx(default)] 这些 attribute 怎么展开、以及为什么 sqlx 的 FromRow 和 serde 的 Deserialize 在设计上是平行的但实现完全不同。从本章的基石往上走一层:Row 是"按索引取值"的底层接口,FromRow 是"按结构整体映射"的上层接口,两者协作完成 SQL 到 Rust 的结构化转换。

基于 VitePress 构建