Rust in Everything
基于 sqlx 0.9.0

Query、QueryAs 与 SqlSafeStr

主线位置:查询主线的起点。sqlx::query("select ... where id = $1") 把 SQL 收成一个 SqlStr,.bind(id) 经 Arguments::add 把 id 编码进 PgArguments 的缓冲区并记下它的 PgTypeInfo,得到的 Query 是一个 Execute,.fetch_one(&pool) 把它交给 Executor。这一页讲执行之前的部分:三种查询类型、bind、persistent、0.9 新加的 SqlSafeStr / SqlStr,以及不走这条路的 raw_sql 和 QueryBuilder。

flowchart TB
  lit["&'static str"] --> safe[SqlSafeStr]
  dyn["AssertSqlSafe String"] --> safe
  safe --> sqlstr[SqlStr]
  sqlstr --> q[Query]
  bind["bind(v)"] -->|"Arguments::add"| q
  q --> qa["QueryAs O"]
  qa --> qs["QueryScalar O"]
  q -->|Execute| exe[Executor]
  qa -->|Execute| exe

三种查询类型

Query 是其余两种的内核:

pub struct Query<'q, DB: Database, A> {
    pub(crate) statement: Either<SqlStr, &'q DB::Statement>,
    pub(crate) arguments: Option<Result<A, BoxDynError>>,
    pub(crate) database: PhantomData<DB>,
    pub(crate) persistent: bool,
}
  • statement:要么是一段 SQL(query() 构造的都是这种),要么借用一个已经 prepare 好的 Statement(query_statement() 构造)。'q 只为后者存在。
  • arguments:None 表示「不用参数、走简单查询」,Some(Ok(args)) 是正常情况,Some(Err(e)) 是某次 bind 编码失败留下的错误。Execute::take_arguments 原样交出去(见 Executor 与 Acquire)。
  • persistent:要不要进连接的语句缓存,默认 true。

query() 把四个字段填好:

pub fn query<'a, DB>(sql: impl SqlSafeStr) -> Query<'a, DB, <DB as Database>::Arguments>
where
    DB: Database,
{
    Query {
        database: PhantomData,
        arguments: Some(Ok(Default::default())),
        statement: Either::Left(sql.into_sql_str()),
        persistent: true,
    }
}

arguments 一开始就是 Some,所以 query("select 1") 不绑任何参数也走预备语句。

query_as 和 query_scalar 是两层薄包装,类型参数 O 决定每一行变成什么:

构造函数类型内部O 的约束fetch_one 返回
query(sql)Query<'q, DB, A>——DB::Row
query_as(sql)QueryAs<'q, DB, O, A>inner: Query,output: PhantomData<O>O: for<'r> FromRow<'r, DB::Row>O
query_scalar(sql)QueryScalar<'q, DB, O, A>inner: QueryAs<'q, DB, (O,), A>(O,): for<'r> FromRow<'r, DB::Row>O(取元组第 0 项)

每个函数还有 _with(sql, arguments) 版本(参数事先准备好,不再 bind)和 query_statement* 版本(用现成的 Statement)。三种类型都实现了 Execute,QueryAs 和 QueryScalar 的实现全部转给 inner。它们的 fetch* 方法把 self.inner 交给执行器,拿回 DB::Row 后调 O::from_row(&row);FromRow 怎么从列里取值见 Row 与 FromRow。

