Skip to content

第4章 Executor trait:连接、Pool、Transaction 的共同接口

"If you can't say it in one primitive, you have the wrong primitive." —— trait 设计的迭代真理

本章要点

  • Executor<'c> trait(sqlx-core/src/executor.rs:33)定义了**"能跑查询的东西"**——4 个必实现方法fetch_many + fetch_optional + prepare_with + describe),6 个带默认实现execute / execute_many / fetch / fetch_all / fetch_one / prepare)—— 驱动作者只需实现 4 个原语、其余由 core 默认构造。
  • 两个核心流式原语——fetch_many 返回 BoxStream<Either<QueryResult, Row>>(流式 + 多结果集),fetch_optional 返回 BoxFuture<Option<Row>>(最多一行)。高层默认实现:execute_many / fetch / fetch_all 基于 fetch_manyfetch_one 基于 fetch_optional(未取到行时返 RowNotFound);execute 基于 execute_manyprepare 基于 prepare_with
  • 只有两种类型实现 Executor&Pool<DB>pool/executor.rs:12)和 &mut DB::Connection(每个驱动各自的 connection/executor.rs)——极简清单。
  • &mut Transaction&mut PoolConnection 被从 Executor impl 列表里删掉不是因为 coherence——是因为 "fails to compile due to lack of lazy normalization"transaction.rs:132 注释原文)和 "Causes an overflow when evaluating &mut DB::Connection: Executor"pool/executor.rs:71 注释原文)。两个都是 Rust trait 求解器的具体限制。
  • Execute<'q, DB> trait 是 query shape 的抽象——&str(&str, Option<Arguments>)Query<DB, A> 各自实现它。为什么不为 String 实现?刻意留的防御层(§4.5)。
  • prepare / describe 方法是 query! 宏的私有接口——describe 标注 #[doc(hidden)],专为编译期类型检查设计。

4.1 问题引入:什么东西应该能跑查询

上一章的 Database trait 描述了"一个 sqlx 驱动应该提供哪些类型"。现在切换视角:用户代码在调 .fetch_one(&x) 时,这个 x 是什么? 用户会传什么?答案有三个:

  1. &pool——从连接池随机借一个连接、跑完归还。
  2. &mut conn——显式拿到的单个连接,能保证两条 SQL 落到同一物理连接。
  3. &mut tx——事务中的连接,保证这些 SQL 在同一事务边界。

这三个"能跑查询的东西"形态不同:Pool 是 Arc<PoolInner>,Connection 是独占的 TCP stream,Transaction 是包装了 Connection 的 RAII guard。让它们共用同一套 .fetch_one() / .fetch_all() / .execute() API,就需要一个操作接口级的 trait——这就是 Executor

注意这里的边界:Database trait 是类型级 manifest(驱动提供什么类型),Executor操作级 contract(什么东西能跑查询)。前者是"名词",后者是"动词"。

sqlx 把这个"能跑查询的东西"抽象成 Executor<'c> trait——其中 'c 是 executor 自身的借用生命周期(Pool 的 &pool 借用、Connection 的 &mut conn 借用)。这个 trait 的设计相当简约——4 个必实现方法(fetch_many / fetch_optional / prepare_with / describe)+ 6 个默认方法(execute / execute_many / fetch / fetch_all / fetch_one / prepare),下文逐一拆。

4.2 Executor trait 的全貌

打开 sqlx-core/src/executor.rs:33-180,省略文档注释:

rust
pub trait Executor<'c>: Send + Debug + Sized {
    type Database: Database;

    // 必实现
    fn fetch_many<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<
        Either<<Self::Database as Database>::QueryResult,
               <Self::Database as Database>::Row>,
        Error>>
    where 'c: 'e, E: 'q + Execute<'q, Self::Database>;

    fn fetch_optional<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<
        Option<<Self::Database as Database>::Row>, Error>>
    where 'c: 'e, E: 'q + Execute<'q, Self::Database>;

    fn prepare_with<'e, 'q: 'e>(self, sql: &'q str,
        parameters: &'e [<Self::Database as Database>::TypeInfo])
        -> BoxFuture<'e, Result<<Self::Database as Database>::Statement<'q>, Error>>
    where 'c: 'e;

    #[doc(hidden)]
    fn describe<'e, 'q: 'e>(self, sql: &'q str)
        -> BoxFuture<'e, Result<Describe<Self::Database>, Error>>
    where 'c: 'e;

    // 默认方法:execute、execute_many、fetch、fetch_all、fetch_one、prepare
}

四个必实现方法、六个默认方法。让我们把它们放进一张分层图:

这张图揭示了 sqlx Executor 最重要的设计判断:fetch_many 是唯一的读原语。所有"读行"类操作都建立在它之上。

4.3 fetch_manyfetch_optional 为什么是两个原语

fetch_many 的返回类型是整章最需要停下来看清楚的:

rust
BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>

拆开读:

  • 外层 Stream——异步可迭代的一连串 item。
  • 每个 item 是 Result<Either<QueryResult, Row>, Error>
  • Either::Left(QueryResult)——"这条 SQL 执行完了,影响了 N 行"。
  • Either::Right(Row)——"下一行数据"。

为什么要这种"混合流"?因为一次 SQL 调用可能包含多条语句:

sql
UPDATE users SET active = false WHERE last_login < '2020-01-01';
SELECT id, name FROM users WHERE active = true;

Postgres / MySQL 的 simple query protocol 允许把这一整段文本一次发过去——服务端会按分号切分、顺序执行、每条语句返回自己的结果。第一条是 UPDATE,返回一个 "UPDATE 42" 命令完成消息(对应 QueryResult { rows_affected: 42 });第二条是 SELECT,返回 0..N 行数据 + 最后一个 "SELECT N" 完成消息。

fetch_many 的 Either 流把这个行为直接暴露到类型系统——你作为调用方能看到这个交替序列。从 execute_many / fetch / execute 这三个派生方法的实现可以看出是怎么"降维"的。

fetchexecutor.rs:73-88)——只要行,丢掉 QueryResult:

rust
fn fetch<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<DB::Row, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
    self.fetch_many(query)
        .try_filter_map(|step| async move {
            Ok(match step {
                Either::Left(_) => None,
                Either::Right(row) => Some(row),
            })
        })
        .boxed()
}

