Rust in Everything
基于 sqlx 0.9.0

Postgres 类型

主线位置:查询主线的 bind 到 Bind 之间,以及 RowDescription 到 PgRow 之间。bind 记下每个参数的 PgTypeInfo;prepare 在发 Parse 之前把其中只有名字的类型解析成 OID;编码时不知道的 OID 在 Bind 之前补上;结果列的 OID 再反查回 PgTypeInfo。Type、Encode、Decode 三个 trait 的约定和 #[derive(sqlx::Type)] 的形态在类型:Type、Encode、Decode,这一页只讲 Postgres 驱动这一层。

flowchart TB
  bindv["bind(value)"] --> ti{"有 OID 吗"}
  ti -->|内建类型| oid["直接用 OID"]
  ti -->|只有名字| res["resolve_types"]
  res --> hit{"连接上的缓存"}
  hit -->|命中| oid
  hit -->|没命中| tq["Query 查 pg_type"]
  tq --> oid
  oid --> parse["Parse 带参数 OID"]
  parse --> pd["ParameterDescription"]
  pd --> patch["apply_patches 补洞"]
  patch --> bindmsg["Bind 二进制参数"]

本页实验都在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上跑,依赖 sqlx = "=0.9.0",类型和表都建在自己的 schema pg2 里。看协议消息用的是一次查询的全过程里那个 TCP 代理,这一页给它加了两样:Bind 后面按「[长度] 十六进制」打出每个参数的字节,超过 60 个字符的 SQL 截断成 …。

PgTypeInfo:一个 OID,或者一个名字

Postgres 用 OID 标识类型。Parse 消息里给参数声明类型、ParameterDescription 和 RowDescription 报告类型,用的都是 OID,协议里没有类型名。PgTypeInfo 包着一个 PgType,前面 92 个变体是 sqlx 认识的内建类型和伪类型 Void(Int4、Text、Timestamptz、Uuid、Jsonb……),最后四个才是这一页的主角:

    // A realized user-defined type. When a connection sees a DeclareXX variant it resolves
    // into this one before passing it along to `accepts` or inside of `Value` objects.
    Custom(Arc<PgCustomType>),

    // From [`PgTypeInfo::with_name`]
    DeclareWithName(UStr),

    // NOTE: Do we want to bring back type declaration by ID? It's notoriously fragile but
    //       someone may have a user for it
    DeclareWithOid(Oid),

    DeclareArrayOf(Arc<PgArrayOf>),
}

内建类型的 OID 写死在两张 match 表里:PgType::try_from_oid 从 OID 到变体(23 => PgType::Int4、1184 => PgType::Timestamptz、2950 => PgType::Uuid、3802 => PgType::Jsonb),try_oid 反过来。写死是可以的,因为这些 OID 来自 Postgres 源码里的 pg_type.dat,每个库都一样。

用户自己建的类型不行。create type mood as enum (...) 拿到的 OID 取决于这个库之前建过多少对象,换一个库就不同。oid() 的文档写得很直接:type OIDs are not stable in PostgreSQL。所以 derive 出来的 Type 只能声明名字:#[sqlx(type_name = "mood")] 生成 PgTypeInfo::with_name("mood"),也就是 DeclareWithName;数组用 PgTypeInfo::array_of("mood"),也就是 DeclareArrayOf。这两种变体的 try_oid() 返回 None,要等一条活的连接去库里查。查到之后,连接自己用的是 Custom:OID、名字和类型的种类(PgTypeKind:Enum 带着标签列表,Composite 带着字段,Domain 带着基础类型,Array 带着元素类型……)。

名字在 Parse 之前解析

查名字的地方在 prepare 的开头,发 Parse 之前:

    // build a list of type OIDs to send to the database in the PARSE command
    // we have not yet started the query sequence, so we are *safe* to cleanly make
    // additional queries here to get any missing OIDs
    let param_types = conn.resolve_types(arg_types).await?;

注释说的「安全」是指:这时候连接上还没有发出去、等着回应的扩展查询,可以插一条别的查询进去。resolve_types 先走快路径:逐个用 try_type_to_oid 查,内建类型直接给 OID,名字查 cache_type_oid,全部查到就返回,一个字节都不发。查不到的才交给 TypeResolver,每个名字变成 VALUES 里的一行 (E'mood', to_regtype(E'mood')::oid),数组还会顺带把元素类型也放进去。TypeResolver::push_type 拼出的查询从 pg_type 一次取回 OID、regtype 文本、typtype、typcategory、元素类型、基础类型、enum 的标签、复合类型的字段,fill_cache 用 raw_sql 执行它:

            let types = raw_sql(AssertSqlSafe(query)).fetch_all(&mut *conn).await?;