QueryAs::fetch 在 0.9 有一处行为变化(#3886):0.8.6 里它调的是 fetch_many,旁边一条 FIXME 写着「this should have used executor.fetch() but that's a breaking change because this technically allows multiple statements in one query string」;0.9 改成了 executor.fetch(self.inner)。Query::map / try_map 返回的 Map 还留着那条 FIXME,仍然走 fetch_many。

bind:当场编码,执行时报错

Query::bind 按值收 self 再还回去,方便链式调用:

    /// If encoding the value fails, the error is stored and later surfaced when executing the query.
    pub fn bind<'t, T: Encode<'t, DB> + Type<DB>>(mut self, value: T) -> Self {
        let Ok(arguments) = self.get_arguments() else {
            return self;
        };

        let argument_number = arguments.len() + 1;
        if let Err(error) = arguments.add(value) {
            self.arguments = Some(Err(format!(
                "Encoding argument ${argument_number} failed: {error}"
            )
            .into()));
        }

        self
    }

    /// Like [`Query::bind`] but immediately returns an error if encoding a value failed.
    pub fn try_bind<'t, T: Encode<'t, DB> + Type<DB>>(
        &mut self,
        value: T,
    ) -> Result<(), BoxDynError> {
        let arguments = self.get_arguments()?;

        arguments.add(value)
    }
  • 值在 bind 里当场编码成字节,不是存起来等执行时再编码。所以 T 只要在这次调用期间有效:'t 和 Query 的 'q 无关。
  • 编码失败时把 arguments 换成 Some(Err(..)),消息里带上参数序号 $N。之后的 bind 在 get_arguments 那一步就发现已经是 Err,直接返回,后面的值不再编码,错误保持是第一个失败的那个。
  • bind 的签名是 -> Self,没法返回 Result,否则 .bind(a).bind(b) 就要变成 .bind(a)?.bind(b)?。想当场知道结果,用 try_bind,它收 &mut self。

Arguments::add 是驱动的实现。PgArguments::add:

    pub(crate) fn add<'q, T>(&mut self, value: T) -> Result<(), BoxDynError>
    where
        T: Encode<'q, Postgres> + Type<Postgres>,
    {
        let type_info = value.produces().unwrap_or_else(T::type_info);

        let buffer_snapshot = self.buffer.snapshot();

        // encode the value into our buffer
        if let Err(error) = self.buffer.encode(value) {
            // reset the value buffer to its previous value if encoding failed,
            // so we don't leave a half-encoded value behind
            self.buffer.reset_to_snapshot(buffer_snapshot);
            return Err(error);
        };

        // remember the type information for this value
        self.types.push(type_info);
        // increment the number of arguments we are tracking
        self.buffer.count += 1;

        Ok(())
    }

类型信息优先取 value.produces()(同一个 Rust 类型按值决定 SQL 类型时用),否则取 T::type_info();它们之后会成为 Parse 消息里的参数类型 OID。buffer.encode 先用 size_hint 粗查一次长度能否放进 i32,写 4 字节长度占位,再调 Encode::encode,最后回填实际长度(NULL 写 -1);失败时用快照把缓冲区退回原样,try_bind 失败后这个 Query 仍然可以继续用。Encode、Type 的细节在 类型,自定义类型的 OID 怎么后补在 Postgres 类型。

QueryAs::bind 和 QueryScalar::bind 的签名没跟上:

    pub fn bind<T: 'q + Encode<'q, DB> + Type<DB>>(mut self, value: T) -> Self {

还要求 T: 'q。0.9 的 #3960 只放宽了「Query::bind and Query::try_bind」。后果是:从函数里返回 Query<'static, ..> 时可以绑一个借来的 &str,返回 QueryAs<'static, ..> 时不行(src/bin/bind_lifetime.rs):

// Query::bind: the value only has to live for the call; it is encoded right away.
fn by_query(name: &str) -> Query<'static, Postgres, PgArguments> {
  sqlx::query("select $1::text").bind(name)
}

// QueryAs::bind still requires T: 'q.
fn by_query_as(name: &str) -> QueryAs<'static, Postgres, (String,), PgArguments> {
  sqlx::query_as("select $1::text").bind(name)
}
error: lifetime may not live long enough
  --> src/bin/bind_lifetime.rs:12:3
   |
11 | fn by_query_as(name: &str) -> QueryAs<'static, Postgres, (String,), PgArguments> {
   |                      - let's call the lifetime of this reference `'1`
12 |   sqlx::query_as("select $1::text").bind(name)
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`