execute_manyexecutor.rs:50-68)——只要 QueryResult,丢掉行:

rust
fn execute_many<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<DB::QueryResult, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
    self.fetch_many(query)
        .try_filter_map(|step| async move {
            Ok(match step {
                Either::Left(rows) => Some(rows),
                Either::Right(_) => None,
            })
        })
        .boxed()
}

executeexecutor.rs:37-46)——execute_many.try_collect(),把所有 QueryResult 累加成一个:

rust
fn execute<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<DB::QueryResult, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
    self.execute_many(query).try_collect().boxed()
}

try_collect 依赖 DB::QueryResult: Extend<DB::QueryResult>——这条 bound 在第 3 章讲过,正是为这里准备的。如果你的 SQL 有两条 UPDATE,execute 返回的是"总 rows_affected = 42 + 17",实现靠 PgQueryResult::extend 里一行 self.rows_affected += other.rows_affected

fetch_allexecutor.rs:96-104)——fetch.try_collect(),把所有 Row 收集进 Vec:

rust
fn fetch_all<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<Vec<DB::Row>, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
    self.fetch(query).try_collect().boxed()
}

fetch_oneexecutor.rs:107-121)——fetch_optional + 空值报错:

rust
fn fetch_one<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<DB::Row, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
    self.fetch_optional(query)
        .and_then(|row| match row {
            Some(row) => future::ok(row),
            None => future::err(Error::RowNotFound),
        })
        .boxed()
}

七个"用户视角的读/执行方法"(executeexecute_manyfetchfetch_manyfetch_allfetch_onefetch_optional)背后只有两个必实现原语:fetch_many 提供流式读主干、fetch_optional 留作驱动的 "最多一行" 协议级优化入口(再加 prepare_with / describe 两个 meta 方法、共 4 个必实现)。这是 sqlx Executor 最值得称道的设计——用单个 Either 流表达所有读路径,然后用 futures-util 里的 combinator 做投影。驱动作者只要写对 fetch_many,其它方法自动获得正确行为。

为什么 fetch_optional 也要做成必实现?从源码注释和其他默认实现的包装方式推测:因为 fetch_one 只需要一行、fetch_optional 要不要拉满、两者都可以在 fetch_many 的基础上通过 take(1) 实现,但驱动可以做协议级的优化——比如 Postgres 的 Extended Query 在 Bind 阶段可以用 max_rows=1 限制服务端,少传送后面的数据。把 fetch_optional 留给驱动覆盖是优化留口。

4.3.1 Either 流的实际形态

为了把 Either<QueryResult, Row> 这个抽象落地,我们看三个具体场景它长什么样:

场景 A:单条 SELECT,返回 3 行

Right(Row1)
Right(Row2)
Right(Row3)
Left(QueryResult { rows_affected: 3 })     // Postgres 以 CommandComplete 结尾;部分驱动也会给一条 QueryResult

场景 B:单条 UPDATE,影响 17 行

Left(QueryResult { rows_affected: 17 })    // UPDATE 不返回行,直接一条 QueryResult

场景 C:多条语句用分号拼接

sql
UPDATE users SET active = false WHERE last_login < '2020-01-01';
SELECT id, name FROM users WHERE active = true;
DELETE FROM audit_log WHERE created < '2019-01-01';

流形态:

Left(QueryResult { rows_affected: 42 })    // UPDATE 完成
Right(Row{id:1, name:"Alice"})             // SELECT 第一行
Right(Row{id:2, name:"Bob"})               // SELECT 第二行
Left(QueryResult { rows_affected: 2 })     // SELECT 完成(影响的"行数"就是返回的行数)
Left(QueryResult { rows_affected: 1000 })  // DELETE 完成

场景 C 是 fetch_many 的真正用武之地——你能一条流看到三条语句的交替结果。如果用 fetch_all,你拿到的是 Vec<Row> 只含 Alice 和 Bob,两条 QueryResult 被丢弃——大多数业务不 care、丢了也就丢了,但迁移脚本或管理工具可能需要知道 UPDATE 和 DELETE 的影响行数。这时候 execute_many(只保留 Left)或 fetch_many(全保留)才有意义。

这也是为什么 sqlx-cli 的 sqlx migrate runexecute_many 而不是 execute——它要把每条迁移 SQL 的结果都记录进 _sqlx_migrations 表。

4.3.2 为什么不用 AsyncIterator

Rust 生态里表示"异步迭代"的 trait 有两种:

  • futures_core::Stream(sqlx 用的)——fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Item>>
  • std::async_iter::AsyncIterator#![feature(async_iterator)] 还在 nightly)

sqlx Executor::fetch_many 返回 BoxStream——前者。这个选择是时代产物:Stream 在 futures-rs 0.1 时代就稳定了,生态里所有的 combinator(try_filter_maptry_collect)都围绕它构建。sqlx 0.1 到 0.8 一直用 Stream,没理由切换到一个还在 nightly 的 trait。

BoxStream 而不是 impl Stream——这是异步 trait 方法的固定代价impl Streamasync fn trait 方法里还不稳定(2026 年 4 月,RPIT in traits 已稳定但 AFIT 对 Stream 返回仍有 Send 推导问题)。Box::pin 强制堆分配,但让 trait 本身 object-safe(虽然带 GAT 依然不行,见 §3.3.1),也让生命周期分析简单。

这条选择在第 9.0-alpha 里传闻会变——用 -> impl Stream<Item = ...> + Send 替换 BoxStream,省一次堆分配。但那依赖 Rust 编译器更多的 AFIT 改进。

4.4 谁实现 Executor?只有两个

