Rust in Everything
基于 sea-query 1.0.2

绑到 sqlx

主线位置:查询主线从 sea-query 进入 sqlx 的那一步。ConnectionTrait::query_one 用 build 得到 sea-orm 的 Statement(SQL 带 $N,加一组 Values),DatabaseConnection 把它分派到 SqlxPostgresPoolConnection::query_one,后者调 sqlx_query(&stmt):sqlx::query_with(AssertSqlSafe(sql), SqlxValues(values)),再 pool.acquire()、fetch_one。sqlx 执行时调 SqlxValues::into_arguments,sea-query-sqlx 在这里把每个 Value 换成一个具体的 Rust 类型 add 进 PgArguments。之后的编码、Parse / Bind 是 sqlx 卷的内容。

flowchart TB
  st["Statement sql + Values"] --> sq["sqlx_query"]
  sq --> qw["query_with + AssertSqlSafe"]
  qw --> acq["acquire 然后 fetch_one"]
  acq --> ta["take_arguments"]
  ta --> ia["SqlxValues::into_arguments"]
  ia --> add["按 Value 分支 add"]
  add --> pga["PgArguments 类型 + 字节"]
  pga --> wire["Parse 带 OID / Bind"]

Statement:SQL 加一个 Option Values

sea-orm 的 Statement 是三个公开字段:

#[derive(Debug, Clone, PartialEq)]
pub struct Statement {
    /// The SQL query
    pub sql: String,
    /// The values for the SQL statement's parameters
    pub values: Option<Values>,
    /// The database backend this statement is constructed for.
    /// The SQL dialect depends on the backend. SQL strings are not portable across databases.
    pub db_backend: DbBackend,
}

values 是 Option:build 出来的查询语句是 Some,Statement::from_string(以及建表这类 schema 语句)是 None;手写 SQL 加参数用 from_sql_and_values,本页的实验都用它绕过 sea-query 直接构造 Values。

sqlx_query 把它变成 sqlx 的 Query:

pub(crate) fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, SqlxValues> {
    let values = stmt
        .values
        .as_ref()
        .map_or(Values(Vec::new()), |values| values.clone());
    sqlx::query_with(sqlx::AssertSqlSafe(stmt.sql.as_str()), SqlxValues(values))
}

三点:

  • None 换成空的 Values,仍然走 query_with。在 sqlx 里,带着一个 Arguments 对象(哪怕是空的)就走扩展查询协议、进语句缓存(Executor 与 Acquire)。所以 from_string 的语句也是预备语句;只有 execute_unprepared 直接把 AssertSqlSafe(sql.to_owned()) 交给 conn.execute,走简单查询,一次可以发多条语句。
  • AssertSqlSafe 是 sqlx 0.9 的 SqlSafeStr 门(Query、QueryAs 与 SqlSafeStr)。Statement::sql 是运行时的 String,不套这一层编译不过;sea-orm 在这里统一断言「这段 SQL 是安全的」,依据是它来自 build,值都在 Values 里。上一页说过,Expr::cust 里拼进去的东西不在这个保证之内。
  • values.clone():sqlx_query 只借用 &Statement,参数要被 into_arguments 按值消费,于是每次执行复制一份 Values。

SqlxPostgresPoolConnection 的 execute、query_one、query_all 都是同一个模式(query_one):

    pub async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
        debug_print!("{}", stmt);

        let query = sqlx_query(&stmt);
        let mut conn = self.pool.acquire().await.map_err(sqlx_conn_acquire_err)?;
        crate::metric::metric!(self.metric_callback, &stmt, {
            match query.fetch_one(&mut *conn).await {
                Ok(row) => Ok(Some(row.into())),
                Err(err) => match err {
                    sqlx::Error::RowNotFound => Ok(None),
                    _ => Err(sqlx_error_to_query_err(err)),
                },
            }
        })
    }

debug_print! 在开了 debug-print feature 时是 tracing::debug!,打印的是 Statement 的 Display:它调 sea-query 的 inject_parameters,把值填回 SQL。sqlx 自己的语句日志打印的是真正发出去的 SQL。两者放在一起看(src/bin/log.rs,M2 Max、macOS、rustc 1.99.0、Postgres 16.14,sea-orm =2.0.4 开 sqlx-postgres、runtime-tokio-rustls、with-chrono、with-json、with-uuid、postgres-array、debug-print,连接池一条连接,search_path 设到自己的 schema):