只有 by_query_as 报错。绕法是 .bind(name.to_owned())。

实验:编码错误什么时候出现

std::time::Duration 编码成 Postgres 的 INTERVAL,后者只有微秒精度。TryFrom<Duration> for PgInterval 遇到不是 1000 纳秒整数倍的值就返回 Err("PostgreSQL INTERVAL does not support nanoseconds precision"),所以 Duration::from_nanos(1) 是一个现成的「编码一定失败」的值。

src/bin/encode_err.rs,池子只有一条连接,打开 sqlx::query=debug 日志,每条真正执行过的语句会留下一行 summary=...(平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

use std::time::Duration;

use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::{Postgres, QueryBuilder};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  tracing_subscriber::fmt()
    .with_env_filter("sqlx::query=debug")
    .without_time()
    .with_target(false)
    .with_ansi(false)
    .init();
  let url = std::env::var("DATABASE_URL")?;
  let opts: PgConnectOptions = url.parse()?;
  let pool = PgPoolOptions::new()
    .max_connections(1)
    .acquire_timeout(Duration::from_secs(1))
    .connect_with(opts)
    .await?;

  // INTERVAL has microsecond precision; 1 ns cannot be encoded.
  let q = sqlx::query("select $1::interval, $2::int4")
    .bind(Duration::from_nanos(1))
    .bind(7_i32);
  println!("bind returned a Query, no error yet");
  let err = q.fetch_one(&pool).await.unwrap_err();
  println!("fetch_one: {err}");
  println!("debug    : {err:?}");

  // The only connection is checked out: the same bad query on &pool.
  let held = pool.acquire().await?;
  let err = sqlx::query("select $1::interval")
    .bind(Duration::from_nanos(1))
    .fetch_one(&pool).await.unwrap_err();
  println!("while held: {err}");
  drop(held);

  // try_bind reports the same failure right away.
  let mut q = sqlx::query("select $1::interval");
  let e = q.try_bind(Duration::from_nanos(1)).unwrap_err();
  println!("try_bind : {e}");

  // A value that does encode: 1 µs.
  let s: String = sqlx::query_scalar("select $1::interval::text")
    .bind(Duration::from_micros(1))
    .fetch_one(&pool).await?;
  println!("1 µs ok  : {s}");

  // QueryBuilder::push_bind does not defer: it panics.
  let r = std::panic::catch_unwind(|| {
    let mut qb = QueryBuilder::<Postgres>::new("select ");
    qb.push_bind(Duration::from_nanos(1));
  });
  println!("QueryBuilder::push_bind panicked: {}", r.is_err());
  Ok(())
}
bind returned a Query, no error yet
fetch_one: error occurred while encoding a value: Encoding argument $1 failed: PostgreSQL `INTERVAL` does not support nanoseconds precision
debug    : Encode("Encoding argument $1 failed: PostgreSQL `INTERVAL` does not support nanoseconds precision")
while held: pool timed out while waiting for an open connection
try_bind : PostgreSQL `INTERVAL` does not support nanoseconds precision
DEBUG summary="select $1::interval::text" db.statement="" rows_affected=1 rows_returned=1 elapsed=5.504042ms elapsed_secs=0.005504042
1 µs ok  : 00:00:00.000001

thread 'main' (3084215) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/query_builder.rs:165:30:
Failed to add argument: "PostgreSQL `INTERVAL` does not support nanoseconds precision"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
QueryBuilder::push_bind panicked: true