to_regtype 在类型不存在时返回 NULL 而不是报错,所以一个名字查不到不会让整条查询失败;之后 try_type_to_oid 还是 None,resolve_types 才报 TypeNotFound(02-core 的 ghost 实验就是这个错)。这条查询故意用简单查询协议,文件开头的注释给了理由:类型解析可能发生在一次扩展查询的中间;有些兼容 Postgres 协议的数据库不支持命名预备语句,用未命名语句又会顶掉用户正要执行的那一条。fill_cache 外面还套着一个最多 64 次的循环:一行结果依赖的类型(数组的元素、domain 的基础类型、复合类型的字段)还不认识时,try_cache_type 返回 ControlFlow::Break(oid),把那个 OID 再推进下一轮查询。

查到的东西存在 PgConnectionInner 的四张表里,每条连接一份,跟连接同生同死:

字段键 → 值谁用
cache_type_oid名字 → OID(regtype 文本、typname、原来的写法都存一份)参数:DeclareWithName 找 OID
cache_elem_type_to_array元素 OID → 数组 OID参数:DeclareArrayOf 找 OID
cache_type_infoOID → PgTypeInfo(Custom)结果列:OID 反查类型
cache_table_data表 OID → 表名和列名describe 的列来源,见 describe 与可空性推断

实验:第一次绑定 enum 多一个来回

src/bin/type_holes.rs 建一个 enum,在同一条连接上依次执行四条查询,再开一条新连接执行第五条:

#[derive(sqlx::Type, Debug)]
#[sqlx(type_name = "mood", rename_all = "lowercase")]
enum Mood {
  Happy,
  Sad,
}

// A、B:同一条 SQL 两次
sqlx::query_scalar("select $1::text").bind(Mood::Happy).fetch_one(&mut conn).await?;
sqlx::query_scalar("select $1::text").bind(Mood::Sad).fetch_one(&mut conn).await?;
// C:另一条 SQL,同一个类型
sqlx::query_scalar("select $1 = 'sad'::mood").bind(Mood::Sad).fetch_one(&mut conn).await?;
// D:数组
sqlx::query_scalar("select cardinality($1)").bind(vec![Mood::Happy, Mood::Sad]).fetch_one(&mut conn).await?;
// E:新连接,结果列是 mood,没有参数
sqlx::query_scalar("select 'happy'::mood").fetch_one(&mut conn2).await?;
--- A: 第一次绑定 Mood ---
  [conn 1] C->S Query("SELECT pg_type.oid, pg_type.oid::regtype::text pretty_name, …")
  [conn 1] S->C RowDescription, DataRow(列长 5/4/4/4/1/1/-1/-1/-1/11/2), CommandComplete("SELECT 1"), ReadyForQuery(I)
  [conn 1] C->S Parse(sqlx_s_1, "select $1::text"), 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; [5] 68 61 70 70 79), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 5), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 "happy"
--- B: 同一条 SQL 再来一次 ---
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1; [3] 73 61 64), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 3), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 "sad"
--- C: 另一条 SQL,同一个类型 ---
  [conn 1] C->S Parse(sqlx_s_2, "select $1 = 'sad'::mood"), Describe(S, sqlx_s_2), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_2; [3] 73 61 64), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 1), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 true
--- D: Vec<Mood>,数组类型第一次出现 ---
  [conn 1] C->S Query("SELECT pg_type.oid, pg_type.oid::regtype::text pretty_name, …")
  [conn 1] S->C RowDescription, DataRow(列长 5/6/5/6/1/1/5/-1/-1/2/2), DataRow(列长 5/4/4/4/1/1/-1/-1/-1/11/2), CommandComplete("SELECT 2"), ReadyForQuery(I)
  [conn 1] C->S Parse(sqlx_s_3, "select cardinality($1)"), Describe(S, sqlx_s_3), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_3; [36] 00 00 00 01 00 00 00 00 00 00 40 7e 00 00 00 02 00 00 00 01 00 00 00 05 68 61 70 70 79 00 00 00 03 73 61 64), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 2