整个 sqlx 生态里实现 Executor 的类型极少

  1. &Pool<DB>——sqlx-core/src/pool/executor.rs:12
  2. &mut PgConnection / &mut MySqlConnection / &mut SqliteConnection——各自驱动的 connection/executor.rs(Postgres 在 sqlx-postgres/src/connection/executor.rs:380
  3. &mut AnyConnection——sqlx-core/src/any/connection/executor.rs:11
  4. &mut PgListener——sqlx-postgres/src/listener.rs:388,用于 Postgres LISTEN/NOTIFY 场景

以上是全部。用户代码看到的所有 .fetch_one(X) 里的 X,最终都走这四条路径之一——其中前两条覆盖 99% 用例。

4.4.1 &Pool<DB> 的实现

sqlx-core/src/pool/executor.rs:12-73 的完整实现值得通读:

rust
impl<'p, DB: Database> Executor<'p> for &'_ Pool<DB>
where
    for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
{
    type Database = DB;

    fn fetch_many<'e, 'q: 'e, E>(self, query: E)
        -> BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>
    where E: 'q + Execute<'q, Self::Database>,
    {
        let pool = self.clone();

        Box::pin(try_stream! {
            let mut conn = pool.acquire().await?;
            let mut s = conn.fetch_many(query);

            while let Some(v) = s.try_next().await? {
                r#yield!(v);
            }

            Ok(())
        })
    }

    // fetch_optional / prepare_with / describe 类似——都是 pool.acquire().await?.XXX
}

几个值得注意的细节:

  1. where for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>——一条高阶 trait bound(HRTB)。它说:对于任何生命周期 'c&'c mut DB::Connection 都必须实现 Executor<'c>。这条约束是必要的——因为 fetch_many 内部要调 conn.fetch_many(query),而那个 connPoolConnection<DB>,用 DerefMut 投影到 &mut DB::Connection。如果不保证后者是 Executor,这条链就断了。
  2. let pool = self.clone();——Pool<DB> 内部是 Arc<PoolInner<DB>>Clone 就是增加引用计数。clone 的原因是要把 pool move 进 try_stream! 生成的异步块,延长生命周期(从 &'p Pool 到 stream 自身的 'e)。
  3. try_stream! { ... }——来自 async-stream crate 的宏,把 "生产多个值的异步代码" 包装成一个 Streamr#yield!(v) 把值送到 stream 的 poll_next
  4. 连接归还let mut conn = pool.acquire().await?; 拿到 PoolConnection——它在 stream 结束(无论正常还是错误)时被 drop,drop 实现里归还连接给 pool。这就是为什么 &pool.fetch_one(...) 用起来这么丝滑——连接的获取和归还全被 Executor impl 包掉了。

4.4.2 &pool vs &mut conn vs &mut *tx:三种 Executor 的选择

既然只有 &Pool<DB>&mut DB::Connection 两种 Executor,用户代码里会看到四种实际写法:

写法类型(实际)适用场景连接归属
fetch_one(&pool)&Pool<DB>单条独立查询,无跨查询一致性需求每次 acquire + 立即归还
fetch_one(&mut conn)&mut DB::Connection已经从 pool 借出,要复用一段时间持续占用,drop 时归还
fetch_one(&mut pool_conn)&mut PoolConnection<DB>(自动 deref)同上,但从 pool 借出同上
fetch_one(&mut *tx)&mut DB::Connection(经 DerefMut)事务中查询同事务内

&pool 的成本:每次调用都执行一次 acquire + fetch + dropacquire 在 pool 空闲时是 O(1)(从 ArrayQueue 弹一个),繁忙时会排队等 permit(AsyncSemaphore)。单看一次调用极快,但高频调用(例如 handler 里六条查询)每条都过 acquire 路径,累计 6 次 permit 争用。

&mut conn 的成本:一次 pool.acquire().await? 借出,后面所有查询共用这一个 TCP 连接。优点是省 6 次 permit;缺点是你占住了池子里一条连接直到变量 drop——其他 handler 如果也用 pool,可能排队等你。

经验法则(在第 22 章会再细化):

  • 单条查询 + 不在循环里:&pool
  • 同一逻辑单元里 ≥ 3 条查询:let mut conn = pool.acquire().await?; 然后重复用 &mut conn
  • 跨查询一致性要求(读-改-写、读后避免其他事务插入):开 Transaction,用 &mut *tx

所有这些写法都通过 Executor 这一个 trait 统一——用户不需要记四个 API,只需要记"能跑查询的就是 Executor"。

4.4.3 &mut PgConnection 的实现

sqlx-postgres/src/connection/executor.rs:380 是真正干活的地方:

rust
impl<'c> Executor<'c> for &'c mut PgConnection {
    type Database = Postgres;

    fn fetch_many<'e, 'q, E>(self, mut query: E)
        -> BoxStream<'e, Result<Either<PgQueryResult, PgRow>, Error>>
    where 'c: 'e, E: Execute<'q, Self::Database>, 'q: 'e, E: 'q,
    {
        let sql = query.sql();
        let metadata = query.statement().map(|s| Arc::clone(&s.metadata));
        let arguments = query.take_arguments().map_err(Error::Encode);
        let persistent = query.persistent();

        Box::pin(try_stream! {
            let arguments = arguments?;
            let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?);

            while let Some(v) = s.try_next().await? {
                r#yield!(v);
            }

            Ok(())
        })
    }
    // ...
}

注意这里不再 clone——&mut PgConnection 是独占借用,本来就是"只借一次"。整个 Executor impl 把用户传入的 query: E 拆成 sql、metadata、arguments、persistent 四块,然后调用 PgConnection::runsqlx-postgres/src/connection/executor.rs 的底部方法)——这是整个 Postgres 协议交互的入口,也是第 16 章的主题。

4.4.4 &mut PgListener 为什么单独实现 Executor

sqlx-postgres/src/listener.rs:388 多了一个看起来突兀的 impl:

rust
impl<'c> Executor<'c> for &'c mut PgListener { ... }

PgListener 是 Postgres 独有的 LISTEN / NOTIFY 事件订阅者——用户调 PgListener::listen("channel_name").await? 注册感兴趣的 channel,然后 .recv().await? 阻塞等通知。它内部持有一个 PgConnection,但不直接暴露——如果把 PgListener 当成普通 PgConnection 用,会把 LISTEN 状态搞坏。

为什么还要让它实现 Executor?因为有时候用户确实想在 Listener 的同一个连接上跑一条 SQL——比如在 recv 之前跑一条初始化查询。如果不让 PgListener 实现 Executor,用户必须"开 listener 之前先用 pool 借一条连接跑 SQL、再换 listener"——两条连接开销加倍。

Listener 的 Executor impl 内部委托给它持有的 connection,但在 SQL 执行前后小心不要把通道订阅状态破坏。这是"特殊情况特殊处理"的一个案例——显示 Executor trait 的 implementor 集合不是一刀切的"所有连接类型都 impl",而是有意识地为需要的场景保留入口。