逐行看:

  • 两次 bind 都没报错。第二个参数 7_i32 没被编码(get_arguments 已经是 Err),错误里写的是 $1。
  • fetch_one 返回 Error::Encode,外层的 error occurred while encoding a value: 是 Error 的 Display,里层是 bind 拼的那句。
  • 两条失败的查询都没有 summary= 日志,只有最后那条成功的有:语句日志在 PgConnection::run 里记,错误在进 run 之前就抛出了,服务端什么也没收到。
  • 唯一的连接被借走时,同一条坏查询报的是 pool timed out,不是编码错误。&Pool 的 fetch_many 先 acquire,拿到连接后才调连接的 fetch_many,take_arguments 和 arguments? 都在那之后(代码见 Executor 与 Acquire)。一条注定失败的查询也要先占一条连接。
  • try_bind 当场返回编码器的原始错误,没有 Encoding argument $N 前缀。
  • QueryBuilder::push_bind 没有延迟,arguments.add(value).expect("Failed to add argument") 直接 panic,见本页最后一节。

persistent(false)

persistent 只是改一个布尔值,但它在一个要求 DB: Database + HasStatementCache 的 impl 块里(为什么要这个标记见 Database 与关联类型):

    /// If `false`, the prepared statement will be closed after execution.
    ///
    /// Default: `true`.
    pub fn persistent(mut self, value: bool) -> Self {
        self.persistent = value;
        self
    }

设成 false 的查询仍然走预备语句,只是不进连接的语句缓存。Executor 与 Acquire 的实验里 persistent(false) 那一行,缓存大小没有变化,服务端也没有留下命名语句;0.9 起这种语句用未命名语句发送,见 扩展查询协议与语句缓存。query() 的文档建议在 pgbouncer、CockroachDB 这类「may have issues with the transparent caching of prepared statements」的场合用它。

SqlSafeStr:在类型上拦住 format!

0.8.6 的 query 收 &str:

pub fn query<DB>(sql: &str) -> Query<'_, DB, <DB as Database>::Arguments<'_>>

0.9 收 impl SqlSafeStr。SqlSafeStr 只有一个方法,所有分量都在属性和实现上:

#[diagnostic::on_unimplemented(
    label = "dynamic SQL string",
    message = "dynamic SQL strings should be audited for possible injections",
    note = "prefer literal SQL strings with bind parameters or `QueryBuilder` to add dynamic data to a query.

To bypass this error, manually audit for potential injection vulnerabilities and wrap with `AssertSqlSafe()`.
For details, see the docs for `SqlSafeStr`.\n",
    note = "this trait is only implemented for `&'static str`, not all `&str` like the compiler error may suggest"
)]
pub trait SqlSafeStr {
    /// Convert `self` to a [`SqlStr`].
    fn into_sql_str(self) -> SqlStr;
}

impl SqlSafeStr for &'static str {
    #[inline]
    fn into_sql_str(self) -> SqlStr {
        SqlStr(Repr::Static(self))
    }
}

实现它的类型一共三类:

类型转成说明
&'static strRepr::Static字面量,不拷贝
AssertSqlSafe<T>,T 是 &str、String、Box<str>、Arc<str>、Arc<String>、Cow<'static, str>对应的 Repr「我审过了」;&str 那个会拷贝一次(源码注释「Note: copies the string.」)
SqlStr 本身原样已经转换过的

Rc<str> 故意没有,注释写着「it would make QueryString: !Send」。文档说这对组合「intentionally analogous to std::panic::UnwindSafe and std::panic::AssertUnwindSafe」:trait 本身挡不住任何东西,只是让「把动态字符串当 SQL 执行」这件事必须在代码里写出 AssertSqlSafe 这个词,代码审查时一眼能看到。sqlx 自己也这样用,begin_ansi_transaction_sql 生成保存点语句时写的是 AssertSqlSafe(format!("SAVEPOINT _sqlx_savepoint_{depth}")),depth 是个整数。

实验:format! 进 query

src/bin/fmt_sql.rs:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let pool = exp_sqlx_core1::pool(1).await?;
  let table = "pg_class";
  let r = sqlx::query(&format!("select count(*) from {table}"))
    .execute(&pool)
    .await?;
  println!("rows_affected = {}", r.rows_affected());
  Ok(())
}

sqlx 0.9.0:

