Rust in Everything
基于 sqlx 0.9.0

operator does not exist: bigint = bigint[]

主线位置:查询主线的 bind 和 Parse。bind(&ids) 记下的类型是 INT8[],prepare 把它放进 Parse 的参数类型列表,服务端按这个类型解析 SQL,id IN ($1) 在解析阶段就失败了。bind 怎么记类型见 Query、QueryAs 与 SqlSafeStr,Parse 带哪些字段见扩展查询。

flowchart TB
  vec["bind Vec i64"] --> ty["类型记为 INT8 数组"]
  ty --> parse["Parse 声明 $1 类型"]
  parse --> sql{"SQL 怎么写"}
  sql -->|"id IN ($1)"| err["没有这个运算符"]
  sql -->|"id = ANY($1)"| ok["逐个元素比较"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"。表 item 有 id 为 1、2、3 的三行。

现象

src/bin/in_list.rs 的第一段:

  let ids: Vec<i64> = vec![1, 3];

  let r = sqlx::query_scalar::<_, i64>("select id from item where id in ($1)")
    .bind(&ids)
    .fetch_all(&pool)
    .await;
  println!("in ($1)     -> {}", r.unwrap_err());
in ($1)     -> error returned from database: operator does not exist: bigint = bigint[] at line 647

at line 647 是服务端 C 源码的行号(见错误的「0.9 的 at line N」)。换成 query!,错误提前到编译期,但说的是另一件事(src/bin/in_list_macro.rs):

  let r = sqlx::query!("select id from item where id in ($1)", &ids[..])
    .fetch_all(&pool)
    .await?;
error[E0308]: mismatched types
 --> src/bin/in_list_macro.rs:5:64
  |
5 |   let r = sqlx::query!("select id from item where id in ($1)", &ids[..])
  |                                                                ^
  |                                                                |
  |                                                                expected `i64`, found `&[i64]`
  |                                                                expected due to the type of this binding

出处

IN ($1) 在 SQL 里就是 id = $1,一个占位符只代表一个值,不会被展开成一串。两种报法的区别在于 $1 的类型是谁定的。

手写查询时由 sqlx 定。PgArguments::add 记下每个值的 type_info,Vec<T> 的 Type 是 T::array_type_info(),i64 给出 INT8[]。prepare 把这些类型的 OID 放进 Parse:

    // build a list of type OIDs to send to the database in the PARSE command
    // we have not yet started the query sequence, so we are *safe* to cleanly make
    // additional queries here to get any missing OIDs
    let param_types = conn.resolve_types(arg_types).await?;

    // flush and wait until we are re-ready
    conn.wait_until_ready().await?;

    // next we send the PARSE command to the server
    conn.inner.stream.write_msg(Parse {
        param_types: &param_types,
        query: sql,
        statement: id,
    })?;

服务端拿到「$1 是 bigint[]」,去找 bigint = bigint[] 这个运算符,没有,Parse 失败。

query! 时由服务端定。宏不知道你要传什么,只发 SQL 让服务端推断参数类型,再要求实参是对应的 Rust 类型。对两种写法调 Executor::describe(src/bin/in_list_types.rs):

select id from item where id in ($1)     params Some(Left([PgTypeInfo(Int8)]))
select id from item where id = any($1)   params Some(Left([PgTypeInfo(Int8Array)]))

IN ($1) 推出来是 INT8,宏就要一个 i64。

修法:= ANY($1)

Postgres 的 = ANY(数组) 拿左边的值和数组的每个元素比较,正好是 IN 的意思,而且整个数组只占一个参数。in_list.rs 的其余部分:

  let r: Vec<i64> = sqlx::query_scalar("select id from item where id = any($1) order by id")
    .bind(&ids)
    .fetch_all(&pool)
    .await?;
  println!("= any($1)   -> {r:?}");

  let empty: Vec<i64> = vec![];
  let r: Vec<i64> = sqlx::query_scalar("select id from item where id = any($1)")
    .bind(&empty)
    .fetch_all(&pool)
    .await?;
  println!("any(empty)  -> {r:?}");
  let r: Vec<i64> = sqlx::query_scalar("select id from item where id <> all($1) order by id")
    .bind(&ids)
    .fetch_all(&pool)
    .await?;
  println!("<> all($1)  -> {r:?}");
  let r: Vec<i64> = sqlx::query_scalar("select id from item where id <> any($1) order by id")
    .bind(&ids)
    .fetch_all(&pool)
    .await?;
  println!("<> any($1)  -> {r:?}");
  // ...
  let r = sqlx::query!("select id from item where id = any($1) order by id", &ids[..])
    .fetch_all(&pool)
    .await?;
  println!("query! any  -> {:?}", r.iter().map(|r| r.id).collect::<Vec<_>>());
= any($1)   -> [1, 3]
any(empty)  -> []
<> all($1)  -> [2]
<> any($1)  -> [1, 2, 3]
query! any  -> [1, 3]
  • 空数组得到空结果,不需要特判。
  • NOT IN 对应的是 <> ALL($1)。<> ANY($1) 的意思是「和其中某一个不相等」,只要数组里有两个不同的值,每一行都满足,于是三行全回来了。
  • query! 的实参要是切片:&ids[..] 或 &ids(&Vec<i64> 会自动转成 &[i64])。直接传 ids 报 expected `&[i64]`, found `Vec<i64>` 。

这些写法和注意事项都在 sqlx 的 FAQ 里,它还给出了不展开 IN 列表的理由:「comma-expansion means each possible length of the array generates a different query」。

另一种修法:QueryBuilder

不在 Postgres 上、或者确实需要 IN (...) 的文本时,用 QueryBuilder 的 separated 逐个 push_bind:

async fn by_builder(pool: &sqlx::PgPool, ids: &[i64]) -> sqlx::Result<Vec<i64>> {
  let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("select id from item where id in (");
  let mut sep = qb.separated(", ");
  for id in ids {
    sep.push_bind(*id);
  }
  sep.push_unseparated(") order by id");
  println!("  sql: {}", qb.sql().as_str());
  qb.build_query_scalar().fetch_all(pool).await
}
builder [1, 3]
  sql: select id from item where id in ($1, $2) order by id
  -> [1, 3]
builder []
  sql: select id from item where id in () order by id
  -> error returned from database: syntax error at or near ")" at line 1244

两个代价:

  • 列表为空时生成 in (),是语法错误,调用方要先判空。
  • 每种长度是一条不同的 SQL,各占语句缓存的一个位置。同一条连接上让长度从 1 跑到 5,两种写法各执行一次(src/bin/in_list_cache.rs),再看 pg_prepared_statements:
select id from item where id in ($1)
select id from item where id = any($1)
select id from item where id in ($1, $2)
select id from item where id in ($1, $2, $3)
select id from item where id in ($1, $2, $3, $4)
select id from item where id in ($1, $2, $3, $4, $5)
select statement from pg_prepared_statements where statement like '%from item where id%' order by prepare_time

= any($1) 只有一条,IN 列表五种长度五条(最后一行是这条查询自己)。语句缓存默认 100 条,LRU;长度分布广时,IN 列表会把别的语句挤出去,见扩展查询。每个元素一个参数,列表上万时还会撞上 65535 个参数的上限,见 too many arguments for query。

怎么确认没中招

  • 搜 SQL 里的 in ($:后面跟单个占位符的,都是这个坑。
  • 搜 <> any( / != any(:多半本意是 <> all(。
  • 用 QueryBuilder 拼 IN 列表的地方,看调用前有没有处理空列表。

小结

  • 一个占位符只代表一个值;IN ($1) 绑 Vec 时,$1 被声明成 bigint[],服务端报 operator does not exist: bigint = bigint[]。query! 里同样的 SQL 要求实参是 i64。
  • Postgres 上写 = ANY($1),绑 &Vec 或切片:一个参数、一条预备语句,空数组返回空结果。NOT IN 写 <> ALL($1),不是 <> ANY($1)。
  • QueryBuilder 的 separated + push_bind 能生成 IN ($1, $2, ...),但空列表是语法错误,每种长度一条预备语句,长列表受参数个数上限限制。

💬 关联讨论

发起讨论
加载中...