DEBUG.WIKI
雨云;给项目一台云服务器
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

SqliteDataReader.GetStream 读到别行 BLOB,检查主键是否真是 rowid

Microsoft.Data.Sqlite 10.0.12 对部分 INTEGER 主键误判 rowid,流式读取可能读错行或报 cannot open table without rowid。先用 GetValue 对照。

同一条查询中,GetValue() 读到一份 BLOB,GetStream() 却读到另一行内容;或者对 WITHOUT ROWID 表报 cannot open table without rowid。在 Microsoft.Data.Sqlite 10.0.12 中,应检查被选出的 INTEGER 主键是否真的等于 rowid。

两个明确边界是列级 INTEGER PRIMARY KEY DESC 和 WITHOUT ROWID 表。前者的主键不是隐藏 rowid 的别名,后者根本没有 rowid。受影响的流式读取会把单列 INTEGER 主键当成行标识,再用它打开 BLOB:值碰巧对应另一行 rowid 时,可能无异常地读错内容。

先对照行身份与两种读取

保留表的实际 CREATE TABLE 定义、驱动和 SQLite 版本,以及失败查询的投影列。不要只看 ORM 模型上的“整数主键”。

在同一有效行上对照普通列值与流式内容,分别记录业务主键、长度和允许保存的内容摘要。测试数据应专门让主键值与 rowid 不同,否则错误路径可能碰巧读对,无法暴露问题。

GetBytes、GetChars 和 GetTextReader 也可能经过流式读取路径,不能只换一个相似方法就认定已绕开。

在读取边界使用普通值物化

对体积可接受的 BLOB,可以暂时用普通列值读取,再明确创建只读内存流。以下假设 reader 已定位到有效行,blobOrdinal 是目标 BLOB 列:

if (reader.IsDBNull(blobOrdinal))
{
    // 按调用方原有约定处理 SQL NULL。
}
else
{
    byte[] bytes = (byte[])reader.GetValue(blobOrdinal);
    using var stream = new MemoryStream(bytes, writable: false);
    // 在此作用域内消费 stream。
}

这里保留了 SQL NULL 与空 BLOB 的区别,片段中的 NULL 分支需接入原有返回契约。GetValue() 会把整份 BLOB 放进内存,大文件或高并发读取前要评估内存成本。不要在返回流之前就通过 using 把它释放,实际方法的所有权边界应由调用方明确。

需要真正流式读取时,先按表结构确认可用的真实 rowid 与查询方式;WITHOUT ROWID 表不支持 SQLite 的增量 BLOB I/O,不能靠给投影起名 rowid 造出它。不要为消除读取错误直接迁移主键或重建生产表。

验收覆盖两条不同主键/内容的行、SQL NULL、空 BLOB,以及相应的特殊表定义。比较普通读取与业务最终交付字节,确保行身份、长度和内容一致。后续升级到含修正的正式包后,仍需保留主键与 rowid 不同的回归用例;当前开发分支的补丁计划不代表已部署版本修好。