Npgsql 读取超大 interval 变成负数:先保留原始分量
PostgreSQL interval 的范围大于 TimeSpan。Npgsql 的 ticks 运算可能在转换前溢出,返回符号错误的值;显式读取 NpgsqlInterval,再按月份语义和目标范围检查。
数据库里的 interval '300000000 hours' 是正时长,Npgsql 读成 TimeSpan 后却变成负数,而且没有抛溢出异常。此时不要再把这个值用于过期时间、计费或保存回数据库,先比较服务器原值与客户端转换前的表示。
PostgreSQL interval 与 TimeSpan 的表达范围不同。Npgsql 10.0.3 的正式转换代码将微秒乘以 10,再加上天数对应的 ticks;这段乘加没有先检查 Int64 范围。结果溢出后仍可能构造出一个形式有效、含义已经错误的 TimeSpan。
在转换前取得月、日和微秒
对于确实可能超过 TimeSpan 范围的字段,可以使用显式读取投影,按 NpgsqlInterval 读取。下面假定 reader 已读取到当前行,第 0 列就是 interval:
NpgsqlInterval raw = reader.GetFieldValue<NpgsqlInterval>(0);
使用 NpgsqlTypes 命名空间,并保持原来的 SQL NULL 处理。不要先让 ORM 把字段物化成 TimeSpan,再在 getter 中判断是否超过上限;原值可能早已在物化阶段丢失。
raw.Months、raw.Days 和 raw.Time 分别保留月份、天数及微秒分量。月份不是固定天数,不能一律按 30 天换算。如果业务需要日历意义的月份,应选择能保留该语义的模型,而不是强转 TimeSpan。
只有范围可表示时才构造 TimeSpan
业务明确不允许月份、并且把一天定义为固定 24 小时时,可先用宽整数计算,再检查目标范围。以下是转换函数片段:
using System;
using System.Numerics;
using NpgsqlTypes;
static TimeSpan ToCheckedTimeSpan(NpgsqlInterval value)
{
if (value.Months != 0)
throw new InvalidOperationException("Interval contains months.");
BigInteger ticks = (BigInteger)value.Time * 10
+ (BigInteger)value.Days * TimeSpan.TicksPerDay;
if (ticks < long.MinValue || ticks > long.MaxValue)
throw new OverflowException("Interval exceeds TimeSpan range.");
return new TimeSpan((long)ticks);
}
必须在乘加前转换成 BigInteger;先用 long 算完再转换,不能挽回溢出。若业务允许范围更小,还应先按业务约束拒绝异常值。拒绝时保留原始记录和分量供核对,不截断、不取绝对值,也不把负数统一改成零。
验收覆盖普通正负时长、接近目标边界、超大正负值、非零月份和 SQL NULL。分别比较数据库原值、原始分量以及转换结果;检查拒绝路径不会继续提交错误业务数据。
这个处理不要求修改数据库已有 interval。若超大值本来就是错误输入,后续清理应另外核对业务来源并备份。升级驱动后,检查转换是否在微秒乘加前校验范围,再用超大正负值复查拒绝路径;验证通过前保留这层显式检查。


