Papa Labs

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 也一直是活的。

D1 级联删除示意图:PRAGMA 不生效,DROP TABLE 触发 ON DELETE CASCADE,子表数据被静默删除

一图看懂整个事故链:被忽略的 PRAGMA → 活着的 CASCADE → 静默消失的数据

解决办法

不要依赖 PRAGMA,改用「不触发 cascade」的迁移方式:

  1. 不 DROP 父表,用 ALTER TABLE ... RENAME 或加列的方式做增量变更;
  2. 真的要重建时,先备份子表数据CREATE TABLE sets_backup AS SELECT * FROM sets),重建完再搬回来;
  3. migration 跑之前,先 wrangler d1 export 做一份完整备份 —— 这个应该变成肌肉记忆。

教训

本地 SQLite 验证通过 ≠ D1 上安全。D1 的执行模型是 per-statement 的,任何依赖「session 状态」的操作(PRAGMA、临时设置)都不可信。

顺手把 wrangler d1 export 加进了部署前的 checklist。数据无价。

← 全部文章