error[E0277]: dynamic SQL strings should be audited for possible injections
   --> src/bin/fmt_sql.rs:5:23
    |
  5 |   let r = sqlx::query(&format!("select count(*) from {table}"))
    |           ----------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dynamic SQL string
    |           |
    |           required by a bound introduced by this call
    |
    = help: the trait `SqlSafeStr` is not implemented for `&std::string::String`
    = note: prefer literal SQL strings with bind parameters or `QueryBuilder` to add dynamic data to a query.
            
            To bypass this error, manually audit for potential injection vulnerabilities and wrap with `AssertSqlSafe()`.
            For details, see the docs for `SqlSafeStr`.
            
    = note: this trait is only implemented for `&'static str`, not all `&str` like the compiler error may suggest
help: the trait `SqlSafeStr` is implemented for `&str`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/sql_str.rs:52:1
    |
 52 | impl SqlSafeStr for &'static str {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `sqlx::query`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/query.rs:653:32
    |
653 | pub fn query<'a, DB>(sql: impl SqlSafeStr) -> Query<'a, DB, <DB as Database>::Arguments>
    |                                ^^^^^^^^^^ required by this bound in `query`

For more information about this error, try `rustc --explain E0277`.

报错的标题、标签和两条 note 都来自 on_unimplemented。第二条 note 是为下面那行 help 准备的:rustc 说「SqlSafeStr is implemented for &str」,实际只对 &'static str 实现。不写 &、直接传 format!(...),报错相同,只是类型变成 std::string::String。

同一个文件在 sqlx 0.8.6 下编译通过,运行输出:

rows_affected = 1

0.8.6 里反而是不加 & 的 sqlx::query(format!(...)) 编译不过:

error[E0308]: mismatched types
 --> src/bin/fmt_sql_owned.rs:6:23
  |
6 |   let r = sqlx::query(format!("select count(*) from {table}"))
  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `String`

也就是说,0.8.6 的拦截只靠「要多写一个 &」。

在 0.9 里,一个不是 'static 的 &str 也过不了,但报的是借用错误,没有 on_unimplemented 的提示,因为 &'_ str: SqlSafeStr 在类型上是匹配的,只是生命周期不够(src/bin/borrowed_sql.rs):

  let sql = String::from("select 1");
  let r = sqlx::query(sql.as_str()).execute(&pool).await?;
error[E0597]: `sql` does not live long enough
   --> src/bin/borrowed_sql.rs:5:23
    |
  4 |   let sql = String::from("select 1");
    |       --- binding `sql` declared here
  5 |   let r = sqlx::query(sql.as_str()).execute(&pool).await?;
    |           ------------^^^----------
    |           |           |
    |           |           borrowed value does not live long enough
    |           argument requires that `sql` is borrowed for `'static`
...
  8 | }
    | - `sql` dropped here while still borrowed
    |
note: requirement that the value outlives `'static` introduced here
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/query.rs:653:32
    |
653 | pub fn query<'a, DB>(sql: impl SqlSafeStr) -> Query<'a, DB, <DB as Database>::Arguments>
    |                                ^^^^^^^^^^

正确的改法按顺序:值用 $N 加 bind;表名、列名这类不能做参数的部分,从固定的白名单里取,再用 AssertSqlSafe 包;结构本身是动态的(可选条件、批量插入),用 QueryBuilder。

📝NOTE
冷知识:&format! 曾经就是那道减速带

最早的 query 签名就是 query<'q>(query: &'q str)。#1594(一个设计提案 issue)解释了当初的用意:允许直接传 String「makes it extremely tempting to simply format!() user input into the query string」,而要求传字符串切片,「it will at least make the code start to smell a bit」,也就是逼人多写一个 &。同一个提案也列出了它的问题:「All you have to do for the function to accept a String is to add a & and let auto-deref do the rest」,而且「This design decision is notorious for making it very difficult to implement query builders on top of SQLx, because of the lifetime issues that it introduces」,例子正是一个想返回 QueryAs<'q, ..> 却借用了局部 String 的函数。提案里的新名字叫 InjectionSafe / AssertInjectionSafe,照的就是 UnwindSafe。