--- E: 新连接,结果列是 mood ---
  [conn 2] C->S Parse(sqlx_s_1, "select 'happy'::mood"), Describe(S, sqlx_s_1), Sync
  [conn 2] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 2] C->S Query("SELECT pg_type.oid, pg_type.oid::regtype::text pretty_name, …")
  [conn 2] S->C RowDescription, DataRow(列长 5/4/4/-1/1/1/-1/-1/-1/11/2), CommandComplete("SELECT 1"), ReadyForQuery(I)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, DataRow(列长 5), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 Happy

逐段看:

  • A:语句缓存没命中,要 prepare;参数类型 mood 不在 cache_type_oid 里,于是在 Parse 之前多了一个来回,一条 Query 查回一行。这一行的第 10 列(长度 11)是 enum 的标签数组 {happy,sad}。之后才是平常的 Parse 那一来回和 Bind 那一来回,见扩展查询协议与语句缓存。enum 的二进制格式就是标签的文本,happy 编成 5 个字节 68 61 70 70 79。
  • B:语句缓存命中,连 prepare 都不进,更不会查类型。
  • C:SQL 换了,prepare 要重新做,但 mood 已经在缓存里,resolve_types 走快路径,没有类型查询。类型缓存和语句缓存是两回事:前者按名字,后者按 SQL 文本。
  • D:Vec<Mood> 的类型是 array_of("mood"),mood[] 第一次出现,又查一次。这次 VALUES 里有两行:mood[] 自己(regtype 文本 mood[] 6 个字节,typname 是 _mood 5 个字节,第 7 列 typelem 非 NULL),还有被顺带推进去的元素类型 mood。数组的字节下一节拆开讲。
  • E:新连接的缓存是空的。这回没有参数,resolve_types 无事可做;mood 是在 RowDescription 里出现的,resolve_statement_metadata 用 OID 去查 cache_type_info,没查到,就以 ("NULL", oid) 把它推给 TypeResolver(按 OID 查,所以 original_name 那一列是 -1)。查询夹在 Parse 那一来回和 Bind 那一来回之间,这时上一个 Sync 的 ReadyForQuery 已经读完,同样「安全」。
📝NOTE
冷知识:0.8.6 查一个 enum 要三条预备语句

0.8.6 没有 TypeResolver。按名字找 OID 是 fetch_type_id_by_name 里的 query_as("SELECT $1::regtype::oid"),按 OID 找类型是 fetch_type_by_oid,enum 再加一条 fetch_enum_by_oid 查标签,复合类型、domain、range 各有自己的一条,每条都是普通的预备语句,各自一个 Parse 来回加一个 Bind 来回。同一个程序换成 0.8.6,A 段是这样(节选):

--- A: 第一次绑定 Mood ---
  [conn 1] C->S Parse(sqlx_s_2, "SELECT $1::regtype::oid"), Describe(S, sqlx_s_2), Sync
  ...
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_2; [4] 6d 6f 6f 64), Execute, Close(P, unnamed), Sync
  ...
  [conn 1] C->S Parse(sqlx_s_1, "select $1::text"), Describe(S, sqlx_s_1), Sync
  ...
  [conn 1] C->S Parse(sqlx_s_3, "SELECT oid::regtype::text, typtype, typcategory, typrelid, t…"), Describe(S, sqlx_s_3), Sync
  ...
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_3; [4] 00 00 40 89), Execute, Close(P, unnamed), Sync
  ...
  [conn 1] C->S Parse(sqlx_s_4, "SELECT enumlabel FROM pg_catalog.pg_enum WHERE enumtypid = $…"), Describe(S, sqlx_s_4), Sync
  ...
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_4; [4] 00 00 40 89), Execute, Close(P, unnamed), Sync
  ...
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1; [5] 68 61 70 70 79), Execute, Close(P, unnamed), Sync

8 个来回,0.9.0 是 3 个;三条元数据查询还各占了一个语句缓存的位置(sqlx_s_2 到 sqlx_s_4)。改动来自 #4226,首次发布于 0.9.0,PR 说明:Avoids the use of prepared statements to fetch type metadata to improve compatibility with third-party databases that don't support named prepared statements. Additionally, metadata queries are batched as much as possible, to reduce round-trips to the server. 现在一次 prepare 里所有不认识的类型拼成一条 VALUES,用 raw_sql 发出去。

洞和补丁:编码时还不知道的东西

resolve_types 解决的是 Parse 里的参数类型。还有一类 OID 藏在参数的字节里:Postgres 数组的二进制格式要写元素类型的 OID,复合类型要写每个字段的 OID。bind 调 Encode 的时候还没有连接,Vec<Mood> 编码时根本不知道 mood 的 OID 是多少。

