SQLx 源码深度解析
第3章 Database trait 家族:用 GAT 收束异构驱动
Databasetrait 把驱动特有的 11 种类型全部声明成关联类型,让上层 API 只需要写DB: Database一个约束。其中四个是 GAT——为此 sqlx 一直等到了 Rust 1.65。
本章要点
Databasetrait(sqlx-core/src/database.rs:72)是整个 sqlx 类型体系的收束点——把驱动特有的 11 种类型(7 个普通关联类型 + 4 个 GAT)全部声明为关联类型,再加两个 const(NAME/URL_SCHEMES),让上层 API 只需要DB: Database一个泛型参数。- 其中四个是泛型关联类型(GAT):
type ValueRef<'r>、type Arguments<'q>、type ArgumentBuffer<'q>、type Statement<'q>——它们带生命周期参数。GAT 在 Rust 1.65(2022-11)才稳定,sqlx 直到 0.8.0(2024-07,#2973)才完成这次迁移。 - 0.7 及之前,sqlx 用
HasValueRef<'r>这样的"带生命周期的 helper trait"绕过——这种 workaround 让Database的 super-trait 挂上三条for<'r>高阶约束,每处用到这些类型都得写<DB as HasArguments<'q>>::Arguments这样的完全限定路径。 NAME和URL_SCHEMES两个关联常量让 Any 驱动能在运行时按 URL scheme 分发(见第 2 章)——这不是显眼的特性,但决定了"同一个 AnyConnection 能连所有 DB"这件事能不能做到。HasStatementCache是一个无方法的 marker trait——它把"客户端预处理语句缓存"这项能力锁进类型:上层 API(Query::persistent、Connection::cached_statements_size、clear_cached_statements)都写着where DB: HasStatementCache。0.8.6 的三家 in-tree 驱动(外加Any)都实现了它,所以这道门槛是留给第三方驱动的。- 对照 Postgres / MySQL / SQLite 三家的
impl Database,你会发现每一行的具体类型都不同但结构完全一样——这就是 trait 家族设计要达成的"等价替换"形态。
3.1 问题引入:一个 DB 要扛 11 个关联类型
第 1 章画过这行代码:
let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", id)
.fetch_one(&pool)
.await?;
以及它展开后生成的那个查询对象(简化):
::sqlx::__query_with_result::<::sqlx::Postgres, _>(...)
.try_map(|row: ::sqlx::postgres::PgRow| { ... })
注意第二行里的两个类型——Postgres 和 PgRow。如果你把 Postgres 换成 MySql,那第二行的 PgRow 必须同步换成 MySqlRow——这是 sqlx-core 的 API 在声明时就约束好的。sqlx::query_as 不知道具体是哪家数据库、不知道具体的 Row 类型长什么样——它只知道"你给我一个 DB: Database,我从 DB::Row 里解码"。
问题来了:这个"只知道 DB"的设计要求 Row、Column、Value、Arguments 等所有具体类型都能从 DB 这一个泛型参数推出来。Rust 里唯一能做到这件事的机制是关联类型(associated type):
pub trait Database {
type Row;
type Column;
type Value;
// ...
}
fn fetch_one<DB: Database>(...) -> <DB as Database>::Row { ... }
但 sqlx 的 11 个关联类型里有四个带生命周期参数:
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
type ArgumentBuffer<'q>;
type Statement<'q>: Statement<'q, Database = Self>;
type ValueRef<'r> 不是普通关联类型——它是泛型关联类型(GAT)。GAT 在 Rust 1.65(2022 年 11 月)才稳定,而 sqlx 直到 0.8.0(2024 年 7 月,#2973)才把 trait 家族改写成 GAT 形态。0.7 及之前的 sqlx 用了一套非常笨拙的绕法——本章第 3.3 节会详细看那段历史。
这一章的任务,就是把 Database trait 的每一个关联类型、每一个 trait 边界拆开,解释它们为什么必须这样写、GAT 消除了哪些痛苦、HasStatementCache 这种"无方法标记 trait"怎么当作类型系统里的能力开关。读完本章,你对 DB: Database 这个约束在上层 API 的每一处出现都能立刻在脑子里展开成完整的关联类型集合。
3.2 Database trait 的全貌
sqlx-core/src/database.rs:72-111 的原文(去掉文档注释,本章后续讨论都以此为锚):
pub trait Database: 'static + Sized + Send + Debug {
type Connection: Connection<Database = Self>;
type TransactionManager: TransactionManager<Database = Self>;
type Row: Row<Database = Self>;
type QueryResult: 'static + Sized + Send + Sync + Default + Extend<Self::QueryResult>;
type Column: Column<Database = Self>;
type TypeInfo: TypeInfo;
type Value: Value<Database = Self> + 'static;
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
type ArgumentBuffer<'q>;
type Statement<'q>: Statement<'q, Database = Self>;
const NAME: &'static str;
const URL_SCHEMES: &'static [&'static str];
}
一共十一个关联类型 + 两个关联常量 + 四条 super-trait bound('static + Sized + Send + Debug)。把这十一个类型按功能分成四组:
graph TD
DB[trait Database]
subgraph 生命周期载体
C1[Connection]
C2[TransactionManager]
end
subgraph 读取链
R1[Row]
R2[Column]
R3[Value]
R4["ValueRef<'r>"]
R5[QueryResult]
end
subgraph 写入链
W1["Arguments<'q>"]
W2["ArgumentBuffer<'q>"]
W3["Statement<'q>"]
end
subgraph 元数据
M1[TypeInfo]
end
DB --> C1 & C2
DB --> R1 & R2 & R3 & R4 & R5
DB --> W1 & W2 & W3
DB --> M1
后文的 3.4–3.7 节按这四组依次展开。
先看几条 super-trait bound:
'static:Database实现者(Postgres / MySql / Sqlite)都是零大小 unit struct——pub struct Postgres;——所以没有非 'static 引用。但'static不是自动推出来的,它是给下游 API 用的:"凡是DB: Database,我就能把DB放进Arc里、跨线程传递、跑在tokio::spawn里"。没有它,上层所有where 'static的泛型边界都会断掉。Sized:意味着DB不能是dyn Database形式——这和 Any 驱动形成对比:Any 的运行时多态放在AnyConnection内部的Box<dyn AnyConnectionBackend>(sqlx-core/src/any/connection/mod.rs:26-28)里,实现Database的仍是一个Sized的 unit structAny。Send:必须能跨线程——因为Pool<DB>的PoolInner会被多个 tokio task 共享。Debug:让DB这个类型 token 本身可以用{:?}打印——三家的 unit struct 都是#[derive(Debug)]。
这四条 bound 加起来把 Database 实现者钉死为"一个零开销的类型 token"——它不持有任何状态,只是类型系统里的一个标识,告诉 Executor::fetch 等泛型方法应该去找哪一套具体的 Row / Column / Arguments 实现。
3.2.1 对照 diesel 的 Backend trait
Rust 生态里做"驱动抽象"的另一条路线是 diesel。把 diesel 2.x 的 Backend trait 和 sqlx 的 Database 放在一起比,能看出两种哲学的差异:
// diesel 2.x 的 Backend trait(简化)
pub trait Backend
where
Self: Sized + SqlDialect + TypeMetadata,
Self: HasSqlType<sql_types::Integer>, // 原文共 10 条 HasSqlType<…>
{
type QueryBuilder: QueryBuilder<Self>;
type RawValue<'a>;
type BindCollector<'a>: BindCollector<'a, Self> + 'a;
}
// 方言特性挂在 Backend 的 super-trait SqlDialect 上
pub trait SqlDialect: private::TrustedBackend {
type ReturningClause;
type OnConflictClause;
type InsertWithDefaultKeyword;
type BatchInsertSupport;
// ...
}
几条对比:
- diesel 的关联类型数量更多(
Backend上 3 个:QueryBuilder/RawValue/BindCollector,见diesel-2.3.9/diesel/src/backend.rs:97-112;SqlDialect上另有 15 个,见diesel-2.3.9/diesel/src/backend.rs:142-343),但语义完全不同——diesel 的关联类型描述"方言特性"(是否支持 RETURNING、ON CONFLICT),sqlx 的关联类型描述"数据形态"(Row、Column、Value)。 - diesel 的
Row不挂在Backend上——diesel 也有Rowtrait(diesel-2.3.9/diesel/src/row.rs:40,配一个Field,diesel-2.3.9/diesel/src/row.rs:99),但它是LoadConnection的关联类型(diesel-2.3.9/diesel/src/connection/mod.rs:437)而不是Backend的;而且 super-trait 里带private::RowSealed,外部 crate 不开i-implement-a-third-party-backend-and-opt-into-breaking-changesfeature 就实现不了。它服务的是FromSqlRow的静态展开,不是像 sqlx 的Row那样当公共基座。Column则确实没有等价物——列名只能从Field::field_name()拿。 - diesel 有 sqlx 没有的
QueryBuilder——这是 diesel DSL 的 SQL 字符串生成器。sqlx 不需要,因为用户写的 SQL 就是原始字符串,不经过构造器翻译。
两种设计的本质差异可以浓缩成一句话:diesel 的 Backend trait 描述的是"如何从 Rust 表达式生成 SQL",sqlx 的 Database trait 描述的是"如何把 SQL 字节流和连接池接入 Rust 的泛型系统"。前者是"出"的问题,后者是"入"的问题——两种工具包的方向完全不同。
这个对照也解释了两边"运行时选库"能力的差别——sqlx 抽象的"数据形态"在三家 DB 下大致等价,运行时擦除一层并不太失真,所以有了 Any 驱动;diesel 抽象的"方言特性"在三家 DB 下差异巨大(Postgres 原生支持 RETURNING,SQLite 3.35 起才有、diesel 还要开 returning_clauses_for_sqlite_3_35 feature,MySQL 则只有 last_insert_id),擦除不干净。diesel 2.1.0 起也提供了 #[derive(MultiConnection)],但它的文档明说派生出的连接"只能覆盖所有内层连接的公共子集"——某个后端不支持 RETURNING,整个多后端连接就都不支持,SQL 类型也只内置支持九种(diesel-2.3.9/diesel_derives/src/lib.rs 中 MultiConnection 的 Limitations 一节)。
3.3 为什么是 GAT:没有泛型关联类型之前的痛苦
要解释 GAT 的必要性,先看没有 GAT 时 sqlx 是怎么写的。
0.7 及之前(以 sqlx-core 0.7.0 为代表)的 Database trait 大致长这样:
// 0.7 时代的简化版
pub trait Database:
for<'r> HasValueRef<'r, Database = Self>
+ for<'q> HasArguments<'q, Database = Self>
+ for<'q> HasStatement<'q, Database = Self>
{
type Connection: Connection<Database = Self>;
type Row: Row<Database = Self>;
// ...
}
pub trait HasValueRef<'r> {
type Database: Database;
type ValueRef: ValueRef<'r, Database = Self::Database>;
}
pub trait HasArguments<'q> {
type Database: Database;
type Arguments: Arguments<'q, Database = Self::Database>;
type ArgumentBuffer;
}
pub trait HasStatement<'q> {
type Database: Database;
type Statement: Statement<'q, Database = Self::Database>;
}
注意这三个 "Has*" trait:每个都带一个生命周期参数,把原本应该是 GAT 的类型变成"HKT 仿真"——通过 for<'r> 全称量化 bound 给 Database trait 附加约束。
这种写法的代价是,每处用到这些类型都得写完全限定路径。DB::Arguments 这种简写没有地方放生命周期参数,只能写成 <DB as HasArguments<'q>>::Arguments。0.7.0 源码里随处可见:
// sqlx-core 0.7.0,src/encode.rs:34
fn encode_by_ref(&self, buf: &mut <DB as HasArguments<'q>>::ArgumentBuffer) -> IsNull;
// sqlx-core 0.7.0,src/query.rs:430
pub fn query<DB>(sql: &str) -> Query<'_, DB, <DB as HasArguments<'_>>::Arguments>
Database 自己也得在 super-trait 里挂三条 for<'r> 高阶约束(higher-ranked trait bound,HRTB),每个驱动还要为三个 Has* trait 各写一个 impl。
GAT 稳定之后,这一切变成:
pub trait Database: 'static + Sized + Send + Debug {
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
// ...
}
上层 API 可以直接写 fn execute<'q, DB: Database>(args: DB::Arguments<'q>)——生命周期参数直接跟在关联类型后面,不再需要 HasArguments 这种中间层。上面两处在 0.8.6 里分别变成 buf: &mut <DB as Database>::ArgumentBuffer<'q>(sqlx-core/src/encode.rs:40-43)和 Query<'_, DB, <DB as Database>::Arguments<'_>>(sqlx-core/src/query.rs:655)。
0.8.6 的 Query 类型(sqlx-core/src/query.rs:17-22)也比 0.7 干净:
pub struct Query<'q, DB: Database, A> {
pub(crate) statement: Either<&'q str, &'q DB::Statement<'q>>,
pub(crate) arguments: Option<Result<A, BoxDynError>>,
pub(crate) database: PhantomData<DB>,
pub(crate) persistent: bool,
}
&'q DB::Statement<'q> 这个类型在 0.7 时代要写成 &'q <DB as HasStatement<'q>>::Statement,GAT 之后直接写在字段位置——这是 GAT 对 sqlx 的最大价值。
但要注意 GAT 没有消掉的部分:在用户自己的泛型代码里,高阶约束照样要写。一个对任意 DB 通用、从 &Pool<DB> 查一个 i64 的函数,在 0.8.6 下要这样声明才能编译(sqlx@=0.8.6 实测,少任何一条都报 E0277):
use sqlx::{ColumnIndex, Database, Decode, Executor, IntoArguments, Pool, Row, Type};
async fn count<DB: Database>(pool: &Pool<DB>) -> Result<i64, sqlx::Error>
where
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
for<'q> DB::Arguments<'q>: IntoArguments<'q, DB>,
i64: for<'r> Decode<'r, DB> + Type<DB>,
usize: ColumnIndex<DB::Row>,
{
let row = sqlx::query("SELECT COUNT(*)").fetch_one(pool).await?;
row.try_get(0)
}
GAT 改变的是这些类型怎么写(DB::Arguments<'q> 取代 <DB as HasArguments<'q>>::Arguments),而不是泛型代码要证明的约束有多少。
这次迁移的动因,在 0.7.0 的 CHANGELOG 里就已经预告过了(就写在那条讲 Executor impl 被删除的条目末尾):
We're expecting to do another major refactor of traits to incorporate generic associated types (GAT). This will mean another major release of SQLx but ideally most API usage will not need to change significantly, if at all.
真正落地是在 0.8.0 的 Breaking 列表第二条:#2973 "Generic Associated Types in Database, replacing HasValueRef, HasArguments, HasStatement"。按 sqlx 自己在 FAQ 里写的 MSRV 政策——"MSRV 是当前发布周期开始时的次新稳定版,并在整个 0.x 周期内保持不变"——0.8.x 的 MSRV 是 1.78.0(0.8.0 发布时最新稳定版是 1.79.0),远高于 GAT 所需的 1.65,所以这次迁移本身并没有额外的 MSRV 代价。
3.3.1 GAT 带来的新代价
GAT 不是免费午餐。sqlx 的实际使用里有两类新问题:
问题 1:lifetime inference 失败。GAT 下 DB::Arguments<'q> 的 'q 必须由编译器从上下文推导。在一些复杂的异步调用链里,编译器会推不出来,报出 "implementation is not general enough" 之类的错误——用户需要手动标注 'q,或者用 type alias 把 GAT 投影固化。
问题 2:dyn 兼容性(dyn compatibility,旧称 object safety)上又多压了一条。先把归因说准:dyn Database 本来就写不出来,根因不是 GAT 而是 Sized——sqlx-core/src/database.rs:72 的 trait 头是 pub trait Database: 'static + Sized + Send + Debug,§3.2 已经列过这条 super-trait bound,它在 GAT 化之前就把任何 unsized 的 dyn Database 排除了。GAT 是在此之上再加的一条:带 GAT 的 trait 本身不是 dyn 兼容的,所以就算哪天摘掉 Sized,dyn Database 依然不成立。两条叠加,才是 Any 驱动必须另起炉灶写 AnyConnectionBackend trait(所有方法用 BoxFuture 返回,无 GAT)的原因。第 2.6 节讨论的"运行时多态叛徒"在类型系统层面的另一种解释就是:Database 从一开始就被钉成"编译期类型 token",Any 必须另起炉灶。
这两个代价对 sqlx 的影响都是可控的——第一个问题靠文档和错误信息缓解,第二个问题通过 Any 驱动的独立 trait 绕开。GAT 总体是净收益。
3.3.2 如何读懂 sqlx 的 trait 边界编译错
GAT + trait 家族的一个直接副作用:编译错信息会变长。新手常常遇到这种错:
error[E0277]: the trait bound `&mut Transaction<'_, Postgres>: Executor<'_>` is not satisfied
--> src/main.rs:3:39
|
3 | sqlx::query("SELECT 1").fetch_one(&mut tx).await?;
| --------- ^^^^^^^ the trait `Executor<'_>` is not implemented for `&mut Transaction<'_, Postgres>`
| |
| required by a bound introduced by this call
|
help: the following other types implement trait `Executor<'c>`
--> .../sqlx-postgres-0.8.6/src/connection/executor.rs:380:1
|
380 | impl<'c> Executor<'c> for &'c mut PgConnection {
| ...
::: .../sqlx-postgres-0.8.6/src/listener.rs:388:1
|
388 | impl<'c> Executor<'c> for &'c mut PgListener {
| ...
::: .../sqlx-core-0.8.6/src/pool/executor.rs:12:1
|
12 | / impl<'p, DB: Database> Executor<'p> for &'_ Pool<DB>
13 | | where
14 | | for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
| |________________________________________________________________^ `&Pool<DB>` implements `Executor<'p>`
(sqlx@=0.8.6 开 postgres feature 实测输出,路径与部分行已省略。)
这条错误在第 1 章"让步二·附"提到过——0.7 之后 &mut Transaction 不再自动是 Executor。但错误信息里藏着第二条线索:help 列出的 &Pool<DB> 那条实现,where 子句是用关联类型投影 DB::Connection 描述的。读懂它需要先读懂 Database trait 的关联类型投影语法。
三个常见 sqlx 错误信息模式:
| 错误模式 | 含义 | 解决 |
|---|---|---|
for<'c> &'c mut <DB as Database>::Connection: Executor<'c> is not satisfied |
泛型函数里把 &Pool<DB> 当 Executor 用 |
在 where 里补上这条 HRTB |
implementation is not general enough |
GAT 生命周期推断失败 | 手动标注 'q |
<X as Database>::ValueRef<'_> doesn't implement ... |
ValueRef 的 GAT 投影缺少某个 trait 实现 | 检查 Decode<DB> 的覆盖 |
读 sqlx 错误信息的第一步永远是把 <X as Database>::Y 翻译成具体类型——<Postgres as Database>::Row 就是 PgRow、<Postgres as Database>::ValueRef<'_> 就是 PgValueRef<'_>。一旦翻译过来,问题就从抽象 trait bound 变成具体类型缺 impl,修起来容易得多。
3.4 读取链:Row / Column / Value / ValueRef / QueryResult
这是 Database trait 里最大的一组关联类型——五个。它们共同描述"从数据库读回一行,怎么拆开、怎么解码"。
3.4.1 Row:一行数据的抽象
sqlx-core/src/row.rs:14-16 的 Row trait:
pub trait Row: Unpin + Send + Sync + 'static {
type Database: Database<Row = Self>;
// ...
}
Unpin + Send + Sync + 'static 四条约束——和 Database 的四条不同,多了 Unpin 与 Sync。每一条都有直接用途:
Unpin:允许 Row 在异步流里自由移动。Send + Sync:Row 经常在 tokio task 之间流转。'static:Row 不能借用外部数据。注意这和ValueRef<'r>形成对比——Row 持有值,ValueRef 持有引用。
Row::Database = Self:这是自反关联类型约束。它说"我这个 Row 的 Database 关联类型,指回去得是能产生我的那个 Database"。展开成具体:PgRow::Database = Postgres,而 Postgres::Row = PgRow——两者形成闭环。这条约束让编译器能在 fn take<R: Row>(r: R) -> <R::Database as Database>::Row 这种泛型代码里做等价替换。
Row trait 的方法包括 try_get<T>(index) / try_get_raw(index) / columns() / try_column(index) / len()——全部都返回泛型或关联类型的值。具体实现由驱动自己给出:PgRow 持有 Postgres 线路协议解析后的 DataRow 字节,MySqlRow 持有 MySQL 的字段值数组,SqliteRow 持有一组 SqliteValue——取行时每列用 sqlite3_value_dup 复制出来的独立值(sqlx-sqlite/src/value.rs:200-205、sqlx-sqlite/src/row.rs:15-19)。三家的存储格式完全不同,但都满足 Row trait。
3.4.2 Column:列元数据
sqlx-core/src/column.rs:6-23:
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;
}
Column 是列元数据——它不持有数据值,只持有"这一列叫什么、类型是什么、顺序是多少"。和 Row 的关系是:"每一 Row 里有一组 Column"——Row::columns() 返回 &[DB::Column]。
一个很重要的观察:Column 的 type_info 返回的是 &DB::TypeInfo 而不是 DB::TypeInfo 的克隆。这是因为列类型信息在一个预处理语句生命周期内是不变的(PostgreSQL 的 RowDescription 在 prepare 时的 Describe 阶段就被服务端返回了),后续每次读行都引用同一份。把 TypeInfo 设计成"按引用传"避免了每行一次的克隆开销。
3.4.3 Value 与 ValueRef:数据持有权的分裂
这一对是整个读取链最微妙的设计。
sqlx-core/src/value.rs:9:
pub trait Value {
type Database: Database<Value = Self>;
fn as_ref(&self) -> <Self::Database as Database>::ValueRef<'_>;
fn type_info(&self) -> Cow<'_, <Self::Database as Database>::TypeInfo>;
fn is_null(&self) -> bool;
// decode / try_decode 等方法
}
sqlx-core/src/value.rs:99:
pub trait ValueRef<'r>: Sized {
type Database: Database;
fn to_owned(&self) -> <Self::Database as Database>::Value;
fn type_info(&self) -> Cow<'_, <Self::Database as Database>::TypeInfo>;
fn is_null(&self) -> bool;
}
Value 是拥有所有权的值——它独立存在,不借用 Row。ValueRef<'r> 是借用的值——生命周期 'r 挂到了 Row 上,意味着"只要 Row 还活着,我这个 ValueRef 就有效"。
为什么要两个? 想象 row.try_get::<i32, _>(0) 这个调用的流程:
try_get需要从第 0 列拿到一个原始值。- 原始值在 PostgreSQL 里可能只是
PgRow那个data: DataRow缓冲区里的几个字节(sqlx-postgres/src/row.rs:14-18,DataRow是协议层消息,见sqlx-postgres/src/message/data_row.rs:10)——拷贝出来当 owned value 是浪费。 - 所以
Row::try_get_raw先返回一个ValueRef<'r>——只是一个指针 + 长度,生命周期绑到 row 上。 - 真正 decode 成
i32的时候,Decode::decode接收的是ValueRef,不是Value。
换句话说,ValueRef 是 decode 路径上的默认形态,Value 只在需要"把未解码的原始值抽出来、脱离 Row 单独保存"时用——对 row.try_get_raw(i)? 调 ValueRef::to_owned() 得到 Value,之后再 Value::try_decode。常规的 try_get、query_scalar! 都直接在 ValueRef 上 decode(query_scalar! 走的是 (O,) 的 FromRow,即 row.try_get(0),sqlx-core/src/from_row.rs:336),不经过 Value。
ValueRef::to_owned 的文档注释(sqlx-core/src/value.rs:104-105)说:Postgres 和 MySQL 下只是一次引用计数递增、O(1),SQLite 下是一次拷贝。对照 0.8.6 的实现:
- Postgres / MySQL:与注释一致——底层 buffer 是
bytes::Bytes,to_owned用row.slice_ref(value)切出一段共享视图,只增引用计数(sqlx-postgres/src/value.rs:101-115、sqlx-mysql/src/value.rs:76-86)。 - SQLite:拷贝其实提前发生了。
SqliteRow在取行时就对每一列sqlite3_value_dup一份,包进Arc<ValueHandle>(第 7 章会细看这段代码,sqlx-sqlite/src/row.rs:31-45);之后从 row 拿到的SqliteValueRef调to_owned,只是克隆这个Arc(sqlx-sqlite/src/value.rs:76-78)。只有 preupdate hook 给出的借用句柄,才会在to_owned时再 dup 一次。
所以在 0.8.6 里,对 row.try_get_raw(0)? 调 to_owned() 在三家都很便宜;SQLite 的复制成本付在了取行那一步,而不是 to_owned 这一步。
3.4.4 一次 row.try_get::<i32, _>(0) 的类型流
把前面几个关联类型串起来,看 let id: i32 = row.try_get(0)?; 这一行背后的类型动线:
sequenceDiagram
participant U as 用户代码
participant Row as PgRow<br/>(DB::Row)
participant VR as PgValueRef<'r><br/>(DB::ValueRef<'_>)
participant TI as PgTypeInfo<br/>(DB::TypeInfo)
participant Dec as Decode::<i32, Postgres>
participant T as Type::<i32, Postgres>
participant Ret as i32
U->>Row: try_get::<i32, _>(0)
Row->>Row: try_get_raw(0)
Row-->>VR: ValueRef<'_>
Row->>T: compatible(&value.type_info())
T->>TI: type_compatible(列的 TypeInfo)
TI-->>T: INT4 == INT4
T-->>Row: true (i32 ↔ INT4)
Row->>Dec: decode(value)
Dec->>VR: 读取字节
Dec-->>Ret: i32 或 Error
Ret-->>U: Ok(123)
关键节点:
try_get_raw先返回ValueRef<'_>——'_生命周期绑到&self(row)。编译器在这一步强制"ValueRef 不能活过 row"。Type::<i32, Postgres>::compatible做类型兼容检查——i32的type_info()是PgTypeInfo::INT4(sqlx-postgres/src/types/int.rs:108-112),默认的compatible用type_compatible与列的 TypeInfo 做相等比较,检查通过才进入 decode。这步失败会返回Error::ColumnDecode(sqlx-core/src/row.rs:111-131),其 source 由mismatched_types(sqlx-core/src/error.rs:174-183)拼出,形如 "mismatched types; Rust type i32 (as SQL type INT4) is not compatible with SQL type ..."。Decode::<i32, Postgres>::decode从ValueRef读字节——Postgres 的i32::decode(sqlx-postgres/src/types/int.rs:128-132)先调int_decode解成i64,再try_into成i32;int_decode(sqlx-postgres/src/types/int.rs:10-33)对二进制格式按大端BigEndian::read_int读,对文本格式按字符串解析。- 整条路径零 owned 分配——
ValueRef借用 row 的 buffer,i32 是栈上 4 字节。对比如果你写let id: String = row.try_get(0)?,Decode 实现会value.as_str()?.to_owned()(sqlx-postgres/src/types/str.rs:144-148)真做一次堆分配。
这张图说明了常说的"零拷贝解码"——不是指所有类型都没有拷贝,而是指只有必要时才拷贝(值类型零、引用类型 / 字符串按需)。这条路径的精妙处是它全部发生在 &self 的借用期内,不需要跨 await——这是 ValueRef 'r 生命周期的真正价值。
3.4.5 QueryResult:写操作的结果
type QueryResult: 'static + Sized + Send + Sync + Default + Extend<Self::QueryResult>;
这是唯一没有独立 trait 的关联类型——它是一个值类型(不是 trait bound)。PgQueryResult 是一个 struct 只带 rows_affected: u64;MySqlQueryResult 是 rows_affected + last_insert_id: u64;SqliteQueryResult 干脆换了字段名,是 changes: u64 + last_insert_rowid: i64(rows_affected() 这个方法返回的就是 changes)。没有公共 trait,只有公共的值蓝图。
关键是 Extend<Self::QueryResult> 这条 bound(连同 Default):PgQueryResult 实现 Extend<PgQueryResult> 意味着多个 QueryResult 可以"累加"成一个——Executor::execute 的默认实现就是把 execute_many 产出的逐条结果 try_collect 成一个(sqlx-core/src/executor.rs:37-46)。翻 sqlx-postgres/src/query_result.rs 能看到 impl Extend<PgQueryResult> for PgQueryResult 的实现——只是把 rows_affected 加起来。
3.5 写入链:Arguments / ArgumentBuffer / Statement
读取链完了,看写入链。
3.5.1 Arguments:参数集合
sqlx-core/src/arguments.rs:12:
pub trait Arguments<'q>: Send + Sized + Default {
type Database: Database;
fn reserve(&mut self, additional: usize, size: usize);
fn add<T>(&mut self, value: T) -> Result<(), BoxDynError>
where T: 'q + Encode<'q, Self::Database> + Type<Self::Database>;
fn len(&self) -> usize;
fn format_placeholder<W: Write>(&self, writer: &mut W) -> fmt::Result { ... }
}
Arguments<'q> 是一组待发送的参数。'q 生命周期挂在参数的潜在借用上——如果你 .bind(&some_string),那 some_string 的生命周期至少活到查询执行完,这条 bound 由 'q 守护。
注意 add 方法的 where T: 'q + Encode<'q, Self::Database> + Type<Self::Database> ——把 Encode 和 Type 绑在一起。这意味着你加一个参数时,系统同时要求"你能 encode 成字节"且"你声明了对应的数据库类型"。我们会在第 5 章详细看 Encode 和 Type 这一对。
format_placeholder 的默认实现是 ?——MySQL 和 SQLite 都用 ?,Postgres 要自定义为 $1 / $2 / ...(按参数位置带编号)。这条看似无关紧要的 API 决定了"同一份 Rust 代码能不能跨 DB 复用"——第 10 章 QueryBuilder 就靠它。
3.5.2 ArgumentBuffer:编码缓冲区
type ArgumentBuffer<'q>; 是唯一没有 trait bound 的关联类型——它没有 impl X<'q> 之类的约束。这意味着每家驱动可以完全自由地选择缓冲区形态:
-
Postgres:
PgArgumentBuffer(sqlx-postgres/src/arguments.rs:26-47)——一个带patches和type_holes的复杂结构:pub struct PgArgumentBuffer { buffer: Vec<u8>, count: usize, patches: Vec<Patch>, // 延迟补写(比如参数最终解析为 JSON 还是 JSONB,决定首字节) type_holes: Vec<(usize, HoleKind)>, // OID 还没解析时先留洞,之后回填 } -
MySQL:
type ArgumentBuffer<'q> = Vec<u8>;—— 一个裸字节缓冲。没有复杂结构,每个参数的类型另记在MySqlArguments的types字段里(sqlx-mysql/src/arguments.rs:10-14),缓冲区只管按顺序拼字节。 -
SQLite:
type ArgumentBuffer<'q> = Vec<SqliteArgumentValue<'q>>;—— 一个带生命周期的值向量(SQLite 的参数不是按字节编码的,是按类型 tag + 值的变体存的)。
三家的 ArgumentBuffer 形态截然不同——这就是为什么没有共同 trait 约束。但这个关联类型仍然存在于 Database trait 里,因为 DB::ArgumentBuffer<'q> 被 Encode 实现引用:Encode::encode_by_ref(&self, buf: &mut <DB as Database>::ArgumentBuffer<'q>)——每个 Encode 实现需要知道"我应该往哪种容器里写"。
3.5.3 Statement:预处理语句的抽象
sqlx-core/src/statement.rs:19:
pub trait Statement<'q>: Send + Sync {
type Database: Database;
fn to_owned(&self) -> <Self::Database as Database>::Statement<'static>;
fn sql(&self) -> &str;
fn parameters(&self) -> Option<Either<&[<Self::Database as Database>::TypeInfo], usize>>;
fn columns(&self) -> &[<Self::Database as Database>::Column];
// ...
}
Statement 代表已经被驱动 prepare 过的 SQL 语句——它缓存了 SQL 文本、参数类型、列元数据。Executor::prepare_with 返回的就是 DB::Statement<'q>。
parameters 返回 Option<Either<&[TypeInfo], usize>> 这个怪异的类型,是因为不同数据库对"参数类型"的支持水平不同:
- Postgres:完整的参数类型——服务端在
Parse响应里返回每个$1、$2的 OID。所以是Either::Left(&[PgTypeInfo])。 - SQLite:只有参数个数——
sqlite3_bind_parameter_count(stmt)告诉你有几个?,但不告诉你每个?期望什么类型。所以是Either::Right(usize)。 - MySQL:0.8.6 的 MySQL 驱动也只返回参数个数
Either::Right(usize)(sqlx-mysql/src/statement.rs:40-42)。
这个 Either 类型是 sqlx 表达"能力差异"的一种手法——不是把每个驱动的 parameters 签名分叉,而是让统一签名返回一个枚举,调用方按 arm 分别处理。query! 宏的编译期校验就用这个差异:Postgres 下能检查每个参数的 Rust 类型匹不匹配 OID,SQLite 下只能检查参数个数匹不匹配。
3.6 元数据:TypeInfo
sqlx-core/src/type_info.rs:4:
pub trait TypeInfo: Debug + Display + Clone + PartialEq<Self> + Send + Sync {
fn is_null(&self) -> bool;
fn name(&self) -> &str;
fn type_compatible(&self, other: &Self) -> bool
where Self: Sized { self == other }
fn is_void(&self) -> bool { false }
}
TypeInfo 是数据库方的类型信息——"这一列是 INT4 还是 TEXT 还是 VARCHAR(255)"这种。不要和 Rust 侧的 Type<DB> trait 混淆——后者讲的是"Rust 的 i32 对应 DB 的哪个 TypeInfo"。这两者构成一对:
TypeInfo:DB → 自己对自己的描述。Type<DB>(第 5 章):Rust 类型 → DB TypeInfo 的映射。
Clone 和 PartialEq 是关键 super-trait——TypeInfo 经常需要按值传递、互相比较(比如"这一列的 TypeInfo 和我期望的是不是一致")。
具体实现差异:
- Postgres:
PgTypeInfo(PgType)是PgType枚举的 newtype(sqlx-postgres/src/type_info.rs:44)。PgType::Int4/PgType::Text/PgType::Custom(Arc<PgCustomType>)(用户自定义类型),另有只带名字或只带 OID、等待解析的DeclareWithName/DeclareWithOid。 - MySQL:
MySqlTypeInfo是ColumnType+ColumnFlags+ 可选的max_size(sqlx-mysql/src/type_info.rs:10-17)。 - SQLite:
SqliteTypeInfo(DataType)——DataType前五个变体 Null/Integer/Float/Text/Blob 对应 SQLite 动态类型系统的五种存储类,另有 Numeric、Bool、Int4、Date、Time、Datetime 这些按列声明类型选出的扩展变体(sqlx-sqlite/src/type_info.rs:13-38)。
is_void 方法默认 false,唯一的例外是 Postgres 的 void 类型(SELECT pg_sleep(1) 的返回)——这个方法让 query! 宏知道"所有列都是 void(或根本没有列)时就不生成记录结构体",直接展开成 __query_with_result(sqlx-macros-core/src/query/mod.rs:266-277)。
3.7 外层锚点:Connection / TransactionManager
这两个关联类型都在上一章已经见过——它们是 Database trait 引用另外两套 trait 家族的锚点。
type Connection: Connection<Database = Self>——PgConnection/MySqlConnection/SqliteConnection。第 12 章详细。type TransactionManager: TransactionManager<Database = Self>——PgTransactionManager/MySqlTransactionManager/SqliteTransactionManager。第 15 章详细。
它们的存在让 Database trait 不仅描述"数据格式",还描述"操作入口"——你从 DB::Connection 能获取连接,从 DB::TransactionManager 能开事务。所以 Database trait 是整个 driver 的类型级 manifest,读它就知道"这家驱动能提供什么"。
3.7.1 Connection 与 TransactionManager 的约束反转
这两个关联类型的约束用的是反向闭环:
type Connection: Connection<Database = Self>;
type TransactionManager: TransactionManager<Database = Self>;
第一行读作"我这个 Database 的 Connection 关联类型,必须指向一个具体的 PgConnection 之类,而那个 Connection 的 Database 关联类型又必须等于我"。
为什么这么绕?因为如果不加反向约束,下面这种病态实现就能编译:
// 假设没有 Database = Self 约束
impl Database for Postgres {
type Connection = MySqlConnection; // 反手实现一个 MySQL 的 Connection
// ...
}
那当用户调用 Pool::<Postgres>::acquire,底层建出来的 <Postgres as Database>::Connection 就是一个 MySQL 连接——类型系统彻底破产。Connection<Database = Self> 这条约束杜绝这种跨驱动张冠李戴。
这种"trait A 引用 trait B 的实现,要求 B 的关联类型回指 A"的模式叫做等式关联类型约束(associated type equality constraint),是 Rust trait 家族设计的基石。sqlx 的 11 个关联类型里,除了 TypeInfo、QueryResult(它们的 bound 不涉及 Database)和没有 bound 的 ArgumentBuffer<'q>,其余 8 个都带 Database = Self 约束,包括 Row<Database = Self>、Column<Database = Self>、Value<Database = Self>、Arguments<'q, Database = Self>——这条约束像胶水把这些关联类型粘成一个不可拆的类型包。
TransactionManager 的反向闭环同理。它的 trait 签名(sqlx-core/src/transaction.rs:15-50,去掉文档注释):
pub trait TransactionManager {
type Database: Database;
fn begin<'conn>(
conn: &'conn mut <Self::Database as Database>::Connection,
statement: Option<Cow<'static, str>>,
) -> BoxFuture<'conn, Result<(), Error>>;
fn commit(conn: &mut <Self::Database as Database>::Connection) -> BoxFuture<'_, Result<(), Error>>;
fn rollback(conn: &mut <Self::Database as Database>::Connection) -> BoxFuture<'_, Result<(), Error>>;
fn start_rollback(conn: &mut <Self::Database as Database>::Connection);
fn get_transaction_depth(conn: &<Self::Database as Database>::Connection) -> usize;
}
注意 begin 的参数类型是 &mut <Self::Database as Database>::Connection——它从 TransactionManager::Database 跳回 Database::Connection。这条类型遍历经过两次关联类型投影(先从 Self 拿到 Self::Database,再从它拿 Connection)。这里不需要 GAT:Connection 是普通关联类型,不带生命周期参数,所以 <Self::Database as Database>::Connection 这个投影写法从 0.6 到 0.8 一直没变(begin 的 statement 参数和 get_transaction_depth 是 0.8.4 的 #3765 才加的)。
3.7.2 约束怎样传播到下游
trait Database: 'static + Sized + Send + Debug 这四条 super-trait bound 管的是 DB 这个类型 token 本身;下游泛型代码能拿到的 DB::Connection: Send、DB::Row: 'static 之类能力,来自关联类型上的 bound 再叠加被引用 trait 自己的 super-trait。举个具体的传播链:
type Connection: Connection<Database = Self>+pub trait Connection: Send(sqlx-core/src/connection.rs:14)→ 任何DB: Database下都有DB::Connection: Send。type Row: Row<Database = Self>+pub trait Row: Unpin + Send + Sync + 'static(sqlx-core/src/row.rs:14)→DB::Row: Send + Sync + 'static。
这些 bound 的作用是"告诉编译器:任何持有 DB: Database 的泛型代码都可以自动获得 DB::Connection: Send 等派生能力"——让你在 fn with_conn<DB: Database>(pool: &Pool<DB>) 里直接 tokio::spawn(pool.acquire())(sqlx@=0.8.6 实测可编译),而不用手写十条 DB::Connection: Send + 'static 的 where 子句。这是 trait bound 的传递性给上层带来的"一次声明,处处可用"收益。
3.8 关联常量:NAME 与 URL_SCHEMES
被绝大多数读者忽略的两行:
const NAME: &'static str;
const URL_SCHEMES: &'static [&'static str];
Postgres::NAME = "PostgreSQL",URL_SCHEMES = &["postgres", "postgresql"]MySql::NAME = "MySQL",URL_SCHEMES = &["mysql", "mariadb"]Sqlite::NAME = "SQLite",URL_SCHEMES = &["sqlite"]
这两个常量的用途有二:
- Any 驱动的 URL scheme 分发(第 2.6 节):
install_drivers注册的是AnyDriver,这个结构体(sqlx-core/src/any/driver.rs:28-45)就持有这两个值(name、url_schemes),AnyConnection::connect("postgres://...")路径里的 scheme 匹配直接用driver.url_schemes.contains(&url.scheme())(sqlx-core/src/any/driver.rs:146);各驱动的AnyConnectionBackend::name()返回的也是NAME(如sqlx-postgres/src/any.rs:27-29)。 query!宏的驱动识别:sqlx-macros-core按URL_SCHEMES把DATABASE_URL分派给对应驱动,NAME则作为db_name写进离线查询数据,读回时核对"这份数据是不是给本驱动的"(sqlx-macros-core/src/query/mod.rs:39-40、query/data.rs:135-147)。
这两个值用关联常量而不是方法来承载,是因为它们对每个实现都是编译期定值,而且关联常量能在 const fn 里使用(这对 Any 驱动的 AnyDriver::without_migrate::<DB>() 是必要的,见 2.6.5 节)。它们是 0.7.0 拆分驱动 crate 时(#2039)才加进 Database 的。
3.9 HasStatementCache:无方法标记 trait
sqlx-core/src/database.rs:113-114:
/// A [`Database`] that maintains a client-side cache of prepared statements.
pub trait HasStatementCache {}
一个 没有方法的 trait。在 0.8.6 里,三家 in-tree 驱动都实现了它——sqlx-postgres/src/database.rs:40、sqlx-mysql/src/database.rs:38、sqlx-sqlite/src/database.rs:39 各有一行 impl HasStatementCache for Xxx {},连 Any 也在 sqlx-core/src/any/database.rs:39 补了一行。SQLite 之所以也算数,是因为它的 worker 里确实挂着一个 StatementCache<VirtualStatement>(sqlx-sqlite/src/connection/mod.rs:178),persistent 参数会一路传到 execute。
那这个 trait 存在的意义是什么?它是给第三方驱动留的一个"我没有语句缓存"的出口:不实现它,Query::persistent 和 Connection::cached_statements_size 这两个 API 在你的驱动上就编译不出来,用户不会写出"设了 persistent(true) 却被悄悄忽略"的代码。
这个 "标记 trait"(marker trait)的作用是把一项能力编码进类型系统。上层 API 用 where DB: HasStatementCache 守护"只有支持缓存的 DB 才能用这些方法"。
具体例子 1:Connection::cached_statements_size(sqlx-core/src/connection.rs:127-132):
fn cached_statements_size(&self) -> usize
where
Self::Database: HasStatementCache,
{
0
}
这条 where 子句让"不带缓存的驱动"上的 cached_statements_size() 编译期就调不了——调用它编译器直接报 trait bound 不满足,而不是运行时返回 0 后你疑惑为什么缓存永远是 0。同一条 where 也守着 clear_cached_statements(sqlx-core/src/connection.rs:136)。
具体例子 2:Query::persistent(sqlx-core/src/query.rs:123-125 的 impl 头):
impl<'q, DB, A> Query<'q, DB, A>
where
DB: Database + HasStatementCache,
{
pub fn persistent(mut self, value: bool) -> Self { ... }
}
persistent 方法只在 DB: HasStatementCache 时存在。三家 in-tree 驱动都实现了这个 marker,所以三家都能写 query("...").persistent(true);一个不实现它的第三方驱动上,这行会编译失败。
这种"把运行时能力差异搬到类型系统"的做法,是 Rust trait 家族设计里最精妙的一招。它避免了"传一个 persistent=true 却被悄悄忽略"这类隐式降级——错误提前到编译期。
Any 驱动(sqlx-core/src/any/database.rs,全文 39 行,最后一行就是这个 impl)也实现了 HasStatementCache——这不是因为 Any 自己有缓存,而是 Any 需要保持和它"可能包装任一驱动"的泛用性一致,实际行为取决于内层具体驱动(any/connection/mod.rs:104 的 cached_statements_size 直接转发给 backend)。
3.9.1 Rust 生态里的 marker trait 家谱
HasStatementCache 这种"无方法 trait 作为能力开关"的做法在 Rust 生态里有一整条谱系。把 sqlx 放进来对照:
| Marker trait | 属于哪个库 | 表达的能力 | 使用形态 |
|---|---|---|---|
Send |
std |
可以跨线程转移所有权 | where T: Send |
Sync |
std |
&T 可以跨线程共享 |
where T: Sync |
Unpin |
std (通过 Pin) |
不受固定语义约束 | where T: Unpin |
Copy |
std |
按位拷贝即可复制 | where T: Copy |
Sized |
std |
编译期已知大小 | where T: Sized(默认隐含) |
HasStatementCache |
sqlx-core |
支持客户端预处理语句缓存 | where DB: HasStatementCache |
DatabaseExt |
sqlx-macros-core |
参与 query! 的编译期校验 | where DB: DatabaseExt |
TypeChecking |
sqlx-core |
能把 DB 类型映射到 Rust 类型字符串 | where DB: TypeChecking |
观察几条规律:
- 无方法 marker 大多表达"能力开关"或"抽象断言"——它们不是告诉你"应该调什么方法",而是告诉类型系统"我有这项能力"。
HasStatementCache精确属于这一类。 - sqlx 的三个能力断言(
HasStatementCache/TypeChecking/DatabaseExt)分布在两个 crate——前两个在sqlx-core(sqlx-core/src/database.rs:114、type_checking.rs),DatabaseExt在sqlx-macros-core(database/mod.rs:16)。这个拆分和 §2.3.1 讨论的 crate 边界一致。 - 没有"方法"的 trait 也可以继承有方法的 trait——比如
DatabaseExt: Database + TypeChecking(§2.3.1),它自身有 3 个方法但主要用法是作为"组合能力断言"。marker trait 的边界其实是模糊的:只要这个 trait 的主要使用方式是作 bound 而不是调方法,就属于这一族。
学会用 marker trait 表达能力差异,是 Rust trait 家族设计的基本功。HasStatementCache 这一行短短的 pub trait HasStatementCache {} 背后,代表的是一种"把动态 bool 搬到静态类型系统"的思路——一个驱动有没有语句缓存,不是运行时查一个 flag,而是它实不实现这个 trait。
3.9.2 如果缺了某个关联类型会怎样
把 Database 的每一个关联类型做减法实验,看看少了它会坏在哪:
- 少了
Row:整条Executor::fetch路径断裂——BoxStream<'e, Result<DB::Row, Error>>签名写不出来。 - 少了
Column:Statement::columns返回类型丢失,query!的列类型映射没处落脚。 - 少了
TypeInfo:Type<DB>trait 的type_info()返回类型丢失;更严重的是第 5 章的 Encode/Decode 整套断了。 - 少了
Value:ValueRef::to_owned()返回什么?无法把一个未解码的值从 Row 里抽出来单独保存。 - 少了
ValueRef<'r>:try_get_raw断——必须先做to_owned()再 decode,所有解码都变成按值传递,性能大幅下降。 - 少了
Arguments<'q>:query(sql).bind(x)无处存值,Query 类型整个需要重做。 - 少了
ArgumentBuffer<'q>:Encode::encode_by_ref的 buf 参数类型丢失。 - 少了
Statement<'q>:Executor::prepare_with断,所有"用相同 SQL 多次查询"的优化消失。 - 少了
QueryResult:execute无法告诉用户"影响了几行"。 - 少了
TransactionManager:没法开事务,Connection::begin断。 - 少了
NAME:Any 连接报不出自己的驱动名,query!的离线数据也没法核对是给哪家驱动的——可以重建但麻烦。 - 少了
URL_SCHEMES:Any 驱动彻底废——无法按 URL scheme 分发;query!宏也没法按DATABASE_URL选驱动。
这个减法实验本身不算新知识,但它证明了每一个关联类型都不是装饰——都对应着上层某一条具体的 API 路径或用户场景。这也回答了"为什么是 11 个不多不少"。这组类型也不是一次定型的:0.3.0 的 Database 上还挂着 TableId、RawBuffer、Error,0.4.0 换成与今天基本一致的一组(当时 QueryResult 叫 Done,0.5.0 改名),0.8.0 再把三个 Has* helper trait 收回成 GAT。
3.10 三家对照:Postgres / MySQL / SQLite 的 impl Database
把三家的 impl Database 并排摆一起(源文件分别是 sqlx-postgres/src/database.rs:14、sqlx-mysql/src/database.rs:12、sqlx-sqlite/src/database.rs:13):
| 关联类型 | Postgres | MySQL | SQLite |
|---|---|---|---|
Connection |
PgConnection |
MySqlConnection |
SqliteConnection |
TransactionManager |
PgTransactionManager |
MySqlTransactionManager |
SqliteTransactionManager |
Row |
PgRow |
MySqlRow |
SqliteRow |
QueryResult |
PgQueryResult |
MySqlQueryResult |
SqliteQueryResult |
Column |
PgColumn |
MySqlColumn |
SqliteColumn |
TypeInfo |
PgTypeInfo |
MySqlTypeInfo |
SqliteTypeInfo |
Value |
PgValue |
MySqlValue |
SqliteValue |
ValueRef<'r> |
PgValueRef<'r> |
MySqlValueRef<'r> |
SqliteValueRef<'r> |
Arguments<'q> |
PgArguments |
MySqlArguments |
SqliteArguments<'q> |
ArgumentBuffer<'q> |
PgArgumentBuffer |
Vec<u8> |
Vec<SqliteArgumentValue<'q>> |
Statement<'q> |
PgStatement<'q> |
MySqlStatement<'q> |
SqliteStatement<'q> |
NAME |
"PostgreSQL" |
"MySQL" |
"SQLite" |
URL_SCHEMES |
&["postgres", "postgresql"] |
&["mysql", "mariadb"] |
&["sqlite"] |
HasStatementCache |
实现(sqlx-postgres/src/database.rs:40) |
实现(sqlx-mysql/src/database.rs:38) |
实现(sqlx-sqlite/src/database.rs:39) |
几条对比观察:
- 11 行关联类型 + 2 行常量 + 1 行 marker trait——三家结构完全平行,只是每个位置的具体类型不同。这就是 trait 家族设计的目标形态:等价替换。
Arguments<'q>的生命周期参数:Postgres 和 MySQL 是PgArguments和MySqlArguments(无'q),因为它们编码后就是字节 buffer 自包含了;SQLite 是SqliteArguments<'q>保留生命周期——因为 SQLite 的参数值(SqliteArgumentValue::Text(Cow<'q, str>))可能借用外部字符串,不会预先序列化。ArgumentBuffer<'q>的形态差异最大——Postgres 的PgArgumentBuffer有 patch 和 type-hole 机制(延迟回填 OID),MySQL 直接裸字节,SQLite 是值向量。这揭示了三家协议的根本差异:Postgres 有 OID 的运行时查表、MySQL 有 COM_STMT_EXECUTE 的二进制格式、SQLite 有 C API 的"值按 tag 传"。后续第 16-18 章分别展开。- 三家都实现
HasStatementCache——所以query("...").persistent(true)在三家下都可用。这个 marker 真正的用处是给"没有语句缓存的第三方驱动"留一个不实现它的出口(§3.9)。
(impl Database for Postgres 在 sqlx-postgres/src/database.rs:14、MySQL 在 sqlx-mysql/src/database.rs:12、SQLite 在 sqlx-sqlite/src/database.rs:13。)这张表不是让你背下来的——它是让你在后面读具体驱动实现时脑子里有一张"同构映射"的参照。当你读 sqlx-postgres/src/row.rs 看到 PgRow 时,你立刻知道它的位置对应 DB::Row,功能和 SqliteRow 等价——只是载荷格式不同。
3.11 本章小结
本章把 sqlx 整个类型体系的"收束点"Database trait 拆开,可以带走以下关键判断:
- 11 个关联类型 + 2 个关联常量 + 4 条 super-trait bound 共同描述了"一个 sqlx 驱动应该提供什么"——读取链(Row/Column/Value/ValueRef/QueryResult)、写入链(Arguments/ArgumentBuffer/Statement)、元数据(TypeInfo)、外层锚点(Connection/TransactionManager)、名字识别(NAME/URL_SCHEMES)。
- GAT(泛型关联类型)是让这套设计成立的语言前提(§3.3)。0.7 及之前的 sqlx 必须用
HasValueRef<'r>/HasArguments<'q>/HasStatement<'q>三个 helper trait +for<'r>HRTB 绕过;0.8.0 的 #2973 才换成 GAT。GAT 简化的是类型的写法,用户泛型代码里的 HRTB 约束仍然要写。0.8.x的 MSRV 是 1.78.0,远高于 GAT 所需的 1.65,这次迁移没有额外的 MSRV 代价。 dyn Database写不出来的根因是Sized而不是 GAT(§3.2、§3.3.1)——trait 头Database: 'static + Sized + Send + Debug里的Sized在 GAT 化之前就排除了它;GAT 只是又加了一条(带 GAT 的 trait 不是 dyn 兼容的)。Any 驱动(§2.6)必须另起炉灶用AnyConnectionBackendtrait 绕过,这条代价预期之内。- Value 与 ValueRef 的分裂(§3.4.3)反映了"数据持有权"的工程现实——ValueRef 是 decode 路径默认形态(零拷贝),Value 用于把未解码的值脱离 row 单独保存的场景。0.8.6 里从 row 取出的 ValueRef 调
to_owned在三家都只是引用计数递增;SQLite 的复制发生在取行时(每列sqlite3_value_dup)——差异在上层 API 不可见但性能可感。 - ArgumentBuffer 没有共同 trait(§3.5.2)——三家形态截然不同:Postgres 的
PgArgumentBuffer带 patch/hole、MySQL 是Vec<u8>、SQLite 是Vec<SqliteArgumentValue<'q>>。这是刻意不抽象的一处:协议差异太大,硬抽象只会让每家都被迫做运行时检查。 - HasStatementCache 是把能力编码进类型系统的标记 trait(§3.9)——0.8.6 的三家 in-tree 驱动加上
Any都实现了它。Query::persistent、Connection::cached_statements_size与clear_cached_statements的where DB: HasStatementCache让"在一个没有语句缓存的驱动上调这些方法"变成编译错误而不是运行时静默。 - NAME 与 URL_SCHEMES 两个常量(§3.8)让 Any 驱动可以在运行时按 URL scheme 分发,也让
query!宏能按DATABASE_URL选驱动、核对离线数据属于哪家驱动。 - 三家对照表(§3.10)显示整个 trait 家族的"等价替换"结构——每一行都平行,每家只是具体类型不同。这就是 trait 家族设计的终极目标。
3.11.1 判断题:如果换你设计
最后做几道"设计选择"判断题——把本章的理解直接转成工程直觉:
Q1:如果你要加一个 type Transaction<'c> 关联类型专门表示事务,让用户写 DB::Transaction<'c>::commit() 而不是 conn.begin().await?.commit().await?——这算好设计吗?
A:不算。Transaction<'c, DB> 作为一个具体 struct 已经够用(sqlx-core/src/transaction.rs:86),它本身不带驱动特有行为——三家 DB 的区别被 TransactionManager trait 消化。再加一层 type Transaction<'c> 关联类型会变成空壳,让 trait 家族膨胀无收益。判断原则:只有当"具体类型在不同 DB 下形态不同"时才值得加关联类型——Transaction 本身是泛型 struct,不满足这条。
Q2:如果把 NAME: &'static str 换成 fn name() -> &'static str 方法,行为等价吗?
A:等价,但 NAME 的关联常量形态能在 const fn 里用。回想 §2.6.5 里 AnyDriver::without_migrate::<DB>() 是 const fn——它要读 DB::NAME 和 DB::URL_SCHEMES。如果改成 fn name(),这条 const fn 路径会断(或者 Rust 的 const fn 能力要往前走一大步)。所以选择关联常量是被 Any 驱动的 const 注册路径倒推出来的。
Q3:ArgumentBuffer<'q> 无 trait bound 是偷懒吗?
A:不是偷懒,是不硬抽。这个缓冲区的形态在 Postgres / MySQL / SQLite 下差异是本质的——硬抽一个 trait Buffer { fn write_bytes(...); } 会让 SQLite 的 Vec<SqliteArgumentValue<'q>> 必须做一次 serialize-to-bytes-then-parse 的无谓开销。不抽,把表达权交给驱动,反而让每家 encode 路径最优。判断原则:抽象应当消除重复、而不是掩盖差异——后者往往导致泄漏。
Q4:为什么 TypeInfo 不叫 DbType 或 ColumnType?
A:因为 TypeInfo 承担的不只是"列类型"——它也被 Arguments::add 的类型检查、Value::type_info、Statement::parameters 共享。ColumnType 会狭化其语义;DbType 歧义——Rust 侧的 Type<DB> trait 也算"类型"。TypeInfo 这个名字精确对应"数据库方提供的类型描述信息",和 Rust 侧的 Type 一目了然区分。好的 trait 命名像好的变量命名——多花十分钟,长期收益巨大。
做完这四道题,你对 sqlx Database trait 的每一处选择就有了自己能解释的直觉,而不只是"背下来"。下一章进入 Executor 时,你会发现同样的设计权衡在不同 trait 上反复出现——这就是 trait 家族设计的整体性。
3.12 下一章指路
下一章,我们进入 Executor trait——Database trait 描述"驱动应该提供什么类型",Executor 描述"连接、Pool、Transaction 应该共同满足什么操作接口"。我们会看到 fetch / fetch_many / execute 这些方法如何围绕 DB: Database 构建、为什么 0.7.0 之后不再支持 impl Executor for &mut Transaction(本专栏第 1 章已经预告过,到第 4 章我们会从 trait 边界视角再看一遍)。