SQLx 源码深度解析

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

作者 杨艺韬 · 11,605 字 · 发布于 · 更新于

Executor<'c> 只要求驱动实现四个方法,另外六个是默认实现。「能跑查询的东西」这件事,最后被收束成两个流式原语。

本章要点

  • 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_many;fetch_one 基于 fetch_optional(未取到行时返 RowNotFound);execute 基于 execute_many;prepare 基于 prepare_with。

  • 实现 Executor 的类型是一张很短的清单:&Pool<DB>(sqlx-core/src/pool/executor.rs:12)、三家驱动各自的 &mut XxxConnection,外加 &mut AnyConnection 与 &mut PgListener 两个特例——业务代码里几乎只会碰到前两类。

  • &mut Transaction 和 &mut PoolConnection 在 0.7.0(#2039)被从 Executor impl 列表里删掉,理由不是笼统的 coherence——源码里两段被注释掉的 impl 各自写明了具体的编译器限制:

    • sqlx-core/src/transaction.rs:132——"NOTE: fails to compile due to lack of lazy normalization"
    • sqlx-core/src/pool/executor.rs:72——"Causes an overflow when evaluating &mut DB::Connection: Executor"

    两个都是 Rust trait 求解器的具体限制,孤儿规则则挡死了"让各驱动自己写"这条退路。

  • Execute<'q, DB> trait 是 query shape 的抽象——&str、(&str, Option<Arguments>)、Query<DB, A> 各自实现它。为什么不为 String 实现?刻意留的防御层(§4.5)。

  • describe 是 query! 宏的私有接口——标注 #[doc(hidden)],专为编译期类型检查设计;prepare 则是公开的元数据检查 API。

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

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

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

这三个"能跑查询的东西"形态不同:Pool 是 Arc<PoolInner>,Connection 是独占的一条数据库连接,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-184,省略文档注释:

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
}

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

graph TD
    subgraph 必实现
      FM["fetch_many → Stream&lt;Either&lt;QueryResult, Row&gt;&gt;"]
      FO[fetch_optional → Future&lt;Option&lt;Row&gt;&gt;]
      PW[prepare_with]
      DS[describe #hidden]
    end
    subgraph 默认方法
      E[execute]
      EM[execute_many]
      F[fetch]
      FA[fetch_all]
      F1[fetch_one]
      P[prepare]
    end
    FM -->|过滤 Right=None, 收集 Left| EM
    EM -->|try_collect: 累加 QueryResult| E
    FM -->|过滤 Left=None, 保留 Right| F
    F -->|try_collect: 成 Vec| FA
    FO -->|unwrap or Err::RowNotFound| F1
    PW -->|prepare_with &#91;&#93;| P

这张图揭示了 sqlx Executor 最重要的设计判断:fetch_many 是读路径的主干原语。除了建在 fetch_optional 上的 fetch_one,所有"读行"类操作都建立在它之上。

4.3 fetch_many 与 fetch_optional 为什么是两个原语

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

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 调用可能包含多条语句:

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

Postgres 的 simple query 协议、MySQL 的 COM_QUERY 文本协议(sqlx 握手时声明了 MULTI_STATEMENTS 能力,sqlx-mysql/src/connection/stream.rs:50)都允许把这一整段文本一次发过去——服务端会按分号切分、顺序执行、每条语句返回自己的结果。第一条是 UPDATE,返回一个 "UPDATE 42" 命令完成消息(对应 QueryResult { rows_affected: 42 });第二条是 SELECT,返回 0..N 行数据 + 最后一个 "SELECT N" 完成消息。

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

fetch(sqlx-core/src/executor.rs:68-84)——只要行,丢掉 QueryResult:

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_many(sqlx-core/src/executor.rs:49-65)——只要 QueryResult,丢掉行:

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()
}