PgArgumentBuffer 的做法是先占位、后回填。数组的编码在 PgBindIter::encode_inner(&[T] 和 Vec<T> 都转到这里):

        buf.extend(&1_i32.to_be_bytes()); // number of dimensions
        buf.extend(&0_i32.to_be_bytes()); // flags

        if let Some(oid) = type_info.oid() {
            buf.extend(oid.0.to_be_bytes());
        } else {
            buf.push_hole(type_info);
        }

        let len_start = buf.len();
        buf.extend(0_i32.to_be_bytes()); // len (unknown so far)
        buf.extend(1_i32.to_be_bytes()); // lower bound

元素类型有 OID(Vec<i32>)就直接写;没有就 push_hole:写 4 个零字节,把偏移量记进 hole_offsets、类型记进 hole_types。执行时,run 在 prepare 之后、写 Bind 之前调 apply_patches:

        let resolved_holes = conn.resolve_types(hole_types).await?;

        for (&offset, oid) in hole_offsets.iter().zip(resolved_holes) {
            buffer[offset..][..4].copy_from_slice(&oid.0.to_be_bytes());
        }

还是 resolve_types,所以洞里的类型同样先查缓存。实验 D 段里,mood 在 prepare 解析 mood[] 时已经被顺带放进缓存,补洞没有再发查询。那 36 个字节拆开是:

字节含义
00 00 00 01维数 1
00 00 00 00flags
00 00 40 7e元素类型 OID 16510,就是补上的洞(select 'pg2.mood'::regtype::oid 也是 16510)
00 00 00 02元素个数,编完所有元素才回填
00 00 00 01下界 1
00 00 00 05 68 61 70 70 79第一个元素:长度 5,happy
00 00 00 03 73 61 64第二个元素:长度 3,sad

每个元素和顶层参数一样用 PgArgumentBuffer::encode 写,前面是 4 字节长度,NULL 是 -1。复合类型的编码器 PgRecordEncoder::encode 遇到没有 OID 的字段类型时也调 push_hole。解码一侧,Vec<T> 的 Decode 读同样的头,维数不是 1、下界不是 1 都直接报错:sqlx 只支持一维、从 1 开始的数组。Vec<T> 的 Type 来自元素的 PgHasArrayType,derive 什么时候生成它见 02-core 的类型页。

另一种是补丁(patches):值本身的字节取决于参数最终是什么类型。patch_with 记下当前偏移量和参数序号,apply_patches 拿 ParameterDescription 里的类型调回调。0.9.0 里唯一的用户是 Json<T> 的 Encode:

    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
        // we have a tiny amount of dynamic behavior depending if we are resolved to be JSON
        // instead of JSONB
        buf.patch_with(|buf, ty: &PgTypeInfo| {
            if *ty == PgTypeInfo::JSON || *ty == PgTypeInfo::JSON_ARRAY {
                buf[0] = b' ';
            }
        });

        // JSONB version (as of 2020-03-20)
        buf.push(1);

        // the JSON data written to the buffer is the same regardless of parameter type
        serde_json::to_writer(&mut **buf, &self.0)?;

        Ok(IsNull::No)
    }

jsonb 的二进制格式是一个版本字节 1 加 JSON 文本;json 的二进制格式就是 JSON 文本本身。Json<T> 声明的是 JSONB,先按 jsonb 写;如果服务端说这个参数其实是 json,就把版本字节改成一个空格。长度不变,所以只能是空格:JSON 允许前导空白。补丁要等 ParameterDescription,正常的 bind 不会触发它,因为 Parse 里已经声明了 JSONB,服务端照单全收。要看到它,得用一条参数类型声明成 json 的预备语句(src/bin/bind_bytes.rs 的最后一段):

let stmt = conn.prepare_with("select $1::text".into_sql_str(), &[PgTypeInfo::with_oid(Oid(114))]).await?;
let s: String = stmt.query().bind(Json(serde_json::json!({"a": 1}))).fetch_one(&mut conn).await?.get(0);
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_3; [8] 20 7b 22 61 22 3a 31 7d), Execute, Close(P, unnamed), Sync
结果 " {\"a\":1}"

第一个字节是 20(空格),不是 01。json 类型原样保存输入文本,那个空格一直留到了 ::text 的结果里。

几种类型的二进制格式

Bind 给所有参数和所有结果列都要二进制格式(formats: &[PgValueFormat::Binary]、result_formats: &[PgValueFormat::Binary],见扩展查询协议与语句缓存)。src/bin/bind_bytes.rs 一次绑七个值,让服务端用 pg_typeof 报出它推断的类型:

