绑到 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(())
}
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 / BigInt | Option<bool> / Option<i16> / Option<i32> / Option<i64> | boolean / smallint / integer / bigint |
TinyUnsigned / SmallUnsigned / Unsigned | 升一级:i16 / i32 / i64 | smallint / integer / bigint |
BigUnsigned | i64,try_from(u64).unwrap() | bigint,超过 i64::MAX panic |
String / Char / Enum | Option<&str> / Option<String> / Option<String> | text(枚举的类型由 SQL 里的 ::"类型名" 转换) |
Json | Option<&serde_json::Value> | jsonb |
ChronoDateTime | Option<NaiveDateTime> | timestamp without time zone |
ChronoDateTimeUtc / Local / WithTimeZone | Option<DateTime<Utc / Local / FixedOffset>> | timestamp with time zone |
Uuid | Option<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> 装不下。
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。