execute(sqlx-core/src/executor.rs:37-46)——execute_many.try_collect(),把所有 QueryResult 累加成一个:

    fn execute<'e, 'q: 'e, E>(
        self,
        query: E,
    ) -> BoxFuture<'e, Result<<Self::Database as Database>::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_all(sqlx-core/src/executor.rs:103-112)——fetch.try_collect(),把所有 Row 收集进 Vec:

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

fetch_one(sqlx-core/src/executor.rs:115-129)——fetch_optional + 空值报错:

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()
}

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

为什么 fetch_optional 也要做成必实现?因为"最多一行"这件事各家的最优做法不一样,core 无法替驱动决定。看 0.8.6 三家的实现就一目了然:

  • MySQL(sqlx-mysql/src/connection/executor.rs:280):老老实实包 fetch_many,拿到第一个 Either::Right 就 return Ok(Some(r)) 提前结束。
  • SQLite(sqlx-sqlite/src/connection/executor.rs:61):把 Some(1) 作为 limit 参数传进 worker 的 execute——真正的协议(这里是 C API)级早停。
  • Postgres(sqlx-postgres/src/connection/executor.rs:412):反而故意把整条流读完,只留第一行。源码注释(同文件 :430-433)写明了原因:"With deferred constraints we need to check all responses as we could get a OK response (with uncommitted data), only to get an error response after"——延迟约束的错误在后面的响应里,提前 return 会漏掉。

三种截然不同的取舍落在同一个方法上,这就是把 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:多条语句用分号拼接

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-postgres/src/migrate.rs:279 的 execute_migration 直接 conn.execute(&*migration.sql),把整个迁移文件当一段多语句 SQL 发出去、用 _ 丢掉合并后的 QueryResult,然后再单独 INSERT 一行到 _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_map、try_collect)都围绕它构建。sqlx 的读路径除了 0.3 那一版用过自定义的 Cursor trait(见 §4.6.4),从 0.1 到 0.8 都是 Stream,没理由切换到一个还在 nightly 的 trait。

BoxStream 而不是 impl Stream——这是异步 trait 方法的历史代价。这套签名从 0.4.0 就是这个形状(git show v0.4.0:sqlx-core/src/executor.rs 里的 fetch_many 已经返回 BoxStream),那时候 trait 里根本写不出返回 impl Trait 的方法。Box::pin 强制一次堆分配,换来的是签名里生命周期关系一目了然、也不依赖任何编译器新特性。

4.4 谁实现 Executor?一张很短的清单

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

  1. &Pool<DB>——sqlx-core/src/pool/executor.rs:12
  2. &mut PgConnection / &mut MySqlConnection / &mut SqliteConnection——分别在 sqlx-postgres/src/connection/executor.rs:380、sqlx-mysql/src/connection/executor.rs:251、sqlx-sqlite/src/connection/executor.rs:13
  3. &mut AnyConnection——sqlx-core/src/any/connection/executor.rs:11
  4. &mut PgListener——sqlx-postgres/src/listener.rs:388,用于 Postgres LISTEN/NOTIFY 场景

以上是全部(Pool 一个、三家驱动各一个,加上 Any 和 PgListener 两个特例,一共六个 impl)。用户代码看到的所有 .fetch_one(X) 里的 X,最终都走这几条路径之一——业务代码里几乎只会遇到前两条。

4.4.1 &Pool<DB> 的实现

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

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),而那个 conn 是 PoolConnection<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 的宏,把 "生产多个值的异步代码" 包装成一个 Stream。r#yield!(v) 把值送到 stream 的 poll_next。
  4. 连接归还:let mut conn = pool.acquire().await?; 拿到 PoolConnection——它在 stream 结束(无论正常还是错误)时被 drop,drop 实现里归还连接给 pool。这就是为什么 .fetch_one(&pool) 用起来这么丝滑——连接的获取和归还全被 Executor impl 包掉了。

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

既然只有 &Pool<DB> 和 &mut DB::Connection 两种 Executor,用户代码里会看到四种实际写法(conn 指直接 XxxConnection::connect 建立的连接,pool_conn 指 pool.acquire() 借出的 PoolConnection):