use sea_orm::ConnectionTrait;
use sea_query::{Expr, ExprTrait, Query};

// One SelectStatement through ConnectionTrait::query_one, with sea-orm's
// debug-print and sqlx's statement log both switched on.
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
  tracing_subscriber::fmt()
    .with_env_filter("sqlx::query=info,sea_orm=debug")
    .without_time()
    .with_ansi(false)
    .init();
  let db = exp_seaorm_seaquery::connect(true).await?;
  db.execute_unprepared("drop table if exists cake; create table cake (id int4 primary key, name text); insert into cake values (1, 'it''s choc')").await?;
  let q = Query::select()
    .columns(["id", "name"])
    .from("cake")
    .and_where(Expr::col("name").like("%choc%"))
    .limit(1)
    .to_owned();
  let row = db.query_one(&q).await?.expect("one row");
  println!("name = {:?}", row.try_get::<String>("", "name"));
  db.execute_unprepared("drop table cake").await?;
  Ok(())
}
...
DEBUG sea_orm::driver::sqlx_postgres: SELECT "id", "name" FROM "cake" WHERE "name" LIKE '%choc%' LIMIT 1
 INFO sqlx::query: summary="SELECT \"id\", \"name\" FROM …" db.statement="\n\nSELECT \"id\", \"name\" FROM \"cake\" WHERE \"name\" LIKE $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=1.66625ms elapsed_secs=0.00166625
name = Ok("it's choc")
...

第一行是 sea-orm 的打印,值已经内联;第二行是 sqlx 的 sqlx::query 事件,$1、$2 才是发给服务端的样子。debug-print 的输出适合复制到 psql 里看结果,但它和 to_string 一样丢了参数类型(QueryBuilder 与参数)。

SqlxValues 与 IntoArguments

sea-query-sqlx 的 SqlxValues 只是一个 newtype:

#[derive(Clone, Debug, PartialEq)]
#[repr(transparent)]
pub struct SqlxValues(pub sea_query::Values);

需要它,是因为孤儿规则:IntoArguments 是 sqlx 的 trait,Values 是 sea-query 的类型,第三个 crate 不能直接给它们写 impl。sqlx 0.9 的 IntoArguments<DB> 只有一个方法 fn into_arguments(self) -> DB::Arguments,query_with 收任何实现了它的东西。Postgres 的 实现 是一个 360 多行的 match(节选):

