DEBUG.WIKI
OnlineSIM:在线接收短信,虚拟号码租用
RackNerd;$21.99/年 · 1GB KVM
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证

pgx 读 xid8 变成 -1,sql.NullString 也没保住原值

pgx 5.11.0 的 xid8 通用 Scanner 转换会把大于 MaxInt64 的无符号值绕成负数。直接按 uint64 或 pgtype.Uint64 读取,保留完整十进制身份。

PostgreSQL 的 xid8 是无符号 64 位值,但通过 sql.NullString 读取,最大值 18446744073709551615 却可能变成字符串 "-1";换成 sql.NullInt64,也可能接收 -1 而不报范围错误。先核对 pgx 的读取路径,别把这个负数当成数据库原本保存的事务身份。

pgx 5.11.0 的 Uint64Codec.DecodeDatabaseSQLValue 在交给通用 sql.Scanner 前会执行 int64(n)。超过 math.MaxInt64 的值在这里已经回绕,随后 scanner 即使目标是字符串,也只能看到转换后的负数。这条边界同时存在于文本与二进制格式,不是修改连接字符集能解决的问题。

直接接收无符号结果

使用 pgx 原生查询接口时,优先让已知的 xid8 列扫描到 uint64;列允许 SQL NULL 时,可以采用 pgtype.Uint64,由其 Valid 状态区分。以下为已有 conn 和 ctx 上的局部片段,读取的示例值不写入数据库:

import "github.com/jackc/pgx/v5/pgtype"

var xid pgtype.Uint64
err := conn.QueryRow(ctx,
    "select '18446744073709551615'::xid8",
).Scan(&xid)
if err != nil {
    return err
}
if !xid.Valid {
    // 按业务处理 SQL NULL。
}

此处假定片段位于返回 error 的应用函数内,conn 为原生 pgx 连接或兼容的池查询入口。pgtype.Uint64 实现专用的 Uint64Scanner,在原生扫描计划中不必先经过有问题的有符号 driver.Value 转换。

不要只把数据库/sql 层的目标换成 pgtype.Uint64,就认定也绕开了它。经由 database/sql 适配器时,驱动可能在调用 Scanner 之前就已经把值转换成 int64;要么使用原生读取入口,要么在 SQL 里将已知列转为 text 后,按十进制字符串接收。

字符串要在有损转换之前取得

如果接口约定输出字符串,可在得到完整 uint64 后使用 strconv.FormatUint(xid.Uint64, 10),并先检查 Valid。已经变成 -1 后,再转字符串不会找回原值。

不要用浮点数中转,也不要把数据库列改成有符号 bigint 来迁就接收类型。下游 JSON、缓存键与比较逻辑应明确采用无符号整数或十进制字符串,避免修好数据库读取后又在下一层发生精度损失。

截至 2026 年 10 月 23 日,已核正式 5.11.0 仍有这条通用 Scanner 转换。后续采用修正时应核对正式版本及实际读取入口,不能把主分支变化等同于已部署包的行为。

在隔离检查中覆盖 0、MaxInt64、MaxInt64+1、最大 uint64 和 SQL NULL,并分别走应用实际使用的文本、二进制或 database/sql 路径。核对完整十进制值与空值状态,而不只是 err == nil。若负值已进入业务记录,先从可信原始身份核实,不能把所有负数统一加一个常量当作数据修复。