写法 类型(实际) 适用场景 连接归属
fetch_one(&pool) &Pool<DB> 单条独立查询,无跨查询一致性需求 每次 acquire + 立即归还
fetch_one(&mut conn) &mut DB::Connection 不经过 pool 的单条连接 独占,drop 时直接断开(不像 close().await 那样先发退出消息)
fetch_one(&mut *pool_conn) &mut DB::Connection(经 DerefMut) 已经从 pool 借出,要复用一段时间 持续占用,drop 时归还
fetch_one(&mut *tx) &mut DB::Connection(经 DerefMut) 事务中查询 同事务内

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

注意第三行的 * 不能省:&mut PoolConnection 从 0.7 起不再是 Executor,直接写 fetch_one(&mut pool_conn) 会报 E0277(`&mut PoolConnection<Sqlite>: Executor<'_>` is not satisfied,0.8.6 实测),原因见 §4.6。

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

经验法则:

  • 单条查询 + 不在循环里:&pool。
  • 同一逻辑单元里有多条查询: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 是真正干活的地方:

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::run(sqlx-postgres/src/connection/executor.rs 的底部方法)——这是整个 Postgres 协议交互的入口,也是第 16 章的主题。

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

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

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

PgListener 是 Postgres 独有的 LISTEN / NOTIFY 事件订阅者——用户调 listener.listen("channel_name").await? 注册感兴趣的 channel,然后 .recv().await? 等通知。它内部持有一条从 Pool 借出的连接(connection: Option<PoolConnection<Postgres>>,sqlx-postgres/src/listener.rs:30),但不直接暴露——如果把 PgListener 当成普通 PgConnection 用,会把 LISTEN 状态搞坏。

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

Listener 的 Executor impl 每个方法都先调私有的 self.connection()(sqlx-postgres/src/listener.rs:189):连接已断就先从 pool 重新借一条、把已订阅的 channel 全部重新 LISTEN(connect_if_needed,同文件 :173-186),再把查询委托给这条连接。这是"特殊情况特殊处理"的一个案例——显示 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 的抽象(sqlx-core/src/executor.rs:193-211):

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——参数。返回 Ok(None)(如裸 &str)表示走 simple query、不做预处理;返回 Some(哪怕是空参数,query(sql) 不 bind 也是这样)就会先 prepare 再执行。
  4. persistent——是否缓存 prepared statement 到连接。

这个 trait 有两个内置实现(同一个文件 215-235 与 237-257 行):

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——形态上就是"SQL + 参数"。不过 query(sql).bind(x) 那条链并不经过它,Query 自己实现了 Execute。

其余实现都在查询类型上:Query<'q, DB, A> 自己(sqlx-core/src/query.rs:41),我们在第 9 章详细看。

此外 Map、QueryAs、QueryScalar 以及 raw_sql() 返回的 RawSql 也各自实现了 Execute。

4.5.1 为什么不给 String 实现 Execute?

sqlx-core/src/executor.rs:213-214——就写在 impl Execute for &'q str 的正上方——有两行意味深长的注释:

// 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 级拒绝。考虑下面这两种写法:

// 类型系统不拦:
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?;
// error[E0277]: the trait bound `std::string::String: sqlx::Execute<'_, Sqlite>` is not satisfied
//   = help: for that trait implementation, expected `&str`, found `std::string::String`
//(0.8.6 + SQLite 实测;Postgres 下只是 DB 类型不同)

要让后者编译过,用户必须显式转成 &str:format!(...).as_str() 或 &*format!(...)(&format!(...) 不行——&String 同样没有实现 Execute,实测 E0277)。这一步多出的打字让"字符串拼 SQL"这个危险操作留下可 grep 的痕迹,审计代码的人搜 .as_str() 和 &*format! 就能顺藤摸到潜在注入点。

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

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

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

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

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

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

