Rust in Everything
基于 sqlx 0.9.0

类型: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_hintArguments::add(由 bind 调)
Decode<'r, DB>decode(value):从 DB::ValueRef<'r> 造出 SelfRow::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

Encode 的返回值是 IsNull:

#[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 的类型。

❗IMPORTANT
动脑筋: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)
    }
}
📝NOTE
冷知识: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")按名字找的类型,通常是 domainarray_of("email"),0.9 起才有
#[repr(i32)] enum Color { .. }转给 repr 的整数类型INT4 等转给整数类型
#[sqlx(type_name = "mood")] enum Mood { .. }Postgres:with_name;MySQL:__enum();SQLite:同 strPostgres 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 走到 _ 分支。
📝NOTE
没有蠢问题

问: 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)
                    }
                }
            ));
        }
    }
📝NOTE
冷知识:domain newtype 的数组实现是 0.9 才补上的

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 读不了它。

💬 关联讨论

发起讨论
加载中...