MySQL 和 SQLite 没有对应的 Listener 类型,所以也没有对应的 Executor impl。这种"只给有用场景留 impl"的精打细算是 sqlx 的一贯做法。

4.5 Execute<'q, DB>:查询形态的抽象

fetch_many 的参数 E: Execute<'q, Self::Database> 里的 Execute 是另一个 trait——query shape 的抽象executor.rs:194-218):

rust
pub trait Execute<'q, DB: Database>: Send + Sized {
    fn sql(&self) -> &'q str;
    fn statement(&self) -> Option<&DB::Statement<'q>>;
    fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError>;
    fn persistent(&self) -> bool;
}

四个方法对应一条 SQL 查询的四个属性:

  1. sql——SQL 字符串。
  2. statement——如果 SQL 已经预处理过,复用 statement 缓存。
  3. take_arguments——参数(可选——无参数的查询用 simple query protocol)。
  4. persistent——是否缓存 prepared statement 到连接。

这个 trait 有两个内置实现(同一个文件 220-260 行):

rust
impl<'q, DB: Database> Execute<'q, DB> for &'q str {
    fn sql(&self) -> &'q str { self }
    fn statement(&self) -> Option<&DB::Statement<'q>> { None }
    fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError> { Ok(None) }
    fn persistent(&self) -> bool { true }
}

impl<'q, DB: Database> Execute<'q, DB> for (&'q str, Option<DB::Arguments<'q>>) {
    fn sql(&self) -> &'q str { self.0 }
    fn statement(&self) -> Option<&DB::Statement<'q>> { None }
    fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError> { Ok(self.1.take()) }
    fn persistent(&self) -> bool { true }
}

第一条让你直接 pool.execute("DELETE FROM cache").await? ——字符串字面量本身就是 Execute。

第二条让你 (sql_str, Some(args)) 也算 Execute——这是 query(sql).bind(x) 这条链的最底层表达。

第三个实现Query<'q, DB, A> 自己——在 sqlx-core/src/query.rs 定义,我们在第 9 章详细看。

4.5.1 为什么不给 String 实现 Execute?

executor.rs:221 上方有一行意味深长的注释

rust
// NOTE: `Execute` is explicitly not implemented for String and &String to make it slightly more
//       involved to write `conn.execute(format!("SELECT {val}"))`

"刻意不为 String&String 实现 Execute,让 conn.execute(format!("SELECT {val}")) 稍微难写一点。"

这是 SQL 注入防御的 trait 级拒绝。考虑下面这两种写法:

rust
// 类型系统不拦:
conn.execute("SELECT * FROM users WHERE id = 1").await?;

// 类型系统拒绝(String 不是 Execute):
let id = "1 OR 1=1";
conn.execute(format!("SELECT * FROM users WHERE id = {id}")).await?;
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//          expected `&str` or tuple, found `String`

要让后者编译过,用户必须显式转换 &format!(...)format!(...).as_str()——这一步多出的打字让"字符串拼 SQL"这个危险操作留下可 grep 的痕迹。审计代码的人 grep 一下 \.as_str\(\).*execute&format!.*execute,就能发现所有潜在注入点。

相比之下,如果 String 直接是 Execute,execute(format!(...)) 就零摩擦——类型系统帮不了你。这一行注释背后的判断是:trait 实现集合是 API 契约的一部分,能用类型系统传达"不鼓励这样用"就不要留方便的 overload。这是 Rust 生态里 trait 设计的一个经典安全案例。

当然,如果你真的有必要动态拼 SQL(例如 "这 N 个 UUID 用 IN 查询"),sqlx 提供了 QueryBuilder——第 10 章详细。QueryBuilder 内部有 pushpush_bind——前者拼裸 SQL(由你保证安全),后者追加参数槽(注入安全)。

4.6 为什么 &mut Transaction&mut PoolConnection 不是 Executor

这是第 1 章"让步二·附"的承诺——现在从 trait 边界视角把这件事讲清楚。

sqlx-core/src/pool/executor.rs:72-142 有一整段被注释掉的 impl Executor for &mut PoolConnection,注释的开头就是原因:

rust
// Causes an overflow when evaluating `&mut DB::Connection: Executor`.

sqlx-core/src/transaction.rs:132-200 同样有一整段被注释的 impl Executor for &mut Transaction,注释的开头:

rust
// NOTE: fails to compile due to lack of lazy normalization

这两个注释记录了 0.7→0.8 这次破坏性变更的真正技术动因——不只是"coherence 冲突"这种表述,而是 Rust 编译器的两个具体限制。

4.6.1 问题一:trait solver overflow

第一个问题出在 PoolConnection 上。原来的 impl(注释前的形态)是这样的:

rust
impl<'c, DB: Database> Executor<'c> for &'c mut PoolConnection<DB>
where
    &'c mut DB::Connection: Executor<'c, Database = DB>,
{
    type Database = DB;
    fn fetch_many<'e, 'q: 'e, E: 'q>(self, query: E) -> ... {
        (**self).fetch_many(query)  // DerefMut 到 DB::Connection,再调 fetch_many
    }
    // ...
}

问题是:当 Rust trait solver 尝试解 &mut PoolConnection: Executor 时:

  1. 看 impl,得到约束 &mut DB::Connection: Executor
  2. 为了验证 &mut DB::Connection: Executor,trait solver 再次查找 impl——找到 impl Executor for &mut PgConnection(具体类型)。
  3. 但它看见上面这条 impl Executor for &mut PoolConnection 也可能匹配(如果 DB::Connection 被替换为 PoolConnection<X>)。
  4. 于是递归展开 &mut PoolConnection<X>: Executor 要求 &mut X::Connection: Executor……
  5. 解不出终点,触发 "overflow evaluating the requirement" 错误。

问题的根源是 DB::Connection 这个关联类型投影,编译器在 Pool Connection 这一层无法惰性规约——它必须把所有可能的 DB 实例都展开一遍才能知道到底是哪个具体 Connection 类型。这就是 "fails to compile due to lack of lazy normalization" 的含义:关联类型 Self::Database::Connection 在泛型上下文中不会被延迟规约(lazy normalize)到具体类型,必须立刻展开,但展开时可能触发无限递归