sqlx-core/src/transaction.rs:132-200 同样有一整段被注释的 impl Executor for &mut Transaction(注释块从 132 行一直排到 200 行的 // }),开头这一行就是理由:

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

这两个注释记录了 0.6→0.7(CHANGELOG 0.7.0,#2039)这次破坏性变更的真正技术动因——不只是"coherence 冲突"这种笼统表述,而是 Rust 编译器的两个具体限制。CHANGELOG 里对应的原话是:"These cannot be blanket impls as it triggers an overflow in the compiler due to the lack of lazy normalization, and the driver crates cannot provide their own impls due to the orphan rule."

4.6.1 问题一:trait solver overflow

先交代一段前史。0.6 及以前这两个 impl 并不是泛型的:sqlx-core 用 impl_executor_for_pool_connection! / impl_executor_for_transaction! 两个宏为每个具体数据库各生成一份,宏上方的注释是 // NOTE: required due to lack of lazy normalization(git show v0.6.0:sqlx-core/src/pool/executor.rs)——写不成泛型 impl 这件事 0.6 就存在,只是当时驱动都在 sqlx-core 里,宏可以逐个展开。0.7 把驱动拆成独立 crate 后,sqlx-core 不再认识 Postgres 这些具体类型,宏这条路断了;驱动 crate 自己写又撞孤儿规则(Executor、PoolConnection、Transaction 都属于 sqlx-core);剩下的泛型 blanket impl 则编译不过。

第一个问题出在 PoolConnection 上。0.7 尝试的 blanket impl(即那段被注释掉的代码)是这样的:

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
    }
    // ...
}

sqlx 的源码注释和 CHANGELOG 只给了结论、没有给推导。下面按 trait 求解过程做一种还原,便于理解,并非 sqlx 官方的解释。当 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 这个关联类型投影:编译器在泛型上下文里无法惰性规约它,不知道它到底是哪个具体 Connection 类型,只能沿着所有可能匹配的 impl 往下展开,于是可能无限递归。CHANGELOG 也是把两者连在一起说的:溢出(overflow)正是"缺少惰性规约(lack of lazy normalization)"的后果。

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

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

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

// 用户显式写这一步 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 物理约束"一样——都是把语言层面的限制诚实地暴露给用户,而不是在内部堆补丁。

CHANGELOG 0.7.0 里的原话是这两个 impl "cannot exist in the new crate architecture without rewriting the Executor trait entirely"——也就是说,彻底的修复要等到 Executor trait 本身被重写为止。本专栏锁定的 0.8.6 还没有走到那一步,我们只能接受 &mut *tx 这个妥协。