impl sqlx::IntoArguments<sqlx::postgres::Postgres> for SqlxValues {
    fn into_arguments(self) -> sqlx::postgres::PgArguments {
        let mut args = sqlx::postgres::PgArguments::default();
        for arg in self.0.into_iter() {
            use sqlx::Arguments;
            match arg {
                Value::Bool(b) => {
                    let _ = args.add(b);
                }
                Value::TinyInt(i) => {
                    let _ = args.add(i);
                }
                // ...
                Value::TinyUnsigned(i) => {
                    let _ = args.add(i.map(|i| i as i16));
                }
                Value::SmallUnsigned(i) => {
                    let _ = args.add(i.map(|i| i as i32));
                }
                Value::Unsigned(i) => {
                    let _ = args.add(i.map(|i| i as i64));
                }
                Value::BigUnsigned(i) => {
                    let _ = args.add(i.map(|i| <i64 as TryFrom<u64>>::try_from(i).unwrap()));
                }
                // ...
                Value::String(s) => {
                    let _ = args.add(s.as_deref());
                }
                Value::Enum(e) => {
                    let value = match e {
                        OptionEnum::Some(v) => Some(v.value.into_owned()),
                        OptionEnum::None(_) => None,
                    };
                    let _ = args.add(value);
                }
                Value::Char(c) => {
                    let _ = args.add(c.map(|c| c.to_string()));
                }
                // ...
                #[cfg(feature = "with-json")]
                Value::Json(j) => {
                    let _ = args.add(j.as_deref());
                }
                // ...

每个分支把 Option<某类型> 原样(或换一个 sqlx 认识的类型)交给 PgArguments::add,类型 OID 由 sqlx 的 Type 实现决定。有三处值得停下来看:

  • let _ =:PgArguments::add 编码失败时把缓冲区退回快照、不增加参数个数、返回 Err。这里把错误丢掉了,into_arguments 的签名也没有地方返回错误。
  • 无符号整数:Postgres 没有无符号类型,u8 / u16 / u32 各升一级成 i16 / i32 / i64;u64 没有更大的有符号类型,只能 try_from(..).unwrap(),超过 i64::MAX 就 panic(下文实验)。
  • 转换发生得很晚:into_arguments 不是在 query_with 里调的,而是 sqlx 执行器调 take_arguments 时才调。sea-orm 先 acquire 再 fetch_one,所以这些 panic 发生在已经拿到连接之后。

数组分支先把 Value::Array(ty, v) 转成 Option<Vec<T>>,再整个 add(Value::Array 分支):

                #[cfg(feature = "postgres-array")]
                Value::Array(ty, v) => match ty {
                    // ...
                    ArrayType::Int => {
                        let value: Option<Vec<i32>> = Value::Array(ty, v)
                            .expect("This Value::Array should consist of Value::Int");
                        let _ = args.add(value);
                    }
                    // ...

Value::expect::<T> 是 ValueType::expect,即 Self::try_from(v).expect(msg)。数组的 try_from 在 impl ValueType for Vec<T> 里,元素是 NULL 时返回 Err(ValueTypeErr),expect 就 panic。

每种 Value 绑成什么类型

把各种 Value 经 sea-orm 的 Statement 绑出去,问 Postgres 参数的类型、是否为 NULL、转成文本是什么(src/bin/bind.rs,平台同上)。sqlx 的语句缓存只按 SQL 文本查,每个用例的 SQL 末尾加了一个不同的注释,保证每次都重新 Parse(原因见本页最后一节):

use sea_orm::{ConnectionTrait, DbBackend, Statement};
use sea_query::{ArrayType, Value};

// Bind each sea-query Value through sea-orm -> SqlxValues -> PgArguments and
// ask Postgres what type the parameter got.
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
  let db = exp_seaorm_seaquery::connect(false).await?;
  let ts = chrono::DateTime::parse_from_rfc3339("2026-10-24T08:00:00+08:00").unwrap();
  let cases: Vec<(&str, Value)> = vec![
    ("Int(None)", Value::Int(None)),
    ("String(None)", Value::String(None)),
    ("Bool(None)", Value::Bool(None)),
    ("TinyInt(Some(1))", Value::TinyInt(Some(1))),
    ("TinyUnsigned(Some(1))", Value::TinyUnsigned(Some(1))),
    ("Unsigned(Some(1))", Value::Unsigned(Some(1))),
    ("BigUnsigned(Some(1))", Value::BigUnsigned(Some(1))),
    ("Char(Some('a'))", Value::Char(Some('a'))),
    ("Json(None)", Value::Json(None)),
    ("ChronoDateTime", ts.naive_utc().into()),
    ("ChronoDateTimeUtc", ts.to_utc().into()),
    ("ChronoDateTimeWithTimeZone", ts.into()),
    ("Uuid(None)", Value::Uuid(None)),
    ("Array(Int, [1, 2])", Value::Array(ArrayType::Int, Some(Box::new(vec![1.into(), 2.into()])))),
    ("Array(String, None)", Value::Array(ArrayType::String, None)),
  ];
  for (i, (label, v)) in cases.into_iter().enumerate() {
    // Distinct SQL text per case: sqlx caches prepared statements by SQL text only.
    let sql = format!("select pg_typeof($1)::text as t, $1 is null as n, $1::text as v -- {i}");
    let row = db
      .query_one_raw(Statement::from_sql_and_values(DbBackend::Postgres, sql, [v]))
      .await?
      .expect("one row");
    let t: String = row.try_get("", "t")?;
    let n: bool = row.try_get("", "n")?;
    let s: Option<String> = row.try_get("", "v")?;
    println!("{label:<28} {t:<28} null={n:<5} text={s:?}");
  }
  Ok(())
}
❗IMPORTANT
动脑筋:Value::TinyInt(Some(1)) 绑出去是什么类型?

sea-orm 实体里 i8 字段的值是 Value::TinyInt,IntoArguments 原样 args.add(Option<i8>)。上面第四个用例打印的 pg_typeof($1) 是 smallint、integer,还是别的?

结果(除 TinyInt 一行,留到动脑筋答案):

Int(None)                    integer                      null=true  text=None
String(None)                 text                         null=true  text=None
Bool(None)                   boolean                      null=true  text=None
...
TinyUnsigned(Some(1))        smallint                     null=false text=Some("1")
Unsigned(Some(1))            bigint                       null=false text=Some("1")
BigUnsigned(Some(1))         bigint                       null=false text=Some("1")
Char(Some('a'))              text                         null=false text=Some("a")
Json(None)                   jsonb                        null=true  text=None
ChronoDateTime               timestamp without time zone  null=false text=Some("2026-10-24 00:00:00")
ChronoDateTimeUtc            timestamp with time zone     null=false text=Some("2026-10-24 00:00:00+00")
ChronoDateTimeWithTimeZone   timestamp with time zone     null=false text=Some("2026-10-24 00:00:00+00")
Uuid(None)                   uuid                         null=true  text=None
Array(Int, [1, 2])           integer[]                    null=false text=Some("{1,2}")
Array(String, None)          text[]                       null=true  text=None

整理成表,左边是分支,中间是 IntoArguments 交给 sqlx 的 Rust 类型:

Value 分支交给 add 的类型Postgres 类型
Bool / SmallInt / Int / BigIntOption<bool> / Option<i16> / Option<i32> / Option<i64>boolean / smallint / integer / bigint
TinyUnsigned / SmallUnsigned / Unsigned升一级:i16 / i32 / i64smallint / integer / bigint
BigUnsignedi64,try_from(u64).unwrap()bigint,超过 i64::MAX panic
String / Char / EnumOption<&str> / Option<String> / Option<String>text(枚举的类型由 SQL 里的 ::"类型名" 转换)
JsonOption<&serde_json::Value>jsonb
ChronoDateTimeOption<NaiveDateTime>timestamp without time zone
ChronoDateTimeUtc / Local / WithTimeZoneOption<DateTime<Utc / Local / FixedOffset>>timestamp with time zone
UuidOption<Uuid>uuid
Array(ArrayType::Int, ..) 等Option<Vec<i32>> 等integer[] 等

几点说明:

  • Json 交给 sqlx 的是 &serde_json::Value,sqlx 把它当 Json<&Value> 编码,声明 JSONB,参数实际是 json 时靠补丁把版本字节改成空格(Postgres 类型的「洞和补丁」)。
  • 三种带时区的 chrono 类型都是 timestamptz,服务端只存一个 UTC 时刻。+08:00 的 08:00 读回来是 00:00:00+00(实验库的会话 TimeZone 是 UTC),偏移量本身不保存。
  • 枚举值绑成 text,类型名靠 PostgresQueryBuilder::prepare_value 在 SQL 里追加的 ::"类型名";同一个值经 to_string 写出时也带这个转换。

None 也带着类型

Value::Int(None) 绑成 integer 而不是 unknown,原因在 sqlx:Option<T> 的 Encode::produces 在 None 时仍返回 T::type_info(),PgArguments::add 把这个类型记进 types,Parse 时声明给服务端(类型:Type、Encode、Decode 的「Option:NULL 的唯一表达」)。sea-query 的 NULL 不是一个单独的分支,而是某个分支里的 None(列与值的「Value:每种类型一个分支」),IntoArguments 把它交给 sqlx 时就是一个 Option<具体类型>::None。

在协议层看一次:一条语句三个参数,Value::Int(None)、Value::String(None)、Value::BigInt(Some(42)),sea-orm 连到一个打印协议消息的代理(复用 sqlx 卷的代理,Parse 打印参数 OID,Bind 打印每个参数的长度和字节;src/bin/wire.rs,sslmode=disable):

let stmt = Statement::from_sql_and_values(
  DbBackend::Postgres,
  "select $1 is null and $2 is null as both_null, $3 as n",
  [Value::Int(None), Value::String(None), Value::BigInt(Some(42))],
);
let row = db.query_one_raw(stmt).await?.expect("one row");
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Parse(sqlx_s_1, "select $1 is null and $2 is null as both_null, $3 as n", param oids [23, 25, 20]), Describe(S, sqlx_s_1), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1; NULL(-1), NULL(-1), [8] 00 00 00 00 00 00 00 2a), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 1/8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
both_null=Ok(true)
  • Parse 声明的参数类型是 [23, 25, 20]:int4、text、int8。两个 NULL 的类型都在。
  • Bind 里 NULL 是长度 -1、没有字节;42 是 8 字节大端整数。
  • 首尾两对 Sync / ReadyForQuery 是连接池在取出和归还连接时的 ping,消息序列和 sqlx 自己执行一条查询时一样(一次查询的全过程):sea-orm 在 sqlx 之上没有多发任何东西。

两个会 panic 的值

BigUnsigned 的 unwrap 和数组的 expect 都在 into_arguments 里,值不合适时整个任务 panic,而不是返回 DbErr(src/bin/panics.rs,命令行参数选用例;下面的输出是三次运行,== 行由外面的 shell 循环打印):

let v = match which.as_str() {
  "array" => Value::Array(ArrayType::Int, Some(Box::new(vec![1.into(), Value::Int(None)]))),
  "text" => Value::String(Some("{1,NULL}".into())),
  _ => Value::BigUnsigned(Some(u64::MAX)),
};
let stmt = Statement::from_sql_and_values(DbBackend::Postgres, "select ($1::text::int4[])::text as v", [v]);
let row = db.query_one_raw(stmt).await?.expect("one row");
println!("{:?}", row.try_get::<String>("", "v"));
== array
thread 'main' (3604831) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-query-1.0.2/src/value.rs:970:27:
This Value::Array should consist of Value::Int: ValueTypeErr
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
== big
thread 'main' (3604863) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-query-sqlx-0.9.1/src/sqlx_postgres.rs:55:83:
called `Result::unwrap()` on an `Err` value: TryFromIntError(PosOverflow)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
== text
Ok("{1,NULL}")

第一个 panic 的消息说数组「应该由 Value::Int 组成」,而它确实是:元素是 Value::Int(Some(1)) 和 Value::Int(None)。真正的原因是第二个元素是 NULL,Vec<i32> 装不下。

📝NOTE
冷知识:#1068 修了,但没修到这条路
#1068 报的就是上面这个用法:sea-orm 1.0.1(sea-query 0.31)里用 Statement::from_sql_and_values 绑一个 vec![Value::Int(None), Value::Int(Some(1))] 的数组,panic 在 ValueType::unwrap。修复它的 #1070(2026-05-24 合并,首次发布于 1.0.0)做了两件事:Vec<T> 的 try_from 不再对元素直接 unwrap,遇到 NULL 返回 Err(ValueTypeErr);另加 impl ValueType for Vec<Option<T>>,NULL 元素变成 None。PR 说明写着「The proper approach to this would probably be to implement a conversion to Vec<Option<T>> instead」。1.0.0 的发布说明据此写「PostgreSQL array values can now contain null elements without panicking during conversion」。但 sea-query-sqlx 的数组分支要的仍是 Option<Vec<i32>>,走的是 Vec<T> 那个 try_from,Err 被 .expect 变回 panic,只是消息换了。上面的实验就是 sea-query 1.0.2 + sea-query-sqlx 0.9.1 上的结果。

要绑含 NULL 的数组,得让它绕开 Value::Array。上面 text 那个用例是一种办法:把数组写成 Postgres 的数组文本 {1,NULL},作为 Value::String 绑定,在 SQL 里 $1::text::int4[] 转回数组。

chrono 数组的分支也出过一次错。#933(首次发布于 0.32.7,也就是本站在用的 sea-query 版本)修的是 ArrayType::ChronoDateTimeWithTimeZone 分支:之前它把值转成 Option<Vec<DateTime<Local>>>,现在 这个分支转成的是 Option<Vec<DateTime<FixedOffset>>>,和标量分支 Value::ChronoDateTimeWithTimeZone(Option<DateTime<FixedOffset>>) 一致。报告它的 #662 只有一句:「This is a bad copy-paste, it should be Option<Vec<DateTime<FixedOffset>>>」。这种逐分支手写的 match,每加一种类型就要在标量和数组两处各写一遍。

同一条 SQL,换一个 Value 分支

参数类型是 Parse 时声明的,而 sqlx 的语句缓存只按 SQL 文本查:get_or_prepare 命中就直接返回,不比较这次的参数类型(缓存的结构见扩展查询协议与语句缓存)。sea-query 生成的 SQL 只取决于语句的形状,不取决于值的类型,于是同一个查询换一种整数类型就会撞上(src/bin/samesql.rs,连接池只有一条连接):

use sea_orm::ConnectionTrait;
use sea_query::{Expr, ExprTrait, Query};

// The same SelectStatement shape, once with an i32 and once with an i64 value.
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
  let db = exp_seaorm_seaquery::connect(false).await?;
  db.execute_unprepared("drop table if exists item; create table item (id int8 primary key); insert into item values (1)").await?;
  let by_id = |v: sea_query::Value| Query::select().column("id").from("item").and_where(Expr::col("id").eq(v)).to_owned();
  println!("i32 first: {:?}", db.query_one(&by_id(1i32.into())).await.map(|r| r.is_some()));
  println!("then i64 : {:?}", db.query_one(&by_id(1i64.into())).await.map(|r| r.is_some()));
  db.execute_unprepared("drop table item").await?;
  Ok(())
}
i32 first: Ok(true)
then i64 : Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "22P03", message: "incorrect binary data format in bind parameter 1", detail: None, hint: None, position: None, where: Some("unnamed portal parameter $1"), schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("pqformat.c"), line: Some(1951), routine: Some("exec_bind_message") }))))

