
数据库后端【免费下载链接】sqlx The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.项目地址https://gitcode.com/gh_mirrors/sql/sqlx点击查看免费下载本文深入剖析 SQLx 在 PostgreSQL 驱动中对bigdecimal::BigDecimal与NUMERIC类型互转时的精度边界问题为何NUMERIC存在物理位数上限、为何 SQLx 的编码 API 必须“永不失败”、越界值会被编码成哨兵值并在服务端触发22P03错误以及解码方向为何能安全覆盖除NaN外的所有NUMERIC值。读完本文你将理解 SQLx 对NUMERIC类型映射的底层机制并掌握如何规避与排查由此引发的数据库错误。背景SQLx 如何把 BigDecimal 映射为 NUMERICSQLx 是 Rust 生态中主打编译期查询检查的异步 SQL 工具包其 PostgreSQL 驱动通过 Cargo feature 控制第三方十进制类型的支持。在 sqlx-postgres/Cargo.toml 中可以看到bigdecimal [dep:bigdecimal, dep:num-bigint, sqlx-core/bigdecimal]即启用bigdecimalfeature 后bigdecimal与num-bigint两个 crate 才会被编译进驱动。类型映射表定义在 sqlx-postgres/src/types/mod.rs其中明确写着Rust 类型PostgreSQL 类型bigdecimal::BigDecimalNUMERIC对应实现位于 sqlx-postgres/src/types/bigdecimal.rs类型映射impl TypePostgres for BigDecimal将type_info()返回PgTypeInfo::NUMERIC数组类型实现PgHasArrayType将BigDecimal数组映射为NUMERIC[]即NUMERIC_ARRAY双向转换TryFromPgNumeric负责“库值 → BigDecimal”TryFromBigDecimal负责“BigDecimal → 库值”。而本文要讨论的核心问题——两个类型范围不对称——正是由该文档 bigdecimal-range.md 正式记录的。该文档通过#[docinclude_str!(...)]被嵌入到Encode实现的 doc 注释中见 bigdecimal.rs并在 types/mod.rs 的模块文档中一并呈现。NUMERIC 的物理上限131,072 位整数与 16,384 位小数PostgreSQL 的NUMERIC即decimal并非任意精度它在存储与运算上都有明确的位数上限小数点前整数部分最多131,072 位十进制数字小数点后小数部分最多16,384 位十进制数字。这一限制由 PostgreSQL 官方手册第 8.1 节Numeric Types规定NUMERIC的底层二进制表示决定了它的容量边界。在 SQLx 中这个边界会进一步转化为线协议wire protocol层面的硬约束。NUMERIC在 Postgres 二进制协议中的表示由 sqlx-postgres/src/types/numeric.rs 中的PgNumeric枚举承担pub(crate) enum PgNumeric { NotANumber, Number { sign: PgNumericSign, // 正负号0 与 -0 均记为正 digits: Veci16, // 以 10000 为基、大端序排列的“位组” weight: i16, // 缩放因子digits[0] * 10000 ^ weight ... scale: i16, // 小数点后的十进制位数 }, }其中每个digits元素是一个i16取值必须在[0, 10000)区间内见is_valid_digitnumeric.rs。编码时numeric.rs驱动会先写入digits_leni16、weight、sign、scale四个头部字段再依次写入每个 digit 的to_be_bytes()。由于digits_len与weight、scale都以i1616 位有符号承载digit 数组长度不能超过i16::MAX否则PgNumeric::encode会直接报错PgNumeric digits.len() should not overflow i16每个 digit 是 4 位十进制数字base-10000因此 131,072 位整数 16,384 位小数正好对应这一线协议容量设计。BigDecimal 的理论范围任意精度与 2^63 有效数字与NUMERIC相反bigdecimal::BigDecimal是真正的“无限扩展”类型理论上可以表示任意数量的十进制小数位唯一的硬性上限是最多 2^63 个有效数字significant figures——这是由num-bigint的BigInt底层存储约束决定的2^63 在实践中的天文数字规模几乎不可能触达。换言之一个在 Rust 内存中合法存在的BigDecimal值完全可能超出 PostgreSQLNUMERIC的线协议表示能力。编码方向必须“永不失败”的 API 设计与哨兵值机制为什么 SQLx 不直接在选择编码时返回Err原因在于 API 设计契约。SQLx 的Encodetrait 签名sqlx-core/src/encode.rs在设计上必须是不可失败的infallible——encode_by_ref只能把数据写进参数缓冲区不能中途报告“此值无法表示”。因此当遇到一个超出NUMERIC线协议容量的BigDecimal时SQLx 的编码实现 bigdecimal.rs 会调用PgNumeric::try_from(self)尽力转换若转换失败值过大退而编码一个“哨兵值”sentinel value——一个超出NUMERIC合法范围、但依然能在线协议上被表示的值将该哨兵值交给 PostgreSQL 服务端处理。关键细节这个哨兵值不是标准NUMERIC值但它的二进制形态合法因此能顺利到达服务端。PostgreSQL 在解析时发现该值的 scale或相应字段超出允许范围于是抛出数据库错误。这就是文档中描述的典型故障现象错误码22P03invalid_binary_representation错误消息invalid scale in external numeric value文档同时注明这条消息的具体文本未来可能会变化。也就是说越界编码不会在 Rust 侧静默失败或 panic而是变成一次必然报错的查询。从用户视角看现象表现为往数据库写入一个“理论上合法”的BigDecimal却收到22P03错误且错误发生在服务端而非客户端。触发条件与规避思路触发条件BigDecimal的整数部分超过 131,072 位或小数部分超过 16,384 位。规避思路在写入前自行校验位数上限或改用NUMERIC(p, s)更窄的列约束并在应用层拒绝越界输入或考虑拆分为字符串/分片存储。SQLx 本身不会替你拦截这类值因为编码契约决定了它只能“编码一个坏值”来暴露问题。解码方向可覆盖除 NaN 外的全部 NUMERIC 值与编码方向的不对称性相反解码方向基本是安全的BigDecimal能表示任何 PostgreSQLNUMERIC值——因为任意一个NUMERIC值的位数都受限于上述 131,072 / 16,384 上限而这个规模对BigDecimal来说完全在其表示能力之内。唯一的例外是NaNBigDecimal没有 NaN 的概念因此无法表示它。在 bigdecimal.rs 的Decode实现中二进制格式先PgNumeric::decode还原为PgNumeric再经try_into()转为BigDecimal文本格式直接把字符串parse::BigDecimal()。而PgNumeric::NotANumber分支bigdecimal.rs会直接返回错误BigDecimal does not support NaN values。NaN在 PostgreSQL 中通常由1 / 0这类运算产生若你的业务数据可能包含它解码前需有兜底方案。另外值得注意解码时驱动会对 digit 合法性做校验bigdecimal.rs一旦发现某个 digit 超出[0, 10000)会返回PgNumeric to BigDecimal: Nth digit is out of range错误——这是对损坏二进制数据的防御而非正常 NUMERIC 值会触发的路径。源码级验证单位测试中的映射证据bijdecimal.rs 内置了一组单元测试直观展示了BigDecimal → PgNumeric的映射规则可直接作为理解本文内容的验证材料0→digits: []、weight: 0、scale: 0Postgres 对 0 返回空 digit 数组见 bigdecimal.rs 的注释与处理10000→weight: 1、digits: [1]印证了“weight 表示 10000 的幂次”12345→weight: 1、digits: [1, 2345]印证 base-10000 分组0.1→scale: 1、weight: -1、digits: [1000]印证负权重与小数位编码12345.67890→scale: 5、weight: 1、digits: [1, 2345, 6789]回归测试issue_423_*系列覆盖了恰好 4 位与 8 位整数的边界情形bigdecimal.rs防止 weight 计算出现 off-by-one。对照实现中的scale (digits.len() - weight - 1) * 4bigdecimal.rs以及编码侧对 base-10 长度与 exp 的换算bigdecimal.rs可以看到驱动在“十进制 ↔ base-10000”之间的精确换算逻辑——这也解释了为什么哨兵值的 scale 会溢出NUMERIC的 16,384 位上限从而被服务端判定为invalid scale。对照参考rust_decimal 是相反的不对称理解BigDecimal的边界时不妨与另一个同样映射到NUMERIC的类型rust_decimal::Decimal对照文档见 rust_decimal-range.mdrust_decimal::Decimal的最大绝对值为 2^96 − 1约 67 位十进制数字最小绝对值为 10^-2828 位小数因此NUMERIC可以表示rust_decimal::Decimal的每一个值范围完全包含但反过来不行结论对rust_decimal::Decimal而言编码永不失败解码可能失败——与BigDecimal恰好相反。这两份文档bigdecimal-range.md与rust_decimal-range.md分别挂在各自的Encode/Decode实现上是 SQLx 源码中针对“类型范围不对称”这一主题的官方说明也是排查十进制类型相关异常时的第一手参考资料。实践总结方向BigDecimal ↔ NUMERIC结论编码值超出 131,072整数位/ 16,384小数位时SQLx 编码 API 不可失败将写入哨兵值服务端报22P03/invalid scale in external numeric value解码NUMERIC→BigDecimal除NaN外全部可解码NaN报BigDecimal does not support NaN values触发前提需启用bigdecimalCargo feature映射见 sqlx-postgres/Cargo.toml在实际项目中若高频写入高精度数值建议在业务层显式检查BigDecimal的整数/小数位数是否突破 PostgreSQL 上限并在必要时使用字符串或分片方案存储超长数值同时在Decode侧为潜在的NaN与越界 digit 预留错误处理分支。这样既能发挥BigDecimal任意精度的能力又能把“范围不对称”引发的22P03错误挡在应用层之外。赞分享数据库后端【免费下载链接】sqlx The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.项目地址https://gitcode.com/gh_mirrors/sql/sqlx点击查看免费下载相关推荐Milvus 错误哨兵约定Error Sentinel Conventiontyped merr 与内部哨兵的两层体系与 gRPC 边界硬性不变量Milvus 错误哨兵约定Error Sentinel Conventiontyped merr 与内部哨兵的两层体系与 gRPC 边界硬性不变量 本篇文数据库向量数据库分布式数据库后端Go 错误包装与哨兵错误分类Agent Substrate 依赖库 natefinch/wrap 的 With 函数深度解析Go 错误包装与哨兵错误分类Agent Substrate 依赖库 natefinch/wrap 的 With 函数深度解析 导读 Go 从 1.13 起引入人工智能AI AgentAgent 沙箱云原生容器运行时零信任Claude-Code-Game-Studios 的 Godot 弃用 API 迁移指南从 4.3 到 4.6 的 API 升级与重构实践Claude Code Game Studios 的 Godot 弃用 API 迁移指南从 4.3 到 4.6 的 API 升级与重构实践 导读 本文以 de可观测性AI 评测LLMOpsAI 应用人工智能上一篇解密Blender MMD Tools三明治架构下的材质双向转换技术下一篇深度解析Blender MMD Tools材质转换的技术演进与实战应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考