4.6.4 Executor trait 的版本演进

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

  • 0.3(2020-03):Executor 还没有 'c 生命周期参数,读路径靠一个单独的 HasCursor trait(git show v0.3.0:sqlx-core/src/executor.rs 开头就是 use crate::cursor::HasCursor;)。文档注释写着 impl 提供给 &Pool、&mut PoolConnection 和 &mut Connection 三种类型。
  • 0.4(2020-11):Executor<'c> 变成今天这个形状——Cursor 被删掉,fetch_many 成为流式读原语,execute / fetch / fetch_all / fetch_one 都改成默认方法。git show v0.4.0:sqlx-core/src/executor.rs 里的方法清单和 0.8.6 已经一模一样。这次 refactor 让驱动作者少写大量重复代码。
  • 0.5、0.6:Executor 接口本身几乎没动,改的是它周边——Done trait 被移除、DbDone 系列类型更名为 DbQueryResult(0.5,#976)、Pool 的一批签名调整(0.6)。
  • 0.7(2023-06):驱动 crate 独立发布,&mut DB::Connection: Executor 的 impl 从 sqlx-core 挪到各驱动 crate;同一次变更里,&mut Transaction 和 &mut PoolConnection 的 impl 被删除,理由就是上面 §4.6.1–4.6.3 的 lack of lazy normalization + overflow 两个编译器限制。
  • 0.8(2024-07,本专栏锁定):GAT 迁移(#2973)落到 Executor 上的表现是签名里的 <DB as HasStatement<'q>>::Statement 全部变成 DB::Statement<'q>;Execute::take_arguments 的返回类型也从 Option<..> 变成 Result<Option<..>, BoxDynError>(#3126 让 Encode 返回 Result 的连带影响)。

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

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

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

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

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-core 的 CachingDescribeBlocking<DB>(DatabaseExt::describe_blocking 见第 2 章 §2.3.1,连接缓存见第 11 章 §11.4.1)——编译期把 SQL 字符串送进 describe,拿回 Describe<DB>(包含参数 OIDs 和列 TypeInfos),再用这些信息生成 try_get_unchecked::<i32, _>(0) 这种带类型的代码。

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

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

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

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

4.7.1 describe 一次实际走过的路径

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

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

编译期发生的事:

  1. sqlx-macros 的 expand_query 入口函数(sqlx-macros/src/lib.rs:9)把 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-core 的 block_on(第 11 章 §11.4)跑一个 Tokio 当前线程 runtime。
  5. block_on 里的逻辑大致是:按 URL 从 CachingDescribeBlocking 的连接缓存里取一条连接(没有才 PgConnection::connect(url) 新建并放回缓存,sqlx-macros-core/src/database/mod.rs:32-62),再 conn.describe(sql).await?——调的正是本章讨论的 Executor::describe。(SQLite 例外:它不走这个缓存,每次 describe 都重新打开数据库,见第 11 章。)
  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 类型能 Encode 到 PgTypeInfo::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 为例:

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

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

这些约束组合起来解决了"异步借用合法性"这个 Rust 的常见问题——返回的 Future/Stream 不能比它借用的东西(executor、query)更长命。因为返回类型是 BoxStream<'e, _>(也就是 Pin<Box<dyn Stream + Send + 'e>>),'e 是写在类型里的显式参数,编译器不会替你从上下文推断,所有关系都必须由 where 子句讲清楚。

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

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

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

编译器的报错是(0.8.6 + SQLite 等价写法实测,Postgres 下只是类型名不同):

error[E0515]: cannot return value referencing local variable `sql`
 --> src/main.rs:4:5
  |
4 |     pool.fetch(&*sql)
  |     ^^^^^^^^^^^^^---^
  |     |            |
  |     |            `sql` is borrowed here
  |     returns a value referencing data owned by the current function

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

修法是让 SQL 字符串活得比 stream 更久——最简单的是直接用字符串字面量:

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

要动态拼 SQL 的话,注意 QueryBuilder 并不能把这个问题变没:build(&mut self) -> Query<'_, DB, _>(sqlx-core/src/query_builder.rs:453)里 statement: Either::Left(&self.query) 借的就是 builder 内部那个 String,Query 没有拿到它的所有权。

所以把 builder 建在函数体里、再把 stream 返回出去,一样过不了编译——报的是 E0515 cannot return value referencing local variable `qb`(实测)。

正确的写法是让 builder 活在调用方,函数只接一个 &mut 引用:

fn query_users_dyn<'a>(
    pool: &'a PgPool,
    qb: &'a mut QueryBuilder<'a, Postgres>,   // builder 由调用方持有
) -> BoxStream<'a, sqlx::Result<PgRow>> {
    qb.build().fetch(pool)
}

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

4.9 Executor 实现者清单的可视化

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

