设想一个场景:某人注册账号、发了一条帖子,随后注销账号。数据库没有删掉他的帖子,只留下一个指向空用户的引用。几天后,另一个人注册了新账号,数据库把同一个内部ID分配给了他——于是新用户的主页下面,挂着一条陌生人写的帖子,而且系统完全不会报错

这不是黑客攻击,是SQLite的默认行为。有博主在7月15日发文指出:SQLite默认不启用外键约束(foreign_keys默认为OFF),而它给每行数据分配的内部ID(ROWID)存在复用机制。两个"正常设置"叠加,就能制造出一次真实的数据错位——旧数据被新用户"认亲"。

一个删除,为什么会传给下一个人

外键约束是关系型数据库防止"悬空引用"的基本工具:删用户前必须先处理其名下的帖子,否则报错拦下来。但SQLite出于兼容考虑,默认把这道防线拆掉了,需要开发者手动执行PRAGMA foreign_keys = ON才能启用。更麻烦的是,这条指令不能在事务或保存点内修改,而且部分编译版本的默认值本身就可能不同,应用不该依赖任何默认行为。

ROWID复用是怎么"认错人"的 Bob注册 ROWID = 1 Bob发帖 user_id = 1 Bob注销 帖子没删 Alice注册 复用ROWID=1 Alice"继承" Bob的帖子 默认 foreign_keys = OFF,这一步不会报错

类型检查同样宽松。声明为INTEGER的列,插入一段普通文字也能存进去——SQLite管这叫"类型亲和性",而不是严格类型。作者举了个真实感很强的例子:一列本该存歌曲时长(秒数),被写入一句吐槽文字,数据库照单全收,毫无提示。对一个数据库来说,这不是灵活,是失职。

SQLite其实在悄悄打补丁

把这些问题全归为"SQLite从不改"并不准确。它已经在动手,只是动作很小心。STRICT表自3.37.0版本起就存在,给单张表加上strict标记就能强制类型检查;命令行工具从3.41.0版本(2023年2月21日发布)起,默认关闭了历史遗留的双引号字符串解析(把双引号内容当字符串字面量,这是被标记为废弃的行为)。想要老行为的人还能用legacy_alter_table之类的开关找回来。

  • 结论.这说明SQLite的策略是"组件级微调",而不是"全局默认值一刀切"——CLI工具、单张表可以先变,核心库连接级的默认值几乎纹丝不动。

editions:把安全开关打包成一个版本号

作者的方案很具体:新增PRAGMA edition = 2026,一次性把foreign_keys = ONbusy_timeout = 5000journal_mode = WALsynchronous = NORMAL这四项设置打包生效,并让新建表默认走STRICT类型检查。灵感直接照搬Rust的edition机制——旧代码声明旧edition,行为不变;新代码声明新edition,享受更安全的默认值,互不干扰。

一个PRAGMA,打包五个安全开关 PRAGMA edition = 2026 foreign_keys = ON busy_timeout = 5000 journal_mode = WAL synchronous = NORMAL 新建表默认 STRICT

Rust的edition绑在crate、编译期生效,老代码不用管。SQLite要复刻这套逻辑,得先回答一个没人给出答案的问题:edition该绑在连接上、数据库文件上,还是表上?连接级最省事,但每个打开数据库的进程都要自己声明;文件级更彻底,却意味着老数据库文件永远换不了新默认值。这个问题目前没有答案,提案本身也没解。


这个提案眼下只在Reddit的r/programming等社区里发酵,没有见到SQLite核心团队的公开回应。这不奇怪。孔子说"名不正则言不顺",制度性的改动需要名分,而SQLite最大的名分就是"从不背叛老代码"。它被塞进无数手机、浏览器、路由器和嵌入式设备里,任何默认值的松动,哪怕用edition隔离得再干净,也得让上层的ORM、驱动、连接池全部学会识别这个新版本号——这本身就是一条新的兼容性负担,而不是免费的安全升级。

默认值,才是一个工具真正的态度。

从STRICT表到CLI默认关闭双引号字符串,SQLite走的一直是"先在小角落改,老用户不受影响"的路子,这次edition提案要的却是一整套连接级的行为切换,难度不是一个量级。参照STRICT表从提出到落地经历的漫长过程,edition这种更激进的方案想被官方采纳,大概率还要等更久,甚至可能不了了之。

  • 风险.数以亿计的存量SQLite文件意味着任何默认值变更都是高风险操作,即便隔离在新edition里,生态适配成本依然真实存在。

不用等官方表态,现在就能自己"攒"一个简化版edition:打开外键约束、设置合理的忙碌超时、开启WAL日志、把同步模式调到NORMAL,新建表能加STRICT就加。SQLite给了工具,只是没把它们调成默认——这笔账,目前还得开发者自己认。