let utc: DateTime<Utc> = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 1).unwrap();
let east8: DateTime<FixedOffset> = utc.with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap());
let naive: NaiveDateTime = utc.naive_utc();
let id = Uuid::parse_str("67e55044-10b1-426f-9247-bb680e5fe0c8")?;

let row = sqlx::query(
  "select pg_typeof($1)::text, pg_typeof($2)::text, pg_typeof($3)::text, pg_typeof($4)::text,
          pg_typeof($5)::text, pg_typeof($6)::text, pg_typeof($7)::text",
)
.bind(258_i32)
.bind("hi")
.bind(utc)
.bind(east8)
.bind(naive)
.bind(id)
.bind(Json(serde_json::json!({"a": 1})))
.fetch_one(&mut conn)
.await?;
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1; [4] 00 00 01 02; [2] 68 69; [8] 00 00 00 00 00 0f 42 40; [8] 00 00 00 00 00 0f 42 40; [8] 00 00 00 00 00 0f 42 40; [16] 67 e5 50 44 10 b1 42 6f 92 47 bb 68 0e 5f e0 c8; [8] 01 7b 22 61 22 3a 31 7d), Execute, Close(P, unnamed), Sync
pg_typeof ["integer", "text", "timestamp with time zone", "timestamp with time zone", "timestamp without time zone", "uuid", "jsonb"]
Rust 值SQL 类型字节编码
258_i32INT400 00 01 02i32:to_be_bytes,大端 4 字节
"hi"TEXT68 69&str:UTF-8 字节,没有结尾的 \0,长度在前缀里
DateTime<Utc>TIMESTAMPTZ00 00 00 00 00 0f 42 402000-01-01 起的微秒数,1 秒 = 1000000 = 0x0f4240
DateTime<FixedOffset>(+08:00,同一时刻)TIMESTAMPTZ同上先 naive_utc(),偏移量不上线
NaiveDateTimeTIMESTAMP同上同样的微秒数,只是类型不同
UuidUUID16 字节原样Uuid:as_bytes()
Json(..)JSONB01 + {"a":1}版本字节 1,再接 serde_json::to_writer 的输出

时间这三行值得多看一眼。DateTime<Tz> 的 Encode 只有一行,Encode::<Postgres>::encode(self.naive_utc(), buf),转给 NaiveDateTime,后者减去 postgres_epoch_datetime(2000-01-01 00:00:00)再按 i64 写。TIMESTAMPTZ 和 TIMESTAMP 在线上的字节格式完全一样,区别只在 Type:带时区的 DateTime<Tz> 声明 TIMESTAMPTZ,NaiveDateTime 声明 TIMESTAMP。TIMESTAMPTZ 存的是一个绝对时刻,不存偏移量,所以 +08:00 编出来和 UTC 一模一样;解码时 DateTime<FixedOffset> 拿回来的偏移量永远是 +00:00。拿 DateTime<Utc> 去读 TIMESTAMP 列会因为 compatible 报类型不匹配,见 02-core 的类型页。

读回来的方向,jsonb 的版本字节由 Json<T> 的 Decode 检查并跳过:

        if value.format() == PgValueFormat::Binary && value.type_info == PgTypeInfo::JSONB {
            // Check JSONB version byte - PostgreSQL currently only supports version 1
            if buf[0] != 1 {
                return Err(
                    format!("unsupported JSONB format version {} (expected 1)", buf[0]).into(),
                );
            }

            buf = &buf[1..];
        }

同一个程序读 select '{"a":1}'::jsonb, '{"a":1}'::json,DataRow 的两列长度是 9/7:jsonb 输出的是规范化后的 {"a": 1}(冒号后多一个空格,8 个字节)加版本字节;json 是输入的 7 个字节原样。

结果列:OID 反查,和简单查询的例外

参数的类型从 Rust 来,结果列的类型从服务端来。RowDescription 里每一列只有一个 OID,resolve_statement_metadata 用 try_oid_to_type 把它变回 PgTypeInfo:先查内建表,再查 cache_type_info。在 prepare 里(const 泛型参数 QUERIES_ALLOWED 为 true)查不到的会先去库里查(实验 E 段);在 run 读结果的循环里(QUERIES_ALLOWED 为 false)不能插查询,查不到就只能是 DeclareWithOid(oid)。

后一种情况出现在简单查询协议里:raw_sql 不 prepare,第一次知道列类型是在读结果的半路上。新连接上(src/bin/text_custom.rs):

