Cloudflare D1 的 PRAGMA foreign_keys 陷阱:一次 DROP TABLE 差点清空整个子表
在给 Fitness Tracker 做 schema migration 的时候踩了一个大坑,差点把生产数据清掉。记录一下,因为这个行为和本地 SQLite 完全不一样。
背景
标准的 SQLite schema 变更套路是这样的:
PRAGMA foreign_keys = OFF;
DROP TABLE profiles;
CREATE TABLE profiles (...);
-- 从备份表把数据搬回来
PRAGMA foreign_keys = ON;
先把 foreign key 检查关掉,重建父表,再打开。本地 sqlite3 里跑完全没问题。
问题
D1 上跑同样的 migration,结果子表的数据全没了。sets 表通过 profile_id 外键指向 profiles,并且带 ON DELETE CASCADE —— DROP TABLE profiles 的瞬间,cascade 触发,把所有关联的训练记录一起删掉了。
原因:D1 会忽略跨 statement 的 PRAGMA foreign_keys = OFF。每个 statement 在 D1 里是独立执行的,你在第一行写的 PRAGMA 到了第二行的 DROP TABLE 已经不生效了。所以外键检查一直是开着的,cascade 也一直是活的。
一图看懂整个事故链:被忽略的 PRAGMA → 活着的 CASCADE → 静默消失的数据
解决办法
不要依赖 PRAGMA,改用「不触发 cascade」的迁移方式:
- 不 DROP 父表,用
ALTER TABLE ... RENAME或加列的方式做增量变更; - 真的要重建时,先备份子表数据(
CREATE TABLE sets_backup AS SELECT * FROM sets),重建完再搬回来; - migration 跑之前,先
wrangler d1 export做一份完整备份 —— 这个应该变成肌肉记忆。
教训
本地 SQLite 验证通过 ≠ D1 上安全。D1 的执行模型是 per-statement 的,任何依赖「session 状态」的操作(PRAGMA、临时设置)都不可信。
顺手把 wrangler d1 export 加进了部署前的 checklist。数据无价。