#3364「breaking: introduce SqlSafeStr API」写下了新方案:「The existing requirement of a borrowed string is meant to discourage adding dynamic data to a query with format!(), but that's very non-obvious and also precludes many useful patterns.」最终合并的是接手它的 #3723「Add SqlStr」,首次发布于 0.9.0,CHANGELOG 写的是「all query*() functions now take impl SqlSafeStr which is only implemented for &'static str and AssertSqlSafe」。上面那段专门的报错是后来的 #4153「Add on unimplemented diagnostic to SqlStr」加的,也在 0.9.0 里。

SqlStr:让查询变成 'static

into_sql_str 的结果是 SqlStr,执行器、Statement、语句缓存拿到的都是它:

pub struct SqlStr(Repr);

#[derive(Debug)]
enum Repr {
    /// We need a variant to memoize when we already have a static string, so we don't copy it.
    Static(&'static str),
    /// Thanks to the new niche in `String`, this doesn't increase the size beyond 3 words.
    /// We essentially get all these variants for free.
    Owned(String),
    Boxed(Box<str>),
    Arced(Arc<str>),
    /// Allows for dynamic shared ownership with `query_builder`.
    ArcString(Arc<String>),
}

文档说它「essentially Cow<'static, str>」,只是多了几种不用拷贝就能接进来的所有权形式。Clone 有一个策略:Static 和 Arced 照抄,其余三种第一次 clone 时拷贝成 Arced,理由是「If .clone() gets called once, assume it might get called again.」

SqlStr 没有生命周期,Query 的 'q 就只剩下借用 Statement 那一种用法,于是可以从函数里返回一个拥有自己 SQL 的查询(src/bin/owned_query.rs):

use sqlx::postgres::PgArguments;
use sqlx::query::Query;
use sqlx::{AssertSqlSafe, Postgres, SqlStr};

// Table names cannot be bind parameters; this one comes from a fixed list.
fn count_rows(table: &str, min_oid: i64) -> Query<'static, Postgres, PgArguments> {
  assert!(["pg_class", "pg_type"].contains(&table));
  let sql = format!("select count(*) from {table} where oid::int8 >= $1");
  sqlx::query(AssertSqlSafe(sql)).bind(min_oid)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  println!("size_of::<SqlStr>()  = {}", size_of::<SqlStr>());
  println!("size_of::<String>()  = {}", size_of::<String>());
  println!("size_of::<Option<String>>() = {}", size_of::<Option<String>>());
  let pool = exp_sqlx_core1::pool(1).await?;
  let q = count_rows("pg_type", 0);
  let row = q.fetch_one(&pool).await?;
  let n: i64 = sqlx::Row::get(&row, 0);
  println!("pg_type rows > 0: {}", n > 0);
  Ok(())
}

count_rows 在 0.9 编译通过、能执行(输出见动脑筋答案)。在 0.8.6 里,同样的函数只能写成 sqlx::query(&sql).bind(min_oid),报:

error[E0515]: cannot return value referencing local variable `sql`
 --> src/bin/owned_query.rs:8:3
  |
8 |   sqlx::query(&sql).bind(min_oid)
  |   ^^^^^^^^^^^^----^^^^^^^^^^^^^^^
  |   |           |
  |   |           `sql` is borrowed here
  |   returns a value referencing data owned by the current function

CHANGELOG 对 #3723 的描述是「This, along with [#3960], finally allows returning owned queries as the type will be Query<'static, DB>」。

❗IMPORTANT
动脑筋:SqlStr 有多大?

Repr 有五个变体,其中 &'static str、Box<str>、Arc<str> 都是两个字的胖指针,String 是三个字,Arc<String> 是一个字。在 64 位平台(M2 Max、macOS、rustc 1.99.0)上,上面程序的第一行 size_of::<SqlStr>() 打印多少?是 24,还是要再加一个字放判别值?