4.6.2 问题二:lazy normalization 的同类症状

Transaction 的问题是同一种——impl Executor for &mut Transaction<'c, DB> 要求 &mut DB::Connection: Executor。因为 Transaction 本身有 DerefMut<Target = DB::Connection>transaction.rs:224),直觉上 &mut tx 应该自动 coerce 成 &mut DB::Connection。但 auto-deref coercion 对 impl Executor 的 trait bound 不生效——Rust 的 trait solver 不会在 "匹配 Executor impl" 这一步做 Deref。

开发者的 workaround 是把 DerefMut 展开留给用户:

rust
// 用户显式写这一步 deref
sqlx::query(...).fetch_one(&mut *tx).await?;

&mut *tx 明确告诉编译器:"我先 deref 到 Transaction 里面的 DB::Connection,再 &mut 它"——这样编译器看到的类型直接是 &mut PgConnection,匹配具体的 impl Executor for &mut PgConnection,无 overflow 无 normalization 问题。

4.6.3 设计教训

这次变更的背后有一条普适教训:trait 家族的可组合性受 Rust 编译器的具体能力限制。你在白板上设计的 trait 层级可能完全合理,但落到 rustc 的 trait solver 上会遇到现实问题——lazy normalization(关联类型的按需规约)、coherence(impl 冲突)、higher-ranked subtyping(HRTB 下的子类型)都可能让一个逻辑上"应该行"的 impl 根本编译不过。

sqlx 团队选择的做法是不对抗编译器,让用户写一次 *——这是最小代价的务实解。这和第 2 章 §2.3 讨论的"proc-macro 物理约束"一样——都是把语言层面的限制诚实地暴露给用户,而不是在内部堆补丁。

0.9.0-alpha.1 的一项传闻改动正是"重写 Executor trait"——可能针对这两个问题做更彻底的修复(比如换成 AsyncFn、等 Rust 的 next_solver 功能稳定)。但 0.8 时代我们只能接受 &mut *tx 这个妥协。

4.6.4 Executor trait 的版本演进

Executor trait 放到版本演进里看,能理解每次改动的因果:

  • 0.4 到 0.5Executor 从一个"有 executefetch_allfetch_one 的大 trait"瘦身成 fetch_many 为原语的精炼 trait。这次 refactor 让驱动作者少写大量重复代码(以前每个驱动要同时实现 executefetch,很多逻辑冗余)。
  • 0.5 到 0.6Executor trait 被挪到 sqlx-core(第 2 章 §2.7.1 里讲过 workspace 演进),接口本身没大改。
  • 0.6 到 0.7:带来了 GAT——fetch_many 签名里的 'q 生命周期约束变得比 for<'q> Hxxx<'q> 时代清爽得多。同时也是这次把驱动 crate 独立发布,&mut DB::Connection: Executor 的 impl 从 sqlx-core 挪到每个驱动 crate(以前在 core 里,现在在 sqlx-postgres 等)。
  • 0.7 到 0.8(本书锁定)&mut Transaction&mut PoolConnection 的 blanket impl 被删除。理由上面 §4.6.1-4.6.3 讲完了——lack of lazy normalization + overflow 两个编译器限制。
  • 0.9.0-alpha.1:传闻中 Executor trait 会被进一步改造,可能利用 Rust 更新的 AFIT(async fn in traits)和 RPITIT 特性,让 BoxStream 变成 impl Stream + Send,少一次堆分配。

这条演进里反复出现的主题是"实现成本 vs 用户友好性"的权衡——0.5 的 refactor 是为了降低驱动作者的实现成本,0.7 的 GAT 是为了让上层代码清爽,0.8 的删除是为了解决编译器限制。每次改动都有取舍,每次取舍都留下一个可追溯的源码印记(比如 executor.rs:22-30 的说明注释就是 0.8 那次改动留下的)。

读懂这条演进线的价值是:当你遇到 sqlx 某一处看起来"绕"的设计,你应该能把它归因到某一次演进——而不是觉得作者当初就这么想的。

4.7 prepare / describe:宏系统的私有接口

两个方法讨论得较少,是因为用户几乎不直接调它们:

rust
fn prepare<'e, 'q: 'e>(self, query: &'q str)
    -> BoxFuture<'e, Result<DB::Statement<'q>, Error>>
where 'c: 'e
{
    self.prepare_with(query, &[])
}

fn prepare_with<'e, 'q: 'e>(self, sql: &'q str,
    parameters: &'e [DB::TypeInfo])
    -> BoxFuture<'e, Result<DB::Statement<'q>, Error>>
where 'c: 'e;

#[doc(hidden)]
fn describe<'e, 'q: 'e>(self, sql: &'q str)
    -> BoxFuture<'e, Result<Describe<DB>, Error>>
where 'c: 'e;

prepare 的意义有两条:

  1. 让你在执行前检查一个查询的元数据——conn.prepare("SELECT ...").await?.columns() 能拿到列名和列类型。这对生成动态 UI(表头)或迁移校验很有用。
  2. prepare_with 接收 parameters: &[TypeInfo] —— Postgres 能利用这些信息做参数类型推断(文档说"Only some database drivers (PostgreSQL, MSSQL) can take advantage of this extra information")。SQLite / MySQL 忽略。

describe宏的私有通道#[doc(hidden)] 意味着文档站看不到、不是公开 API。它的用户只有一个:sqlx-macros-coreCachingDescribeBlocking<DB>(第 2 章 §2.3.1 讨论过)——编译期把 SQL 字符串送进 describe,拿回 Describe<DB>(包含参数 OIDs 和列 TypeInfos),再用这些信息生成 try_get_unchecked::<i32, _>(0) 这种带类型的代码。

Describe<DB> 结构sqlx-core/src/describe.rs)简单:

rust
pub struct Describe<DB: Database> {
    pub columns: Vec<DB::Column>,
    pub parameters: Option<Either<Vec<DB::TypeInfo>, usize>>,
    pub nullable: Vec<Option<bool>>,
}

parametersEither<Vec<TypeInfo>, usize> 正是第 3 章 §3.5.3 讨论过的"Postgres 给完整类型 / SQLite 只给个数"的差异,在 Describe 里以统一形态表达。nullableVec<Option<bool>>——每一列是否可空,None 表示驱动也说不准(比如表达式列)。