graph TD
    subgraph 用户代码路径
      U1["&pool"]
      U2["&mut conn"]
      U3["&mut *tx<br/>(经 DerefMut)"]
      U4["&mut *pool_conn<br/>(经 DerefMut)"]
      U5["&mut listener"]
      U6["&mut any_conn"]
    end
    subgraph Executor impl
      E1["impl Executor for &Pool&lt;DB&gt;<br/>sqlx-core/src/pool/executor.rs:12"]
      E2["impl Executor for &mut PgConnection<br/>sqlx-postgres/src/connection/executor.rs:380"]
      E3["impl Executor for &mut MySqlConnection<br/>sqlx-mysql/src/connection/executor.rs"]
      E4["impl Executor for &mut SqliteConnection<br/>sqlx-sqlite/src/connection/executor.rs"]
      E5["impl Executor for &mut PgListener<br/>sqlx-postgres/src/listener.rs:388"]
      E6["impl Executor for &mut AnyConnection<br/>sqlx-core/src/any/connection/executor.rs:11"]
    end
    subgraph 已删除
      D1["~~impl for &mut Transaction~~<br/>transaction.rs:132 注释"]
      D2["~~impl for &mut PoolConnection~~<br/>sqlx-core/src/pool/executor.rs:72 注释"]
    end
    U1 --> E1
    U2 --> E2 & E3 & E4
    U3 --> E2 & E3 & E4
    U4 --> E2 & E3 & E4
    U5 --> E5
    U6 --> E6

几个观察:

  1. 四种用户写法里,三种(&mut conn / &mut *tx / &mut *pool_conn)最终都落到驱动自己的 Executor impl——这是 DerefMut 的功劳。
  2. 没有一条线落到已删除的两个 impl——从用户视角看,这两个 impl 的缺席是隐形的,因为 &mut *tx 直接到达 &mut PgConnection。
  3. &pool 是唯一走 sqlx-core/src/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 章):

// 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::Service sqlx::Executor
输入 任意 Request 类型参数 Execute<'q, DB> trait bound
输出 关联 Future BoxFuture + 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 内容是:

-- 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 自己的迁移实现用的是 execute——sqlx-postgres/src/migrate.rs:279 的 execute_migration 一行 conn.execute(&*migration.sql),把整个文件当一段多语句 SQL 发出去,然后用 _ 丢掉合并后的 QueryResult。它只关心"整段跑完不报错"。

但如果你要自己写一个 迁移预演工具,想知道每一条语句各改了多少行,execute_many 才是那个 API:

let raw_sql = fs::read_to_string(path)?;
let mut tx = pool.begin().await?;
let results: Vec<PgQueryResult> = (&mut *tx)
    .execute_many(&*raw_sql)
    .try_collect::<Vec<_>>()
    .await?;
// 每个 PgQueryResult 对应一条 ALTER/CREATE/UPDATE 的 rows_affected
tx.rollback().await?;   // 预演:不落地

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

  1. &mut *tx —— DerefMut 到 &mut PgConnection(§4.6.2)
  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 不执行。

对比 execute:它把三个 QueryResult 用 Extend 累加成一个,你只知道"总共改了 12042 行",分不清哪条改了多少——这正是 execute_many 存在的理由。

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:fetch_optional 不能在第一个 Either::Left 上就收工。三家 in-tree 驱动的写法是一致的——一直 try_next()、只在拿到 Either::Right 时才返回(sqlx-mysql/src/connection/executor.rs:290-296、sqlx-sqlite/src/connection/executor.rs:65-71、sqlx-postgres/src/connection/executor.rs:435-441)。如果你的实现看到 Left 就 return Ok(None),那么在"服务端先回一条 CommandComplete 再回行"或者多语句 SQL 的场景下,fetch_one 会凭空报 Error::RowNotFound——一个很难查的 bug。Postgres 那份更进一步:连拿到 Right 之后也要把流读完(同文件 :430-433 的注释),否则会漏掉延迟约束(deferred constraint)在后续响应里报的错。

坑 3:prepare_with 返回的 Statement 生命周期和参数 sql 绑定。签名是 Statement<'q>,'q 是 sql 的生命周期——三家驱动的 Statement 都用 Cow<'q, str> 存 SQL(如 sqlx-postgres/src/statement.rs:14),借用即可、不必拷贝;上层需要脱离 'q 时由 Statement::to_owned() 得到 Statement<'static>(sqlx-core/src/statement.rs:24)。别为了凑 'static 在驱动里 Box::leak SQL 字符串——每次 prepare 都会泄漏一份内存。