raw_sql

raw_sql 也收 SqlSafeStr,返回的 RawSql(SqlStr) 只有一个字段。它的 Execute 实现决定了它的全部行为:

impl<DB: Database> Execute<'_, DB> for RawSql {
    fn sql(self) -> SqlStr {
        self.0
    }

    fn statement(&self) -> Option<&<DB as Database>::Statement> {
        None
    }

    fn take_arguments(&mut self) -> Result<Option<<DB as Database>::Arguments>, BoxDynError> {
        Ok(None)
    }

    fn persistent(&self) -> bool {
        false
    }
}

take_arguments 是 None,Postgres 驱动就发一条简单查询的 Query 消息:一个字符串里可以有多条用 ; 分开的语句,不能有参数,结果是文本格式。Executor 的文档把它定位成「for DDL (CREATE, ALTER, etc.), batch queries and administration functions」。本页的实验用它建表、删表就是这个用法。RawSql 的方法在 0.9 都多了一个 DB 类型参数(#3613),数据库类型从传进来的执行器推出来。

QueryBuilder

动态 SQL(可选的 WHERE 条件、不定长的 IN 列表、批量插入)的正路是 QueryBuilder:

pub struct QueryBuilder<DB>
where
    DB: Database,
{
    query: Arc<String>,
    init_len: usize,
    arguments: Option<<DB as Database>::Arguments>,
}

它一边拼 SQL 文本,一边往同一个 Arguments 里绑值,两者的序号由 Arguments 的计数保证一致。几个方法:

  • new(init: impl Into<String>):注意这里不是 SqlSafeStr,push(sql: impl Display) 也不是。QueryBuilder 里的文本是什么都可以拼的,最后由 sql() / into_sql() 在内部包一层 AssertSqlSafe 交出去。安全靠的是只用 push 拼结构、只用 push_bind 放值。
  • push_bind:arguments.add(value).expect("Failed to add argument"),再调 Arguments::format_placeholder 往文本里写占位符。Postgres 的实现写 $ 加当前参数个数,MySQL 用默认实现写 ?。编码失败直接 panic,不像 Query::bind 那样存起来(上面实验的最后一行)。
  • separated(", "):返回一个 Separated,它的 push / push_bind 从第二次起自动先写分隔符,push_unseparated 不写。
  • build():把 arguments 用 take() 拿走,构造一个借用 &mut self 的 Query。之后再 push 会触发 sanity_check 的 panic「QueryBuilder must be reset before reuse after .build()」,要先 reset(),它把文本截回 init_len。

push_values 是用 separated 搭出来的批量插入:

    pub fn push_values<I, F>(&mut self, tuples: I, mut push_tuple: F) -> &mut Self
    where
        I: IntoIterator,
        F: FnMut(Separated<'_, DB, &'static str>, I::Item),
    {
        self.sanity_check();

        self.push("VALUES ");

        let mut separated = self.separated(", ");

        for tuple in tuples {
            separated.push("(");

            // use a `Separated` with a separate (hah) internal state
            push_tuple(separated.query_builder.separated(", "), tuple);

            separated.push_unseparated(")");
        }

        debug_assert!(
            separated.push_separator,
            "No value being pushed. QueryBuilder may not build correct sql query!"
        );

        separated.query_builder
    }

外层的 Separated 负责元组之间的 , ,每个元组内部再开一个新的 Separated 管值之间的 , 。迭代器为空时,debug 构建会在 debug_assert! 处 panic,release 构建会生成一句以 VALUES 结尾的残缺 SQL。

实验:push_values 和 separated 生成的 SQL

src/bin/builder.rs:

use sqlx::{Postgres, QueryBuilder};

struct User {
  id: i64,
  name: &'static str,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let pool = exp_sqlx_core1::pool(1).await?;
  sqlx::raw_sql("drop table if exists qb_users; create table qb_users(id int8 primary key, name text not null)")
    .execute(&pool).await?;

  let users = [User { id: 1, name: "ann" }, User { id: 2, name: "bob" }, User { id: 3, name: "cy" }];
  let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("insert into qb_users(id, name) ");
  qb.push_values(&users, |mut b, u| {
    b.push_bind(u.id).push_bind(u.name);
  });
  qb.push(" on conflict (id) do nothing");
  println!("push_values: {}", qb.sql().as_str());
  let r = qb.build().execute(&pool).await?;
  println!("inserted   : {}", r.rows_affected());

  let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("select name from qb_users where id in (");
  let mut sep = qb.separated(", ");
  for id in [1_i64, 3] {
    sep.push_bind(id);
  }
  sep.push_unseparated(") order by id");
  println!("separated  : {}", qb.sql().as_str());
  let names: Vec<String> = qb.build_query_scalar().fetch_all(&pool).await?;
  println!("names      : {names:?}");

  sqlx::raw_sql("drop table qb_users").execute(&pool).await?;
  Ok(())
}
push_values: insert into qb_users(id, name) VALUES ($1, $2), ($3, $4), ($5, $6) on conflict (id) do nothing
inserted   : 3
separated  : select name from qb_users where id in ($1, $2) order by id
names      : ["ann", "cy"]

VALUES 是大写的,因为它是 push_values 自己写的;占位符从 $1 连续编到 $6,和绑定顺序一致。每一批行数不同,SQL 文本就不同,在连接的语句缓存里各占一条。Postgres 一条语句最多 65535 个参数,超过时 PgConnection::run 在发任何消息之前报 too many arguments for query(见 扩展查询协议与语句缓存),所以 push_values 一批的行数要按列数折算。IN 列表也可以不拼:where g = any($1) 绑一个 &Vec<i64>(编码成 INT8[]),SQL 文本固定,缓存里只有一条;在同一个 lab 库上,select g from generate_series(1::int8, 4) g where g = any($1) order by g 绑 vec![1, 3] 返回 [1, 3]。

动脑筋答案

src/bin/owned_query.rs 的完整输出:

size_of::<SqlStr>()  = 24
size_of::<String>()  = 24
size_of::<Option<String>>() = 24
pg_type rows > 0: true

是 24,不需要额外的判别值字。String 的容量字段不会超过 isize::MAX,最高位永远是 0,编译器把这个不可能的取值范围(niche)拿来编码其余变体,Option<String> 也是 24 就是同一个原因。剩下四个变体都不超过两个字,放得进 String 的另外两个字里。源码注释「Thanks to the new niche in String, this doesn't increase the size beyond 3 words」说的就是这件事。最后一行说明 count_rows 返回的 Query<'static, ..> 确实能拿去执行。

小结

  • query / query_as / query_scalar 是一层套一层:QueryScalar 包 QueryAs<(O,)>,QueryAs 包 Query,执行时都把内层的 Query 交给执行器,再用 FromRow 转换。
  • query() 一开始就放了一个空的 Arguments,不绑参数也走预备语句;只有 &str 和 raw_sql 走简单查询。
  • bind 当场编码,失败的错误连同 $N 存进 Query,后续 bind 被跳过,执行时(拿到连接之后、进 run 之前)才以 Error::Encode 抛出;try_bind 当场返回;QueryBuilder::push_bind 直接 panic。
  • 0.9 起 SQL 必须是 SqlSafeStr:&'static str 直接通过,动态字符串要包 AssertSqlSafe;&format!(..) 报 E0277,借来的 &str 报 E0597。
  • SqlStr 没有生命周期、只占 24 字节,Query<'static, ..> 可以从函数返回;但 QueryAs::bind 仍要求 T: 'q。
  • 动态结构用 QueryBuilder:push 拼文本、push_bind 放值、separated 和 push_values 管分隔符。

💬 关联讨论

发起讨论
加载中...