这三个字段就是编译期类型检查的全部信息源——query!("SELECT id, name FROM users WHERE id = $1", id) 的类型检查只看 describe 的返回。第 11 章会详细讲这条编译期校验路径。

4.7.1 describe 一次实际走过的路径

用一次具体 query! 展开来理解 describe 怎么被宏消费。假设用户代码是:

rust
sqlx::query!("SELECT id, name, email FROM users WHERE id = $1", user_id)

编译期发生的事:

  1. sqlx-macrosexpand_query 入口函数(sqlx-macros-0.8.6/src/lib.rs:10)把 TokenStream 交给 sqlx-macros-core::query::expand_input
  2. expand_input 解析出 SQL 字符串 "SELECT id, name, email FROM users WHERE id = $1" 和参数表达式 user_id
  3. 它根据 DATABASE_URL 环境变量决定去哪个驱动:postgres://localhost/test 就选 QueryDriver::new::<Postgres>()(来自 FOSS_DRIVERS,第 2 章 §2.3.1)。
  4. 调用 Postgres::describe_blocking(sql, database_url)——这是 DatabaseExt trait 的方法,内部用 sqlx-macros-coreblock_on(第 2.5.1 节)跑一个 Tokio 当前线程 runtime。
  5. block_on 里的逻辑大致是:let conn = PgConnection::connect(url).await?; conn.describe(sql).await? ——调的正是本章讨论的 Executor::describe
  6. Postgres 驱动的 describe 实现发 Parse + Describe 消息给服务端,拿回 ParameterDescription(参数 OIDs)和 RowDescription(列信息)。
  7. 返回的 Describe<Postgres> 含:
    • columns: Vec<PgColumn>,三列:id (OID 23 = INT4)、name (OID 25 = TEXT)、email (OID 25 = TEXT)。
    • parameters: Some(Left(vec![PgTypeInfo::INT4]))——一个参数,期望 INT4。
    • nullable: vec![Some(false), Some(false), Some(true)]——从 pg_catalog 查出的可空性。
  8. expand_input 拿着 Describe 做两件事:
    • 验证 user_id 的 Rust 类型能 EncodePgTypeInfo::INT4(第 5 章详细)。
    • 根据 columns + nullable 生成出参结构({id: i32, name: String, email: Option<String>})。
  9. 最终生成的 TokenStream 就是第 1 章 §1.5.1 讨论过的那段带 try_get_unchecked::<i32, _>(0) 的展开。

整条路径唯一和 Executor trait 直接接触的是步骤 5 的 conn.describe(sql).await?——这是宏系统看 sqlx 的"窥视孔"。#[doc(hidden)] 这个标注不是简单为了"不想暴露"——它是明确告诉用户"这不是给你调的 API,是宏内部用的"。公共 API 里 fetch_* 系列永远够用,describe 是保留字段。

4.8 生命周期约束 'c: 'e, 'q: 'e

Executor trait 的每个方法签名都有两条生命周期约束,值得单独讲。以 fetch_many 为例:

rust
fn fetch_many<'e, 'q: 'e, E>(self, query: E)
    -> BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>;

四个生命周期:

  • 'c——Executor 自身的借用(&'c pool&'c mut conn)。来自 Executor<'c>
  • 'e——返回的 stream/future 的生命周期。
  • 'q——查询(SQL 字符串 + 参数)自身的生命周期。
  • E: 'q——E 的实现至少活到 'q(即 E 里的任何引用都比 'q 长)。

约束 'c: 'eexecutor 的借用至少活到 stream 结束——这是必须的,因为 stream 内部要持有 executor(比如 Pool 的 clone、Connection 的 &mut self)。如果 executor 先 drop,stream 的 poll_next 就变成悬垂引用。

约束 'q: 'equery 至少活到 stream 结束——Postgres 协议在 Parse 阶段就把 SQL 字符串发给服务端,之后 SQL 本身可以丢弃,但 sqlx 保留了 borrow 链一路到 stream 完成。这是保守的约束,实际上对大多数驱动可以更宽松(比如 MySQL simple query 是一次性发的)。

这些约束组合起来解决了"异步借用合法性"这个 Rust 的常见问题——返回的 Future/Stream 不能比它借用的东西(executor、query)更长命。Rust 1.85 之前这类约束必须显式写出来(RPIT lifetime capture rules);1.85 之后部分场景可以依赖 use<> 推导,但 sqlx 为了 MSRV 兼容一直保留显式标注。

4.8.1 一次具体的生命周期错误

想象用户写了一个返回 stream 的 helper:

rust
fn query_users(pool: &PgPool) -> BoxStream<Result<PgRow>> {
    let sql = format!("SELECT * FROM users");     // String,活到这行结束
    pool.fetch(&*sql)                             // 返回 stream
}                                                 // 这里 sql 被 drop 了!

编译器的报错大致是:

error[E0597]: `sql` does not live long enough
   --> src/main.rs:4:18
    |
  3 |     let sql = format!("SELECT * FROM users");
    |         --- binding `sql` declared here
  4 |     pool.fetch(&*sql)
    |     -----------^^^^^-
    |     |          |
    |     |          borrowed value does not live long enough
    |     argument requires that `sql` is borrowed for `'q`
    |     where `'q: 'e`
  5 | }
    | - `sql` dropped here while still borrowed

错误的根因是 fetch 签名 'q: 'e——stream 的生命周期 'e 至少要和 'q(SQL 字符串的借用)一样短。但 sql 是局部变量,&*sql 的生命周期不能延伸到函数外。

修法是把 String 的所有权移入 stream

rust
fn query_users(pool: &PgPool) -> BoxStream<'_, Result<PgRow>> {
    let sql = "SELECT * FROM users";   // &'static str
    pool.fetch(sql)
}

或者用 QueryBuilder 把动态字符串移入 Query 对象(Query 拥有 String 的所有权,生命周期就和 stream 绑定):

rust
fn query_users_dyn(pool: &PgPool, table: &str) -> BoxStream<'_, Result<PgRow>> {
    let mut qb = QueryBuilder::<Postgres>::new("SELECT * FROM ");
    qb.push(table);           // 字符串被 move 进 QueryBuilder
    qb.build().fetch(pool)
}