第一次 Parse 声明 $1 是 int4,语句进了缓存。第二次 SQL 文本一模一样,命中缓存、跳过 Parse,直接 Bind 一个 8 字节的 int8,服务端按 int4 读,报 22P03。换成 Int 再 String 也一样,报的是 08P01 insufficient data left in message(src/bin/misc.rs)。用 sea-orm 实体时列的 Rust 类型是固定的,Column::Id.eq(..) 却收任何 Into<Value>;同一个过滤条件在不同调用处传了 i32 和 i64,就会在同一条连接上先后出现这两种分支。

动脑筋答案

TinyInt(Some(1))             "char"                       null=false text=Some("\u{1}")

是 "char",Postgres 内部的单字节类型(OID 18),不是整数。Postgres 没有 1 字节整数,sqlx 把 i8 的 Type 定成 PgTypeInfo::CHAR,sea-query-sqlx 原样交过去,$1::text 读回来是控制字符 \u{1}。往 smallint 列里插这样的值(src/bin/misc.rs,每条 SQL 文本不同,没有缓存干扰):

insert SmallInt(Some(1))  -> Ok(1)
insert TinyInt(Some(1))   -> Err(Exec(SqlxError(Database(PgDatabaseError { severity: Error, code: "42804", message: "column \"x\" is of type smallint but expression is of type \"char\"", detail: None, hint: Some("You will need to rewrite or cast the expression."), position: Some(Original(30)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_target.c"), line: Some(586), routine: Some("transformAssignedExpr") }))))

"char" 到 smallint 没有赋值转换,服务端拒绝。和无符号整数不同,TinyInt 分支没有升级成 i16。

换成实体也一样:一个带 pub level: i8 字段的实体,Schema::create_table_from_entity 建出来的列是 smallint,插入就失败(src/bin/tiny_entity.rs,实体是 DeriveEntityModel 的展开写法,id: i32 主键加 level: i8):

let create = Schema::new(db.get_database_backend()).create_table_from_entity(tiny::Entity);
println!("{}", db.get_database_backend().build(&create));
db.execute(&create).await?;
let am = tiny::ActiveModel { id: Set(1), level: Set(1) };
println!("{:?}", tiny::Entity::insert(am).exec(&db).await.map(|r| r.last_insert_id));
CREATE TABLE "tiny_e" ( "id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "level" smallint NOT NULL )
Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42804", message: "column \"level\" is of type smallint but expression is of type \"char\"", detail: None, hint: Some("You will need to rewrite or cast the expression."), position: Some(Original(50)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_target.c"), line: Some(586), routine: Some("transformAssignedExpr") }))))

sea-orm 自己建的表,自己插不进去。Postgres 上的整数列用 i16 起步。

小结

  • sea-orm 的 Statement 是 SQL、Option<Values> 和后端;sqlx_query 把它变成 sqlx::query_with(AssertSqlSafe(sql), SqlxValues(values)),None 也换成空参数走预备语句,只有 execute_unprepared 走简单查询。
  • debug-print 打印的是值内联后的 SQL,sqlx 的 sqlx::query 日志才是带 $N 的真实语句。
  • SqlxValues 是为孤儿规则存在的 newtype;into_arguments 在执行器 take_arguments 时才调,逐分支选 Rust 类型 add,丢弃编码错误。
  • 每个 Value 分支对应一个 Postgres 类型,None 也带类型(Parse 里 Int(None) 是 OID 23);无符号升一级,Json 是 jsonb,三种带时区的 chrono 都是 timestamptz,TinyInt 是 "char"(实体的 i8 字段插不进 sea-orm 自己建的 smallint 列)。
  • 含 NULL 元素的 Value::Array 和超过 i64::MAX 的 BigUnsigned 在绑定时 panic;#1070 加了 Vec<Option<T>> 转换,但绑定路径没用上。
  • 同一条 SQL 换了 Value 分支(i32 换 i64),会命中按 SQL 文本缓存的预备语句,报 22P03 或 08P01。

💬 关联讨论

发起讨论
加载中...