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 str | Repr::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。
&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>」。
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管分隔符。