这类错误是 sqlx 新手最常遇到的一种。读懂 'c: 'e, 'q: 'e 的含义,就能把错误原因直接翻译成"SQL 字符串的所有权没有正确传递给 stream"。

4.9 Executor 实现者清单的可视化

把本章讨论的所有实现关系画一张图:

几个观察:

  1. 四种用户写法里,三种(&mut conn / &mut *tx / &mut pool_conn)最终都落到驱动自己的 Executor impl——这是 DerefMut 的功劳。
  2. 没有一条线落到已删除的两个 impl——从用户视角看,这两个 impl 的缺席是隐形的,因为 &mut *tx 直接到达 &mut PgConnection
  3. &pool 是唯一走 pool/executor.rs 的路径——Pool 自己实现 Executor 是在 sqlx-core,连接管理的逻辑在那里集中。

这张图总结了本章的拓扑——六种用户入口、六个 Executor impl、两个被删除的 impl 靠 DerefMut 绕过。

4.10 跨章关联:Executor 与 tower::Service

读完 Executor trait,值得把它和另一个 trait 家族放一起对照——tower 的 Service trait(《Hyper 与 Tower:工业级 HTTP 栈》第 2 章):

rust
// tower::Service
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}

// sqlx::Executor(简化)
pub trait Executor<'c>: Send + Debug + Sized {
    type Database: Database;
    fn fetch_many<E: Execute<DB>>(self, query: E) -> BoxStream<...>;
    fn fetch_optional<E: Execute<DB>>(self, query: E) -> BoxFuture<...>;
    // 其他默认方法
}

两者在设计意图上高度相似——都抽象"能处理某类请求的东西":

  • tower 的 Service 抽象"能处理 HTTP 请求的东西"——HTTP handler、中间件、客户端 backends 都实现它。
  • sqlx 的 Executor 抽象"能跑 SQL 查询的东西"——Pool、Connection 都实现它。

但设计上三处关键差异:

