sqlite-utils 4.1.1 补上了一个从不报错的漏洞
Simon Willison 刚发布的 sqlite-utils 4.1.1,changelog 里最重要的一条只有一句话:table.transform() 在事务开启、PRAGMA foreign_keys 打开、且表被 CASCADE、SET NULL、SET DEFAULT 这类破坏性外键动作引用时,现在会抛出 TransactionError。
听起来像一次普通的边界情况修复。但往前倒几个版本会发现,这条外键处理逻辑从 3.35 一路改到 4.0 再改到 4.1.1,是同一个技术痛点被反复打磨的结果——而且过去所有版本里,出问题时都不会报错,数据就那么悄悄没了。
静默删除是怎么发生的
sqlite-utils 的 table.transform() 用来改表结构——加字段、改类型、加约束。SQLite 本身不支持大多数 ALTER TABLE 操作,所以 transform 走的是经典迂回路线:建一张新表,把数据拷过去,删掉旧表,再把新表改名顶替。
问题出在“删掉旧表”这一步。如果这张表被其他表通过外键引用,并且引用关系设置了 ON DELETE CASCADE 之类的动作,SQLite 会把“删旧表”理解成“删父行”,进而自动触发级联,改掉甚至删掉那些引用它的数据。
SQLite 另有一条冷门规则:PRAGMA foreign_keys 只能在没有活跃事务时切换,事务内部改这个开关不会生效。sqlite-utils 的每次迁移都跑在一个事务里,这意味着开发者没法在 transform 中途临时关掉外键检查来避开这个坑——想关也关不掉。
三个条件叠在一起:事务中、外键检查开着、表被破坏性外键引用,drop 旧表这一下就会连带删掉别的数据,而且全程没有任何报错。4.1.1 之前,这个坑一直开着。
三个版本才补完的一条线索
这不是一次孤立修复,而是同一个问题被拆成三段逐步收紧。
- 3.35.新增外键的方式改用 table transformation 机制,
transform()拿到foreign_keys=、add_foreign_keys=参数,CLI 也加了--add-foreign-key。 - 4.0.
transform()开始保留ON DELETE CASCADE等外键动作,不再像更早版本那样直接静默丢弃约束本身。 - 4.1.1.补上事务安全检查,遇到危险组合直接报错,而不是任由级联静默执行。
三步看起来方向一致:外键约束从“被忽略”到“被保留”,再到“被保留后如果会出事就提前拦下”。中间这一步——保留了约束却没检查执行时机——恰恰是这次修复要堵上的窗口期。
谁该回头查一下自己的迁移记录
受影响的是特定组合:用 sqlite-utils 做 schema 迁移,表结构里有 CASCADE/SET NULL/SET DEFAULT 外键约束,并且在事务中调用了 table.transform()。命中这个组合的开发者,理论上过去的每一次迁移都可能悄悄改动过引用表的数据,而不会留下任何错误日志——因为压根没有异常抛出,只有一次“成功”的迁移记录。
sqlite-utils 广泛用于数据抓取、ETL 流程,也是 Datasette 生态的底层工具,用户群体不算小众。4.1.1 把这类风险从“完全无感”变成“显式失败”,是一种迟到但方向正确的保守修复:宁可让迁移中断报错,也不再允许它悄悄跑完却留下一堆被篡改的数据。
- 风险.升级之后如果历史迁移曾经命中这个组合,报错本身不会帮你找回已经丢失或被级联改动的数据,只能靠自己回溯排查。
同一次发布还顺手把 CLI 文档和 Python API 文档做了互相链接。相比外键那条修复,这算是维护者在处理完一个安全隐患后,回头补的一点可用性债——量不大,但说明这类项目的维护精力,确实是先紧着风险修,再腾出手做体验。
