column "level" is of type smallint but expression is of type "char"
主线位置:定义主线和写入主线的交界。一个
i8字段在定义主线上得到ColumnType::TinyInteger,建表时写成smallint;在写入主线上变成Value::TinyInt,绑到 sqlx 时用的是 sqlx 给i8定的 Postgres 类型"char"。两条线对同一个字段给出了两种类型。Value到 sqlx 参数的对应表见绑到 sqlx。
flowchart TB fld["字段 level: i8"] --> ct["ColumnType TinyInteger"] fld --> val["Value TinyInt"] ct --> ddl["建表: smallint"] val --> bind["sqlx: i8 是 char"] ddl --> clash["smallint 对 char"] bind --> clash clash --> e1["INSERT: 42804"] clash --> e2["WHERE: 42883"] clash --> e3["读取: mismatched types"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sea-orm = "=2.0.4",对照组 =1.1.20。
现象
src/bin/i8char.rs 用一个宏定义几个只有两列的实体(id: i32 主键加 level),level 分别是 i8、i16、u8。表由 Schema::create_table_from_entity 按实体建,也就是 sea-orm 自己认为正确的表:
level_entity!(tiny, "tiny", i8);
level_entity!(small, "small", i16);
level_entity!(unsigned, "unsigned8", u8);
// ...
let create = schema.create_table_from_entity(tiny::Entity);
println!("{}", db.get_database_backend().build(&create));
db.execute(&create).await?;
let r = tiny::Entity::insert(tiny::ActiveModel { id: Set(1), level: Set(1) }).exec(&db).await;
println!("insert: {r:?}");
db.execute_unprepared("INSERT INTO tiny VALUES (1, 1)").await?;
show("filter", tiny::Entity::find().filter(tiny::Column::Level.eq(1_i8)).all(&db).await);
show("select", tiny::Entity::find_by_id(1).one(&db).await);
输出(sqlx 日志只留了 INSERT 那一条):
=== i8
CREATE TABLE "tiny" ( "id" integer NOT NULL PRIMARY KEY, "level" smallint NOT NULL )
INFO sqlx::query: summary="INSERT INTO \"tiny\" (\"id\", …" db.statement="\n\nINSERT INTO \"tiny\" (\"id\", \"level\") VALUES ($1, $2) RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=1.79525ms elapsed_secs=0.00179525
insert: 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(48)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_target.c"), line: Some(586), routine: Some("transformAssignedExpr") }))))
filter: Query Error: error returned from database: operator does not exist: smallint = "char" at line 647
select: Query Error: error occurred while decoding column "level": mismatched types; Rust type `core::option::Option<i8>` (as SQL type `"CHAR"`) is not compatible with SQL type `INT2`
三处都坏:
- 插入:
$2的类型是"char",Postgres 没有从"char"到smallint的赋值转换,报42804。 - 过滤:
smallint = "char"没有这个运算符,报42883。 - 读取:行是手工用 SQL 插进去的,服务端返回
INT2,sqlx 检查 Rust 类型i8对应的"CHAR"和它不兼容,在解码前就拒绝了。Option<i8>是因为 sea-orm 总是向 sqlx 要Option<T>(列与值)。
本站用的 1.1.20(exp-seaorm-pit1-old/src/bin/i8char_old.rs,同样的实体,展开写法):
CREATE TABLE "tiny" ( "id" integer NOT NULL PRIMARY KEY, "level" smallint NOT NULL )
insert: Execution Error: error returned from database: column "level" is of type smallint but expression is of type "char"
filter: Query Error: error returned from database: operator does not exist: smallint = "char"
select: Query Error: error occurred while decoding column "level": mismatched types; Rust type `core::option::Option<i8>` (as SQL type `"CHAR"`) is not compatible with SQL type `INT2`
建表和三处报错都一样,只是插入那一条在 1.1.20 里包成 DbErr::Exec(Execution Error),2.0.4 里是 DbErr::Query。
出处
一个 i8 在三个地方各被翻译一次。
sea-query 的 type_to_value!(i8, TinyInt, TinyInteger) 定下 i8 的值是 Value::TinyInt、列类型是 ColumnType::TinyInteger。Postgres 没有 1 字节整数,Postgres 的建表代码把它升成 smallint:
ColumnType::TinyInteger | ColumnType::TinyUnsigned => sql.write_str("smallint"),
绑定时,sea-query-sqlx 的 Value::TinyInt 分支把 Option<i8> 原样交给 sqlx,没有升宽;隔几行的无符号分支是升宽了的:
Value::TinyInt(i) => {
let _ = args.add(i);
}
// ...
Value::TinyUnsigned(i) => {
let _ = args.add(i.map(|i| i as i16));
}
sqlx 的 impl Type<Postgres> for i8 返回 PgTypeInfo::CHAR,也就是 Postgres 内部那个单字节的 "char"(OID 18)。它的 Decode 里有一句注释说明了这个选择:「decoding here is for the "char" type as Postgres does not have a native 1-byte integer type」。所以建表的一侧说 smallint,绑定和解码的一侧说 "char",两边谁都没错,合在一起就不能用。
上游报过几次,都没有改:
- SeaQL/sea-orm#2632(「postgresql int2 mapped i8 error」,2025-06 提出,仍开着):和上面一样,用
create_table_from_entity建表,读回时报同一句mismatched types。 - SeaQL/sea-query#884(「When using Postgres, the
i8type cannot be queried. It indicates thati8does not match thechartype.」):以 not planned 关闭。 - SeaQL/sea-orm#1673(「i8 enum value is mistaken for a char in postgresql」):
DeriveActiveEnum写rs_type = "i8",插入integer列报column "xx" is of type integer but expression is of type "char"。维护者的回答是「changing thers_typeto any integer types excepti8will solve the problem. This limitation comes from the fact that Postgres treati8aschar.」,问题按已解决关闭,代码没有改。
问: 无符号的 u8 在绑定时升成了 i16,那把字段换成 u8 行不行?
答: 能写,不能读。同一个程序的最后一段:
=== u8
INFO sqlx::query: summary="INSERT INTO \"unsigned8\" (\"id\", …" db.statement="\n\nINSERT INTO \"unsigned8\" (\"id\", \"level\") VALUES ($1, $2) RETURNING \"id\"\n" rows_affected=1 rows_returned=1 elapsed=1.228375ms elapsed_secs=0.001228375
insert: Ok(1)
INFO sqlx::query: summary="SELECT \"unsigned8\".\"id\", \"unsigned8\".\"level\" FROM …" db.statement="\n\nSELECT \"unsigned8\".\"id\", \"unsigned8\".\"level\" FROM \"unsigned8\" WHERE \"unsigned8\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=567.125µs elapsed_secs=0.000567125
select: Type Error: u8 unsupported by sqlx-postgres
读取走 sea-orm 的 try_getable_unsigned!,它的 Postgres 分支不去读,直接返回 "{} unsupported by sqlx-postgres"。所以 u8 字段在 Postgres 上只写得进去,find 一律失败。
修法
字段换成 i16,和 smallint 一一对应。同一个程序的 i16 段:
=== i16
INFO sqlx::query: summary="CREATE TABLE \"small\" ( …" db.statement="\n\nCREATE TABLE \"small\" ( \"id\" integer NOT NULL PRIMARY KEY, \"level\" smallint NOT NULL )\n" rows_affected=0 rows_returned=0 elapsed=3.893084ms elapsed_secs=0.003893084
INFO sqlx::query: summary="INSERT INTO \"small\" (\"id\", …" db.statement="\n\nINSERT INTO \"small\" (\"id\", \"level\") VALUES ($1, $2) RETURNING \"id\"\n" rows_affected=1 rows_returned=1 elapsed=1.328042ms elapsed_secs=0.001328042
insert: Ok(1)
INFO sqlx::query: summary="SELECT \"small\".\"id\", \"small\".\"level\" FROM …" db.statement="\n\nSELECT \"small\".\"id\", \"small\".\"level\" FROM \"small\" WHERE \"small\".\"level\" = $1\n" rows_affected=1 rows_returned=1 elapsed=640.292µs elapsed_secs=0.000640292
filter: Ok([Model { id: 1, level: 1 }])
INFO sqlx::query: summary="SELECT \"small\".\"id\", \"small\".\"level\" FROM …" db.statement="\n\nSELECT \"small\".\"id\", \"small\".\"level\" FROM \"small\" WHERE \"small\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=586µs elapsed_secs=0.000586
select: Ok(Some(Model { id: 1, level: 1 }))
表结构不用动:i8 建出来的本来就是 smallint,改的只是 Rust 字段的类型。值域要限制在 -128..=127 的,在 ActiveModelBehavior::before_save 或数据库的 CHECK 约束里做,不要靠字段类型。DeriveActiveEnum 同理,rs_type 用 i16 或 i32,不用 "i8"。
怎么确认没中招
- 在实体目录里找
: i8、Option<i8>、: u8、Option<u8>字段和rs_type = "i8"/"u8"的DeriveActiveEnum。只要后端是 Postgres,这些都是坏的,而且编译期没有任何提示。 - 本站:按固定提交
89a61b1在crates/下找,实体里没有i8/u8字段,也没有DeriveActiveEnum,不受影响。
小结
i8在 sea-query 里是TinyInt/TinyInteger,Postgres 建表升成smallint;绑定和解码却按 sqlx 给i8定的"char"(OID 18)走。- 结果是插入报
42804、过滤报42883 operator does not exist: smallint = "char"、读取报mismatched types,2.0.4 和 1.1.20 一样。 - 上游有 sea-orm #2632(开着)、sea-query #884(not planned)、sea-orm #1673(回答是「除了
i8都行」),代码没改。 u8绑定时升成i16,写得进去,但读取报u8 unsupported by sqlx-postgres。- 修法:Postgres 上整数字段用
i16起步,表不用改。