维度tower::Servicesqlx::Executor
输入任意 Request 类型参数Execute<'q, DB> trait bound
输出关联 FutureBoxFuture + BoxStream
背压poll_ready 显式拉取无(假设连接池提供背压)
方法两个(poll_ready + call四个必实现 + 六个默认方法

最大的差异是 tower 的 poll_ready 显式背压——每次 call 前先 poll_ready 检查是否 ready,Poll::Pending 表示"我现在不处理新请求,请稍后"。sqlx 的 Executor 没有等价机制——背压由 Pool::acquire 的 Semaphore 隐式提供(第 14 章会看)。

这条差异的根因是工作负载模式不同

  • tower 的 Service 可能是一个下游 HTTP 后端,接一百个并发请求时需要显式告诉上游"慢一点"。背压是网络层面的必需品。
  • sqlx 的 Executor 要么是 Pool(背压在 Semaphore 层),要么是 Connection(独占,天然串行)——没有中间的"还能接 N 个但超了要拒绝"的状态。

换句话说:Service 抽象分布式系统的节点、Executor 抽象本地数据库访问——设计的侧重点决定了 trait 的形态。这个对照不是说"哪个设计更好",而是让你看到 trait 设计随"场景约束"的变形。第 10 章 QueryBuilder 还会遇到类似对照——sqlx 的 QueryBuilder vs diesel 的 DSL vs SQL 原生字符串,同样是"场景约束"决定"抽象形态"。

4.10.1 一个 sqlx migrate run 的 fetch_many 走查

把上面讨论的所有机制合起来,看 sqlx migrate run 一次迁移执行时 Executor 链路长什么样。假设 migrations/20240424120000_add_index.sql 内容是:

sql
-- 20240424120000_add_index.sql
ALTER TABLE users ADD COLUMN last_seen TIMESTAMPTZ;
CREATE INDEX idx_users_last_seen ON users (last_seen);
UPDATE users SET last_seen = created_at WHERE last_seen IS NULL;

sqlx-cli 的 migrate.rs 大致这样执行:

rust
let raw_sql = fs::read_to_string(path)?;
let mut tx = pool.begin().await?;
let results: Vec<AnyQueryResult> = (&mut *tx)
    .execute_many(&*raw_sql)
    .try_collect::<Vec<_>>()
    .await?;
// 每个 AnyQueryResult 对应一条 ALTER/CREATE/UPDATE 的 rows_affected
tx.commit().await?;

这条链路里触到本章每个概念:

  1. &mut *tx —— DerefMut 到 &mut AnyConnection(§4.4 提到 Any 也实现 Executor)
  2. execute_many —— 默认方法,内部调 fetch_many 过滤 Right(§4.3)
  3. fetch_many(&*raw_sql) —— &str 作为 Execute 实现(§4.5)
  4. 服务端顺序执行三条语句,每条返回一个 Either::Left(QueryResult),Either::Right 流里没有(全是 DDL/UPDATE 不返回行)
  5. try_collect 把三个 QueryResult 收到 Vec

如果其中一条失败(比如 CREATE INDEX 遇到已有同名索引),Either 流会 yield Err(Error::Database(...))try_collect 立刻短路、剩下的 SQL 不执行。事务 drop 时 start_rollback 被触发(§第 1.6 节 Transaction Drop 讨论过)——整次迁移全部回滚。

这个场景是 execute_manyexecute 有价值的地方:你能拿到每一条 DDL 的影响行数,用来给 DBA 看"ALTER 改了 0 行(预期)、UPDATE 改了 1.2 万行(和线上数据量一致)"。如果自己想做 "迁移预演" 工具—— execute_many + 事务 rollback 的组合能精确报告每条 SQL 的影响、而 schema 不落地。

4.10.2 实现 Executor 的三个易错点

如果你要给某个第三方数据库(例如 ClickHouse)写驱动,实现 Executor 的时候最容易踩三个坑:

坑 1:fetch_many 不应该 eagerly 拉取所有行fetch_many 返回 Stream,语义是"按需拉取"——用户用 try_next() 逐个消费。如果你的实现内部 while let Some(row) = stream.next().await? { collected.push(row); } return collected;,相当于把异步流变成同步 Vec,失去了背压和早停能力。正确实现是 Box::pin(try_stream! { ... }),每拉到一行立即 r#yield!(Right(row))

坑 2:Either::Left 和 Right 的顺序不能随便fetch_one 依赖的语义是"Right 先于 Left"——数据行先出,最后一个 Either::Left 才标志查询完成。如果你的驱动颠倒顺序(先发 QueryResult 再发 rows),fetch_one 会收到一个 Left 后 try_filter_mapOk(None),最终 fetch_optional 返回 None,用户看到 Error::RowNotFound ——诡异的 bug。

坑 3:prepare_with 返回的 Statement 生命周期必须和参数 sql 绑定。签名是 Statement<'q> where 'q 是 sql 的生命周期——你不能 Box::leakclone 让它变 'static(除非你确定 Statement 内部拷贝了 sql 字符串)。否则上层代码的 &'q str 可能在 Statement 还存活时被 drop。

这三个坑大多数驱动作者第一次实现都会踩——sqlx-core 的集成测试里针对每条都有对应的测试用例。如果你要写新驱动,先读一遍 tests/any/ 目录下的测试,它们是 Executor 契约的活 spec。

4.11 本章小结

本章把 Executor trait 拆成了它的三条设计主干:

  1. 双原语 fetch_many + fetch_optional(§4.3)——前者用 BoxStream<Either<QueryResult, Row>> 表达"多条 SQL 的交替结果"、后者给驱动留 "最多一行" 的协议级优化口(Postgres Bind 的 max_rows=1 能省服务端内存)。高层方法 execute / fetch / fetch_all 基于 fetch_many 默认实现、fetch_one 基于 fetch_optional。这种"两个原语覆盖所有读路径"让驱动作者只需实现两条 + 两条 meta(prepare_with / describe)= 共 4 个必实现方法。
  2. 实现者极少(§4.4)——只有 &Pool<DB>&mut DB::Connection(以及 Any、PgListener 两个特殊路径)。Pool::fetch_many 内部 pool.acquire().await? 借连接、Connection 的 Executor impl 调用具体驱动的协议交互。
  3. Execute<'q, DB> 是 query shape 抽象(§4.5)——&str(&str, Option<Arguments>)Query<DB, A> 各自实现。刻意不为 String 实现 是类型系统对 SQL 注入的间接防御(§4.5.1)。

两条关于 0.7→0.8 的破坏性变更:

  1. &mut Transaction&mut PoolConnection 的 Executor impl 被删除(§4.6)——不只是"coherence 冲突"那么简单,是 **"trait solver overflow"(PoolConnection)**和 **"lack of lazy normalization"(Transaction)**两个 Rust 编译器的具体限制。workaround 是用户手写 &mut *tx——把 Deref 展开的责任推给用户而不是编译器。
  2. 设计教训:trait 家族的可组合性受 rustc 具体能力约束——白板上合理的设计可能落到编译器上不成立。sqlx 选择"诚实暴露限制、让用户写一次 *"而不是内部堆补丁。

两个配套的宏系统接口:

  1. prepare / prepare_with(§4.7)——暴露 Statement 元数据给用户;Postgres 独享参数类型影响。
  2. describe(§4.7,#[doc(hidden)])——query! 宏的私有通道,编译期拿 Describe<DB>(columns / parameters / nullable)做类型推断。

一条 trait 级生命周期技巧:

  1. 'c: 'e, 'q: 'e(§4.8)——executor 和 query 都要至少活到 stream 结束,这是所有异步 trait 返回的普适生命周期约束。

下一章(第 5 章)我们进入类型映射三位一体——Encode / Decode / Type 三个 trait 如何共同表达"Rust 类型 ↔ 数据库类型"的双向映射,以及 Postgres 的 OID 查表、MySQL 的字符集、SQLite 的动态类型三家如何各自落地这套抽象。

4.12 对本章的设计审查

回头审视 Executor trait 的设计,从"如果你是代码评审员"视角做几点判断:

做得好的地方:

  1. 原语 fetch_many 足够小——一个流原语表达了所有读路径,驱动作者只需正确实现一个方法(加 fetch_optional 这个优化口),其他七个自动获得。
  2. Either 类型选择恰当——不用自己的枚举、直接用生态里成熟的 either::Either,避免了重复的 From/TryFrom 样板。这也和 Statement::parameters 返回 Either<&[TypeInfo], usize> 形成风格一致。
  3. 不为 String 实现 Execute——这种"类型系统级的拒绝"传递的是 API 文档写十遍都传不出的设计意图:注入风险代码应当显眼。
  4. describe#[doc(hidden)]——清晰地把"用户 API"和"宏内部 API"分开,文档站不出现 describe,但 sqlx-cli 和 sqlx-macros-core 能用。

有争议的地方:

  1. BoxStream / BoxFuture 的堆分配——每次 .fetch_one(...) 都要 Box::pin 一次,对微基准测试可观测。0.9.0-alpha 试图改成 impl Stream + Send 消除这一层,但还没稳定。
  2. where 'c: 'e 这种生命周期约束——对新手友好度不高;错误消息拉长。但替代方案(用 GAT 声明返回类型)会让 trait 更难实现。
  3. &mut *tx 的手动 DerefMut——这是 §4.6 讨论过的 Rust 编译器限制的妥协。每个 sqlx 用户都要学一次,文档里也要反复解释。

不该做的地方(反面素材):

  1. 如果 sqlx 给 Pool 和每个 Connection 类型都单独定义一套 fetch_one / fetch_all 方法(而不用 Executor trait),代码会重复 N 遍且每次驱动增加方法要同时改 Pool 侧——这就是没有 trait 时的世界。
  2. 如果把 fetch_many 的 Either 流改成 "先返回 Vec<Row> 再返回 QueryResult" 两段式 API,用户拿不到交替顺序,sqlx migrate run 这种"按 DDL 顺序执行并收集"的场景就无法实现。

这些判断不是"绝对标准答案"——它们是把本章的具体设计细节提升到"Rust trait 家族设计通用原则"层面的一次归纳。理解了这些,你在自己的项目里写 trait 的时候能更自觉地问"我这里到底是 impl 的负担 vs 用户的负担、我的 trait 对新手友好吗、哪些是编译器限制而不是我的选择"——这才是"读源码学设计"的真正价值。

基于 VitePress 构建