类型:Type、Encode、Decode
主线位置:查询主线的两端。开头,
bind(id)用Encode把值写进PgArguments的缓冲区,用Type::type_info记下参数类型;结尾,每条DataRow里的一列先过Type::compatible,再交给Decode变成 Rust 值。这一页只讲三个 trait 的约定和 sqlx-core 里的通用实现;Postgres 每种类型的二进制格式、OID、自定义类型怎么按名字解析,在 Postgres 类型。compatible在哪一步被调用、报什么错,在 Row 与 FromRow。
flowchart TB
bindv["bind(value)"] --> ti["Type::type_info"]
bindv --> enc["Encode::encode"]
ti --> types["PgArguments.types"]
enc --> isnull{IsNull}
isnull -->|No| bytes["长度 + 字节"]
isnull -->|Yes| neg["长度 -1"]
row["DataRow 的一列"] --> compat["Type::compatible"]
compat -->|兼容| dec["Decode::decode"]
compat -->|不兼容| mis["mismatched types"]
dec --> val["Rust 值"]
三个 trait 都以数据库为类型参数:Type<DB>、Encode<'q, DB>、Decode<'r, DB>。同一个 i32 对 Postgres 是 INT4,对 MySQL 是 INT,实现各写各的,靠 DB 区分(Database 的关联类型见 Database 与关联类型)。本页实验都在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上跑,依赖 sqlx = "=0.9.0"(features 见怎么读这一卷),表都建在自己的 schema 里。
三个 trait 各管什么
| trait | 方法 | 谁调用 |
|---|---|---|
Type<DB> | type_info():这个 Rust 类型声明成哪种 SQL 类型;compatible(ty):能不能从类型为 ty 的列解码 | bind 记参数类型;Row::try_get 检查列类型 |
Encode<'q, DB> | encode / encode_by_ref:写进 DB::ArgumentBuffer,返回 IsNull;produces:按值给出类型;size_hint | Arguments::add(由 bind 调) |
Decode<'r, DB> | decode(value):从 DB::ValueRef<'r> 造出 Self | Row::try_get / try_get_unchecked |
bind 要求 T: Encode + Type,try_get 要求 T: Decode + Type。Type 两边都要:编码时它告诉服务端参数是什么类型,解码时它决定接不接受这一列。
Type:声明和兼容
Type 只有一个必需方法:
pub trait Type<DB: Database> {
fn type_info() -> DB::TypeInfo;
fn compatible(ty: &DB::TypeInfo) -> bool {
Self::type_info().type_compatible(ty)
}
}
两个方法都是关联函数,没有 self:类型信息属于类型,不属于某个值。默认的 compatible 是「声明的类型和列的类型相等」,Postgres 的 PgTypeInfo::type_compatible 就是 self == other。需要放宽时由实现覆盖,str 的实现是典型:
impl Type<Postgres> for str {
fn type_info() -> PgTypeInfo {
PgTypeInfo::TEXT
}
fn compatible(ty: &PgTypeInfo) -> bool {
[
PgTypeInfo::TEXT,
PgTypeInfo::NAME,
PgTypeInfo::BPCHAR,
PgTypeInfo::VARCHAR,
PgTypeInfo::UNKNOWN,
PgTypeInfo::with_name("citext"),
]
.contains(ty)
}
}
绑定一个 &str 时声明 TEXT;读的时候 TEXT、VARCHAR、CHAR(n)、NAME、citext 都接受,因为它们在线上都是 UTF-8 字节。反过来,i32 没有覆盖 compatible,只认 INT4,count(*) 返回的 INT8 它不接受,哪怕值很小。这条规则是故意的:解码不看值,只看类型,INT8 的列下一行就可能放不进 i32。
引用、Option 和智能指针都把 Type 转给内层类型,「底层 SQL 类型一样」(源码注释原话 for references, the underlying SQL type is identical)。
Encode:写进缓冲,报告 NULL
#[must_use]
pub enum IsNull {
/// The value is null; no data was written.
Yes,
/// The value is not null.
///
/// This does not mean that data was written.
No,
}
/// Encode a single value to be sent to the database.
pub trait Encode<'q, DB: Database> {
/// Writes the value of `self` into `buf` in the expected format for the database.
fn encode(self, buf: &mut <DB as Database>::ArgumentBuffer) -> Result<IsNull, BoxDynError>
where
Self: Sized,
{
self.encode_by_ref(buf)
}
fn encode_by_ref(
&self,
buf: &mut <DB as Database>::ArgumentBuffer,
) -> Result<IsNull, BoxDynError>;
fn produces(&self) -> Option<DB::TypeInfo> {
// ...
None
}
#[inline]
fn size_hint(&self) -> usize {
mem::size_of_val(self)
}
}
NULL 不是一种值,而是「没有字节」。所以 Encode 不写一个特殊标记,而是告诉调用方「我没写」,由驱动按自己协议的方式表达 NULL。Postgres 驱动的 PgArgumentBuffer::encode 先占 4 个字节的长度,编码完回填:
// reserve space to write the prefixed length of the value
let offset = self.len();
self.extend(&[0; 4]);
// encode the value into our buffer
let len = if let IsNull::No = value.encode(self)? {
// Ensure that the value size does not overflow i32
value_size_int4_checked(self.len() - offset - 4)?
} else {
// Write a -1 to indicate NULL
// NOTE: It is illegal for [encode] to write any data
debug_assert_eq!(self.len(), offset + 4);
-1_i32
};
IsNull::Yes 的时候长度写 -1,并且 debug_assert 要求 encode 一个字节都没写。IsNull::No 的注释说「不代表写了数据」:空字符串就是不写字节、返回 No,长度 0,和 NULL 是两回事。Bind 消息里每个参数的长度前缀怎么排,在扩展查询协议与语句缓存。
另外三处细节:
- 为什么有两个 encode。
encode拿走self,encode_by_ref只借用。文档说按值的版本「can take advantage of re-using memory」,默认实现转给encode_by_ref;&T的实现反过来把encode转给T::encode_by_ref。 size_hint只是一次粗检查。 默认是size_of_val(self),对String来说是 24(三个字),和内容长度无关。Postgres 驱动只在编码前用它做一次i32溢出检查(注释:Won't catch everything but is a good sanity check),编码之后再按真实长度查一次。produces是按值给类型的钩子。 默认None,PgArguments::add用value.produces().unwrap_or_else(T::type_info)决定参数类型。0.9.0 里 Postgres 驱动只有几何类型(PgPoint等)和PgCube覆盖它;下面的Option<T>也用到了它。
add 在编码失败时把缓冲区恢复到快照,不留半个值;错误本身被 bind 存起来,到执行时才报,见 Query、QueryAs 与 SqlSafeStr。
Option:NULL 的唯一表达
SQL 的 NULL 在 Rust 里只有一种写法:Option<T>。三个 trait 都对它有通用实现。
编码一侧是一个宏 impl_encode_for_option!,每个驱动展开一次:
impl<'q, T> $crate::encode::Encode<'q, $DB> for Option<T>
where
T: $crate::encode::Encode<'q, $DB> + $crate::types::Type<$DB> + 'q,
{
#[inline]
fn produces(&self) -> Option<<$DB as $crate::database::Database>::TypeInfo> {
if let Some(v) = self {
v.produces()
} else {
T::type_info().into()
}
}
#[inline]
fn encode(
self,
buf: &mut <$DB as $crate::database::Database>::ArgumentBuffer,
) -> Result<$crate::encode::IsNull, $crate::error::BoxDynError> {
if let Some(v) = self {
v.encode(buf)
} else {
Ok($crate::encode::IsNull::Yes)
}
}
// ...
}
None 返回 IsNull::Yes,但 produces 仍然给出 T 的类型:None::<i32> 是「一个类型为 INT4 的 NULL」,不是「类型未知的 NULL」。这一点决定了服务端怎么推断 $1 的类型。
bind(None::<i32>) 之后,pg_typeof($1) 是什么?M2 Max、macOS、rustc 1.99.0、Postgres 16.14:let (is_null, ty): (bool, String) =
sqlx::query_as("select $1 is null, pg_typeof($1)::text")
.bind(None::<i32>)
.fetch_one(&pool)
.await?;
SQL 里没有给 $1 写任何类型。打印出来的 ty 是 unknown、text 还是别的?
解码一侧是 Option<T> 的 Decode:
impl<'r, DB, T> Decode<'r, DB> for Option<T>
where
DB: Database,
T: Decode<'r, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
if value.is_null() {
Ok(None)
} else {
Ok(Some(T::decode(value)?))
}
}
}
先问 ValueRef::is_null,是 NULL 就不调内层的 decode。不包 Option 时,NULL 一路走到具体类型的 decode,Postgres 的实现第一步是 PgValueRef::as_bytes,值为 None 就返回 UnexpectedNullError。实验(src/bin/decode_errors.rs 的前几行):
let row = sqlx::query("select null::int4 as n").fetch_one(&pool).await?;
println!("Option<i32> -> {:?}", row.try_get::<Option<i32>, _>("n"));
println!("i32 -> {}", row.try_get::<i32, _>("n").unwrap_err());
Option<i32> -> Ok(None)
i32 -> error occurred while decoding column "n": unexpected null; try decoding as an `Option`
Type for Option<T> 的 compatible 写成 ty.is_null() || T::compatible(ty),给「值的类型本身就是 NULL」的数据库留了口子:SQLite 的 SqliteTypeInfo::is_null 在类型是 DataType::Null 时为真。Postgres 的列总有类型,PgTypeInfo::is_null 恒为 false,这一半在 Postgres 上不起作用。
Decode 和 'r
Decode 的签名:
pub trait Decode<'r, DB: Database>: Sized {
/// Decode a new value of this type using a raw value from the database.
fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError>;
}
'r 是行的生命周期。PgValueRef<'r> 里装的是 &'r [u8],指向 DataRow 的缓冲区,所以 &'r str、&'r [u8] 可以直接借出来,不复制。和 serde 的 'de 是同一种设计(见 serde 卷的生命周期 'de 与借用)。错误类型是 BoxDynError,什么错都能装,Row::try_get 再把它包成 Error::ColumnDecode(见错误)。
借用在实际中用得不多:query_as、query_scalar 和 query! 都在解码完就扔掉行,借不出去。0.9 为 Box<T>、Arc<T>、Rc<T>、Cow<'_, T> 加了通用实现(#3674),另有 Arc<str>、Arc<[u8]> 和 Rc 版本(#3675)。Type 和 Encode 都是转给内层,Decode 是先解出内层再 Self::new。同一个程序(src/bin/smart_ptr.rs):
let name: Arc<str> = Arc::from("ann");
let row = sqlx::query("select $1 as a, $2 as b, $3 as c")
.bind(name.clone())
.bind(Box::new(42_i32))
.bind(Cow::Borrowed("borrowed"))
.fetch_one(&pool)
.await?;
let a: Arc<str> = row.try_get("a")?;
let b: Box<i32> = row.try_get("b")?;
let c: Cow<'_, str> = row.try_get("c")?;
let r: Rc<str> = row.try_get("a")?;
println!("Arc<str> {a:?}, Box<i32> {b:?}, Rc<str> {r:?}");
println!("Cow<str> {c:?} is_owned={}", matches!(c, Cow::Owned(_)));
0.9.0 的输出:
Arc<str> "ann", Box<i32> 42, Rc<str> "ann"
Cow<str> "borrowed" is_owned=true
换成 0.8.6 编译,Arc<str>、Box<i32>、Rc<str> 的每一处都报 E0277(节选):
error[E0277]: the trait bound `Arc<str>: sqlx::Encode<'_, _>` is not satisfied
error[E0277]: the trait bound `Box<i32>: sqlx::Encode<'_, _>` is not satisfied
error[E0277]: the trait bound `Rc<str>: sqlx::Decode<'_, Postgres>` is not satisfied
Cow<str> 在 0.8.6 的 Postgres 驱动里本来就有,所以那几行编译得过;变的是它解出来的样子。Cow 的 Decode:
impl<'r, DB, T> Decode<'r, DB> for Cow<'_, T>
where
DB: Database,
// `ToOwned` is required here to satisfy `Cow`
T: ToOwned + ?Sized,
<T as ToOwned>::Owned: Decode<'r, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
// See https://github.com/launchbadge/sqlx/pull/3674#discussion_r2008611502 for more info
// about why decoding to a `Cow::Owned` was chosen.
<<T as ToOwned>::Owned as Decode<DB>>::decode(value).map(Cow::Owned)
}
}
Cow<str> 在 0.9 不再借用0.8.6 的 Postgres 驱动给 Cow<'r, str> 单独写了 Decode,返回 Cow::Borrowed(value.as_str()?)。同一个只读 try_get 的程序,0.8.6 打印 is_owned=false,0.9.0 打印 is_owned=true。
改动来自 #3674(首次发布于 0.9.0)。代码注释指向的那条评审意见说借用版「不太实用」:there is basically never a row retained that can be borrowed from. query_as(), query_scalar() and all the macro variants throw away the row value after deserializing. #[derive(FromRow)] doesn't support borrowed values. 并且拿 serde 作对照:serde 的 Cow 也只反序列化成 Owned。CHANGELOG 把它列为破坏性改动:impl Decode for Cow now always decodes Cow::Owned, lifetime is unlinked。现在 Cow 的 Decode 只是先解出 T::Owned 再包一层,生命周期和行脱钩。
Json:用 serde 编解码
json feature 下的 Json<T> 是一个透明的包装:
#[derive(
Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct Json<T: ?Sized>(pub T);
T: Serialize 时能 bind,T: Deserialize 时能 try_get。sqlx-core 里只有几个「给驱动用」的辅助方法(encode_to、decode_from_bytes,都标着 UNSTABLE: for driver use only!),本身就是 serde_json::to_writer / from_slice;serde 怎么把字节变成 T,见 serde 卷的 serde_json 的 Deserializer。Postgres 一侧的 Type for Json<T> 声明 JSONB,compatible 接受 JSON 和 JSONB。jsonb 二进制格式前面的版本字节,在 Postgres 类型。
serde_json::Value(导出为 JsonValue)和 RawValue(JsonRawValue)的实现都是转给 Json<Self> / Json<&Self>。0.9 补上了 JsonRawValue、&JsonRawValue、Box<JsonRawValue> 的 Encode 和 Box<JsonRawValue> 的 Decode(#3859),并给 Json 加了 into_inner(#4123)。
#[derive(sqlx::Type)]:五种形态
derive 的入口 expand_derive_type 按结构分派:
match &input.data {
// Newtype structs:
// struct Foo(i32);
// struct Foo { field: i32 };
Data::Struct(DataStruct { fields, .. })
if fields.len() == 1 && (matches!(fields, Fields::Unnamed(_)) || attrs.transparent) =>
{
expand_derive_has_sql_type_transparent(input, fields.iter().next().unwrap())
}
// Record types
// struct Foo { foo: i32, bar: String }
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => expand_derive_has_sql_type_struct(input, named),
// ...
Data::Enum(DataEnum { variants, .. }) => match attrs.repr {
// Enums that encode to/from integers (weak enums)
Some(_) => expand_derive_has_sql_type_weak_enum(input, variants),
// Enums that decode to/from strings (strong enums)
None => expand_derive_has_sql_type_strong_enum(input, variants),
},
// ...
}
「单字段命名结构体也能 transparent」(struct Foo { field: i32 })是 0.9 加的(#3971),0.8.6 只认单字段元组结构体。整理成表:
| 写法 | 生成的 Type | 对应的 SQL 类型 | PgHasArrayType |
|---|---|---|---|
#[sqlx(transparent)] struct PersonId(i64) | 对所有 DB 泛型,转给内层 | 内层的类型(INT8) | 转给内层的 array_type_info |
#[sqlx(type_name = "email")] struct Email(String) | 只有 Postgres,with_name("email") | 按名字找的类型,通常是 domain | array_of("email"),0.9 起才有 |
#[repr(i32)] enum Color { .. } | 转给 repr 的整数类型 | INT4 等 | 转给整数类型 |
#[sqlx(type_name = "mood")] enum Mood { .. } | Postgres:with_name;MySQL:__enum();SQLite:同 str | Postgres enum;SQLite 是文本 | array_of("mood") |
#[sqlx(type_name = "pair")] struct Pair { a: i32, b: String } | 只有 Postgres,with_name("pair") | 复合类型 | array_of("pair") |
type_name 不写时用 Rust 类型名本身(type_name 函数里的 ident.to_string())。按名字声明的类型,OID 要到执行时才去服务端查,名字查不到就报 TypeNotFound;查询过程、patches 和缓存在 Postgres 类型。字符串枚举的 Decode 先按 &str 解,再 match 变体名(expand_derive_decode_strong_enum),匹配不上报 invalid value {:?} for enum {}。
实验建了一个 enum、一个 domain 和一个复合类型:
create type mood as enum ('happy', 'sad');
create domain email as text check (value like '%@%');
create type pair as (a int4, b text);
src/bin/derive_type.rs 把上表的几种都 derive 一遍(Ghost 的 type_name 是一个不存在的 no_such_type;EmailText 是 #[sqlx(transparent)] struct EmailText(String)),先打印各自的 type_info 和 array_type_info,再逐个查:
Mood type_info="mood" array="mood[]"
PersonId type_info="INT8" array="INT8[]"
Email type_info="email" array="email[]"
Pair type_info="pair" array="pair[]"
rows [(PersonId(1), Some(Happy)), (PersonId(2), None)]
mood any 1
email any 1
email col error occurred while decoding column 0: mismatched types; Rust type `derive_type::Email` (as SQL type `email`) is not compatible with SQL type `TEXT`
email col as transparent EmailText("ann@x.io")
email[] [Email("ann@x.io"), Email("bob@x.io")]
pair Pair { a: 7, b: "seven" }
ghost type named no_such_type not found
text->Mood error occurred while decoding column "m": mismatched types; Rust type `derive_type::Mood` (as SQL type `mood`) is not compatible with SQL type `TEXT`
angry error returned from database: invalid input value for enum mood: "angry" at line 129
meh error occurred while decoding column 0: invalid value "meh" for enum Mood
逐行看:
rows、mood any、email any:Vec<PersonId>、Vec<Mood>、Vec<Email>都能作为数组参数绑定给= any($1),靠的是生成的PgHasArrayType。email col:Email声明的是email,可这一列报上来的类型是TEXT,于是不兼容。换成 transparent 的EmailText(按TEXT比较)就能读。而email[]数组能解成Vec<Email>。ghost:no_such_type在执行时查不到,报TypeNotFound。text->Mood:一个text列不能直接读成Mood,哪怕内容恰好是happy,因为compatible比较的是类型,不是值。angry:'angry'::mood是服务端报的错,后缀at line 129是 0.9 新加的,含义见错误。meh:先alter type mood add value 'meh',Rust 这边没有这个变体,生成的Decode走到_分支。
问: Email 能绑定,email[] 也能解,为什么单独一个 email 列解不了?
答: try_get 比较的是 RowDescription 里这一列的类型,实验里它是 TEXT,不是 email;数组列报上来的则是 email[] 自己的类型。所以按 domain 名字声明的 newtype 适合绑参数和读 domain 数组;单列要读,用 #[sqlx(transparent)] 的 newtype 按基础类型比较。
透明的那一支只是把两个关联函数转给内层(expand_derive_has_sql_type_transparent),并且对任意 DB 泛型,所以一个 PersonId 同时能用于 Postgres 和 SQLite。不带 transparent 的单字段元组结构体走同一个函数的后半段,只为 Postgres 生成:
let mut tts = TokenStream::new();
if cfg!(feature = "postgres") {
let ty_name = type_name(ident, attr.type_name.as_ref());
tts.extend(quote!(
#[automatically_derived]
impl ::sqlx::Type<::sqlx::postgres::Postgres> for #ident #ty_generics {
fn type_info() -> ::sqlx::postgres::PgTypeInfo {
::sqlx::postgres::PgTypeInfo::with_name(#ty_name)
}
}
));
if !attr.no_pg_array {
tts.extend(quote!(
#[automatically_derived]
impl ::sqlx::postgres::PgHasArrayType for #ident #ty_generics {
fn array_type_info() -> ::sqlx::postgres::PgTypeInfo {
::sqlx::postgres::PgTypeInfo::array_of(#ty_name)
}
}
));
}
}
0.8.6 里,上面的 if !attr.no_pg_array { ... } 这一段不存在:transparent newtype、两种枚举、复合类型都会生成 PgHasArrayType,唯独「按名字声明的元组 newtype」不生成。同一个读 email[] 的程序在 0.8.6 下编译不过:
error[E0277]: the trait bound `Email: PgHasArrayType` is not satisfied
--> src/bin/domain_array.rs:12:6
|
12 | .fetch_one(&pool)
| ^^^^^^^^^ unsatisfied trait bound
|
help: the trait `PgHasArrayType` is not implemented for `Email`
#3996 报告的正是这个场景:一个包着 VARCHAR 的 domain,表里是 TITLE[] 列,标题是 derive from sqlx::Type does not automatically generate PgHasArrayType implementation for newtype without #[sqlx(transparent)] attribute,当时只能手写 array_of("title")。修复 #4008 首次发布于 0.9.0,PR 说明这是破坏性改动:People who use domain types in PostgreSQL have likely already implemented the PgHasArrayType trait manually ... my changes would introduce a duplicate implementation of the trait. The solution would be to either remove the manual implementation or add the #[sqlx(no_pg_array)] attribute to the struct.
#[sqlx(no_pg_array)] 的另一个用处写在 Type 的文档里:包着 Vec<i64> 的 transparent newtype,内层没有 PgHasArrayType(sqlx 不支持多维数组),生成的数组实现的 where 子句满足不了,要用它关掉。
动脑筋答案
src/bin/decode_errors.rs 的第一行输出:
bind None::<i32> -> is_null=true pg_typeof=integer
is_null=true:Option::encode返回IsNull::Yes,PgArgumentBuffer给这个参数写长度-1。pg_typeof=integer:SQL 没写类型,可None::<i32>的produces返回了T::type_info(),也就是INT4,sqlx 把它当作$1的类型随Parse发给服务端,服务端就按integer推断。NULL 也带着类型。
小结
Type有两个关联函数:type_info声明绑定时的 SQL 类型,compatible决定解码时接受哪些列类型,默认要求相等,str这类实现会放宽。Encode写进驱动的参数缓冲,用IsNull报告「没写」;Postgres 驱动据此写长度-1。size_hint只是溢出粗检查,produces让值决定类型。- NULL 只能用
Option<T>表达:编码时None仍带着T的类型,解码时先看is_null;不包Option读到 NULL 报unexpected null; try decoding as an \Option``。 Decode<'r>能从行里借数据,但query_as等会扔掉行;0.9 加了Box、Arc、Rc、Cow的通用实现,Cow一律解成Owned。#[derive(sqlx::Type)]有五种形态:transparent newtype、按名字的 newtype(domain)、整数枚举、字符串枚举、复合类型;0.9 起按名字的 newtype 也生成PgHasArrayType。domain 单列报上来的是基础类型,按 domain 名字声明的 newtype 读不了它。