let row = sqlx::raw_sql("select 'happy'::mood as m, 'happy'::text as t").fetch_one(&mut conn).await?;
列 m 类型 "?"
列 t 类型 "TEXT"
m as Mood Ok(Happy)
t as Mood Err("error occurred while decoding column \"t\": mismatched types; Rust type `text_custom::Mood` (as SQL type `mood`) is not compatible with SQL type `TEXT`")

m 的类型名是 ?,也就是 DeclareWithOid。按理 Mood(with_name("mood"))和一个只知道 OID 的类型没法比较,可它解出来了。这是 PgType::eq_impl 故意放的水:两边都有 OID 就比 OID;一边是 DeclareWithOid、另一边只有名字,== 直接返回 true。PgTypeInfo 的文档说明了原因:这种时候证明不了两者不兼容,类型检查本来也只是一道健全性检查(it was deemed acceptable to fudge equality in this very specific case)。t 是内建的 TEXT,有 OID,照常比较,照常报错。

❗IMPORTANT
动脑筋:类型被删掉重建之后,老连接还认得它吗?

M2 Max、macOS、rustc 1.99.0、Postgres 16.14,src/bin/stale_oid.rs:

let recreate = "drop type if exists mood cascade; create type mood as enum ('happy', 'sad');";
sqlx::raw_sql(recreate).execute(&mut setup).await?;

let mut conn = PgConnection::connect_with(&opts).await?;
let s: String = sqlx::query_scalar("select $1::text").bind(Mood::Happy).fetch_one(&mut conn).await?;
println!("第一次 {s:?}");

// 另一条连接把类型删掉重建,名字一样,OID 变了
sqlx::raw_sql(recreate).execute(&mut setup).await?;
let r: Result<String, _> = sqlx::query_scalar("select $1::text || '!'").bind(Mood::Happy).fetch_one(&mut conn).await;
println!("重建类型之后 {:?}", r.map_err(|e| e.to_string()));

conn.clear_cached_statements().await?;
let r: Result<String, _> = sqlx::query_scalar("select $1::text || '!'").bind(Mood::Happy).fetch_one(&mut conn).await;
println!("clear_cached_statements 之后 {:?}", r.map_err(|e| e.to_string()));

第二次用的是一条新 SQL,prepare 会重新做。它会成功,还是报错?报错的话,clear_cached_statements 之后呢?

动脑筋答案

第一次 "happy"
重建类型之后 Err("error returned from database: cache lookup failed for type 16700 at line 2546")
clear_cached_statements 之后 Ok("happy!")
  • 第二次失败。prepare 确实重新做了,但 resolve_types 先查 cache_type_oid,mood 还对着旧的 OID,快路径直接返回,Parse 带着一个已经不存在的 OID 发出去,服务端报 cache lookup failed for type 16700(at line 2546 是 0.9 加的服务端源码行号,见错误)。类型缓存没有失效机制,跟连接同生同死。
  • clear_cached_statements 第一行就是 self.inner.cache_type_oid.clear(),再关掉所有缓存的语句。名字重新解析到新 OID,第三次成功。连接池里每条连接各有一份这样的缓存,都要各自清,或者等连接被换掉。

小结

  • PgTypeInfo 要么是内建类型(OID 写死在 try_from_oid 里),要么只有名字(DeclareWithName / DeclareArrayOf),要么是连接解析出来的 Custom。自定义类型的 OID 不稳定,所以 derive 只声明名字。
  • 名字在 prepare 发 Parse 之前由 resolve_types 解析:先查连接上的缓存,查不到的拼成一条 VALUES,用 raw_sql 一次查回。第一次绑定一个 enum 多一个来回,之后同一条连接上不再查(0.8.6 是三条预备语句)。
  • 参数字节里需要的 OID(数组元素、复合类型字段)编码时留 4 字节的洞,apply_patches 在 Bind 之前补上;Json<T> 的补丁在参数实际是 json 时把版本字节改成空格。
  • 二进制格式:整数大端,文本是裸 UTF-8,TIMESTAMPTZ 和 TIMESTAMP 都是 2000-01-01 起的微秒数(偏移量不上线),UUID 是 16 字节,jsonb 前面有版本字节 1。
  • 结果列的 OID 反查 cache_type_info;简单查询里查不到时类型是 DeclareWithOid,和按名字声明的类型比较时一律算兼容。类型删掉重建后,老连接的名字缓存会指向失效的 OID,clear_cached_statements 能清掉它。

💬 关联讨论

发起讨论
加载中...