写新驱动时这三点值得逐一对照。仓库根目录的 tests/ 下按驱动分了 postgres/ / mysql/ / sqlite/ / any/ 几套集成测试,它们是 Executor 契约事实上的活 spec——写新驱动之前值得先通读一遍。

4.11 本章小结

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

  1. 双原语 fetch_many + fetch_optional(§4.3)——前者用 BoxStream<Either<QueryResult, Row>> 表达"多条 SQL 的交替结果"、后者给驱动留 "最多一行" 的自由(SQLite 传 Some(1) 早停,Postgres 反而必须读完整条流才能捕获延迟约束错误)。高层方法 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.6→0.7 的破坏性变更:

  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 足够小——一个流原语表达了所有读路径。Executor(sqlx-0.8.6/sqlx-core/src/executor.rs:33-184)一共 10 个方法,其中只有 4 个没有默认体、必须由驱动实现:fetch_many(sqlx-0.8.6/sqlx-core/src/executor.rs:88)、fetch_optional(sqlx-0.8.6/sqlx-core/src/executor.rs:132,作为 fetch_one 的优化口)、prepare_with(sqlx-0.8.6/sqlx-core/src/executor.rs:164)、describe(sqlx-0.8.6/sqlx-core/src/executor.rs:178)。剩下 6 个——execute / execute_many / fetch / fetch_all / fetch_one / prepare——全是靠这四个拼出来的默认实现,驱动一行都不用写。
  2. Either 类型选择恰当——不用自己的枚举、直接用生态里成熟的 either::Either,避免了重复的 From/TryFrom 样板。这也和 Statement::parameters 的返回类型 Option<Either<&[<Self::Database as Database>::TypeInfo], usize>>(sqlx-0.8.6/sqlx-core/src/statement.rs:33——注意外面还套了一层 Option,表示"驱动根本不给参数信息"这第三种情况)形成风格一致。
  3. 不为 String 实现 Execute——这种"类型系统级的拒绝"传递的是 API 文档写十遍都传不出的设计意图:注入风险代码应当显眼。
  4. describe 被 #[doc(hidden)](sqlx-0.8.6/sqlx-core/src/executor.rs:177)——清晰地把"用户 API"和"宏内部 API"分开,文档站不出现 describe,但 sqlx-macros-core 照样能调用。

有争议的地方:

  1. BoxStream / BoxFuture 的堆分配(两者从 futures_core 引入,见 sqlx-0.8.6/sqlx-core/src/executor.rs:6-7)——每次 .fetch_one(...) 都要 Box::pin 一次。这是 0.4 定型时 trait 里写不出 -> impl Stream 留下的形状;要去掉它,就得像 0.7.0 release note(sqlx-0.8.6/CHANGELOG.md:899)说的那样把整个 Executor trait 重写一遍。同一句话也抄在 sqlx-0.8.6/sqlx-core/src/executor.rs:26-27 的 trait 文档注释里,原文是 "cannot exist in the new crate architecture without rewriting the Executor trait entirely"(注释里的 Executor 没加反引号,CHANGELOG 里加了)。
  2. where 'c: 'e 这种生命周期约束——trait 里 10 个方法每一个都要重复写一遍 'c: 'e(sqlx-0.8.6/sqlx-core/src/executor.rs:42、sqlx-0.8.6/sqlx-core/src/executor.rs:54、sqlx-0.8.6/sqlx-core/src/executor.rs:73、sqlx-0.8.6/sqlx-core/src/executor.rs:99、sqlx-0.8.6/sqlx-core/src/executor.rs:108、sqlx-0.8.6/sqlx-core/src/executor.rs:120、sqlx-0.8.6/sqlx-core/src/executor.rs:137、sqlx-0.8.6/sqlx-core/src/executor.rs:154、sqlx-0.8.6/sqlx-core/src/executor.rs:170、sqlx-0.8.6/sqlx-core/src/executor.rs:183);对新手友好度不高,错误消息也拉长。但替代方案(用 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,用户拿不到交替顺序,§4.10.1 那种"逐条语句收集影响行数"的迁移预演工具就无法实现。

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