给老楼换一截水管,最怕的不是扳手滑了,是你一拧,隔壁三户先停氧。我对 sqlite-utils(操作 SQLite 的工具库)4.1.1 的判断只有一句:它不是在加功能,它是在防你改表时误删关联数据。

这事最反常识的地方在于:你明明没想删数据,删数据的机关却埋在“改表”里面。SQLite(嵌入式轻量数据库)改表,常常不是裁缝改袖口,而是拆旧屋、搬家具、立新墙。table.transform()(sqlite-utils 用来改表结构的方法)背后会丢掉旧表;旧表一丢,那些指向它的外键若写着 ON DELETE CASCADE(父表删,子表也删)、SET NULL(把引用清空)或 SET DEFAULT(改回默认值),就会被当成真的删表来执行。

最坏的事故,不是数据库尖叫,而是它点头。

程序员常把事务当护身符,仿佛一进事务,世界就自动文明了。偏偏这一次,事务像反锁的手术室门。PRAGMA foreign_keys(SQLite 里控制外键约束的开关)没法在事务中切换,sqlite-utils 也就没法像平时那样先临时关掉外键、改完表、再跑 foreign_key_check(外键一致性检查)。所以 4.1.1 干脆改了脾气:不开绿灯,不赌运气,直接抛出 TransactionError(事务错误)把你拦下。

改表不是装修,是拆楼。

凌晨一点二十,小周在电商后台里把 users 表的主键从整数改成字符串,因为新接的编号系统不再吐连续数字。他很规矩,把操作包进事务里,觉得这样“稳”。orderscart_itemscoupon_logs 都通过外键指着 users,其中两张表还挂着级联删除。结果第二天,用户还在,订单主表还在,订单明细和优惠券流水却像被猫舔过,只剩一层空壳。小周没有写一行 DELETE(删除语句),但数据库替他完成了灭门。

真正可怕的不是报错,是没报错。

下午四点,教务系统里,老陈给 students 表调整字段顺序,顺手开了严格模式,好让脏数据少混进来。attendance 表的外键动作设成 SET NULL,意思是学生档案没了,考勤记录还留着,只是先失去指向。一次改表之后,页面依旧平静,日志也没有红字,只是三千多条考勤记录突然都“失忆”了。人还在教室里坐着,系统里却像集体退学。

外键不是注脚,它是关系的水管。

周五傍晚,社区诊所的预约系统要重排 doctors 表字段。appointments.doctor_id 的外键动作是 SET DEFAULT,默认值 0 代表“待分配医生”。改表跑完,没人报警,任务也显示成功。周一早上护士打开后台,几百条复诊预约整整齐齐回到了“待分配”,像病历柜被人悄悄抽走了一层隔板。数据没有消失,意义先死了。

所以我才说,4.1.1 的价值不在“更会改表”,而在“终于学会拒绝”。它不是一刀切地不让你动表;它只在三件事同时出现时踩刹车:事务已经开着,外键开关已经开着,而且别的表正用会连坐的删除规则指着你要改的表。没有这三样,它照常干活;有了这三样,它宁可惹你烦,也不替你办丧事。

工具世界里有一种虚伪的勤快:你给命令,它就装作忠诚,把危险也一并执行到底。看上去听话,其实是没见识。因为数据库从来不是一张张孤立的表,它更像一座小镇:订单靠用户,考勤靠学生,预约靠医生。你动一堵墙,晃的是整条街。4.1.1 看见的,恰恰不是“表”,而是“关系”。

事务通常是安全带;在这件事里,它差点成了蒙眼布。

我一直觉得,好工具的体面,不是永远服从,而是知道什么时候该抗命。sqlite-utils 4.1.1 的终点不是炫技,而是节制:它承认有些“改表”表面是维护,骨子里却可能是一场误杀。把这种误杀拦在门外,比再多加一个新选项,都更像工程师该有的良心。

这判断不是凭空发狠。4.1.1 的发布说明和官方文档都把机制写得很明白:table.transform() 在事务里遇到来自别的表、且带破坏性删除动作的外键时,会拒绝执行;原因正是旧表删除可能悄悄触发关联行删除或改写。原文可见:4.1.1 发布说明“外键与事务”官方文档


别人聊 AI,我们测 AI——每个结论都能下载原始数据自己复算。 🔗 官网 👉 https://crawdpad.com