防复发:把「删数据集带走站点自建数据」做成结构性门禁 + 常驻体检

用户放弃继续救那批旧图,要求的是「别再发生同类事件」。所以不靠记性,靠结构:

1) 归属铁律(写进 schema.sql 抬头 + README 新章节)
   判断标准只有一句:**这份记录能不能从备份重新导入?**
   能(places/visits/movements/weather/activities/tags/entity_raw)⇒ 可挂
   `REFERENCES datasets(id) ON DELETE CASCADE`;
   不能(place_images/place_notes/place_votes/topics)⇒ 只留 dataset_id 当出处、**不许挂外键**。

2) 两道门禁(dataset_ownership_test.go),都做了变异验证:
   - 运行时:查 pg_constraint,凡挂 datasets 的表必须在白名单里;**反向也查**
     (白名单里的表不许被顺手摘掉,否则删数据集会留成片孤儿行)。
   - 静态:扫 schema.sql 的 CREATE TABLE。只看运行时有盲点——新建的库走 CREATE TABLE,
     开发库可能压根没跑过那次迁移。变异(把 topics 的 CASCADE 补回去)当场红。
   - 变异验证顺带发现:扫描必须先跳过 `--` 注释行,规矩的说明文字里就写着
     `REFERENCES datasets(id)`,不跳过会把自己写的文档当成违规。
   - 还发现运行时那道单独看**不够**:夹具每次跑迁移,迁移自己就把坏外键摘了,
     它永远不会在不健康的库上报警 ⇒ 补了「注入事故原样的外键再跑迁移,断言被摘掉」
     的自愈用例(并先确认变异真的生效,防假绿)。

3) 常驻体检:后台首页显示「库里 N 张 / 本地盘上 M 个 / 落单 K 个」,落单 >0 直接报警。
   事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**,等照片看不见了才追。

自愈用例又抓出一个此前漏掉的缺口:place_notes / place_votes 上的同类外键迁移并不会摘
(原先只写了 place_images 与 topics 两句 DROP)⇒ 补成五张自建数据表全覆盖。

⚠️ 排序坑(本项目第二次踩):摘外键那段最初放在 place_images 建表**之前**,
开发库毫无症状、**全新库起不来**(ALTER 报「关系不存在」),被
TestFreshDatabaseServesAppQueries 抓住。整段已挪到 place_images 之后,并把坑写进注释。

顺带订正「删除数据集」的二次确认文案(专题改完之后它还在说专题会一起删)。
全量验收 PASS=336 FAIL=0 SKIP=0。
This commit is contained in:
tamakyi
2026-10-01 01:15:04 +08:00
parent 6b665b6056
commit 4c3032cf48
10 changed files with 415 additions and 6 deletions
+25 -5
View File
@@ -1,5 +1,16 @@
-- rond-with-you 数据结构
-- 一次上传 = 一个 dataset,所有事实表挂在其下,支持多份备份共存与切换
--
-- ⚠️️ 新增一张带 dataset_id 的表时,先答一个问题:
-- **它的记录能不能从备份重新导入?**
-- 能(到访/地点/位移/天气/活动/标签/原始行留档)⇒ 可以挂
-- `REFERENCES datasets(id) ON DELETE CASCADE`:删数据集时它们跟着走是对的。
-- 不能(图片、备注、结论、专题等**站点自建**的数据)⇒ **只留 dataset_id 当出处,不许挂外键**:
-- 删数据集会把它们一起删掉,而这些记录不在任何备份里,删了就是永久丢失
-- (2026-09-30 图片事故:行被 CASCADE、文件却留在盘上,读取按坐标键、行按数据集级联,
-- 两套口径打架)。参考 place_images / place_notes / place_votes / topics 现在的写法。
-- 这条规矩由 `TestOnlyBackupDerivedTablesCascadeFromDatasets` 与
-- `TestSchemaDeclaresNoDatasetFKForUserData`(运行时 + 静态两道)守住,改这里会当场红。
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
@@ -398,9 +409,6 @@ CREATE TABLE IF NOT EXISTS topics (
);
CREATE INDEX IF NOT EXISTS idx_topics_user ON topics(user_id, sort_order, id);
-- 老库摘掉那根外键(新库上面的 CREATE 里就没有)。幂等:约束不在时什么都不做。
-- 摘掉之后删数据集不再带走专题——它们只存在这张表里,删了就没有别的恢复来源了。
ALTER TABLE topics DROP CONSTRAINT IF EXISTS topics_dataset_id_fkey;
-- fog of world 快照里「非已探索层」的原始瓦片文件(Model/~ 与 Model/#)。
-- 站点只用已探索层渲染,但导出必须原样写回这两层,否则重导出的快照会丢掉约 55% 的内容。
@@ -470,8 +478,20 @@ CREATE TABLE IF NOT EXISTS place_images (
);
CREATE INDEX IF NOT EXISTS idx_place_images_place ON place_images(dataset_id, src_pk, position);
-- 老库摘掉那根外键(新库上面的 CREATE 里就没有)。幂等:约束不在时什么都不做。
ALTER TABLE place_images DROP CONSTRAINT IF EXISTS place_images_dataset_id_fkey;
-- ---------- 站点自建数据:一律摘掉 datasets 的外键 ----------
-- ⚠️ 这一段必须排在**所有相关 CREATE TABLE 之后**(place_images 是最后一张):
-- 迁移块插在建表之前时,开发库(表已存在)毫无症状,**全新库却起不来**
-- (`ALTER TABLE place_images …` 报「关系不存在」)——`TestFreshDatabaseServesAppQueries`
-- 就是守这个的,本项目踩过。
-- 这一段是给**已经部署出去的库**自愈用的,幂等:约束不在时什么都不做。
-- 五张表全都兜住:只摘 place_images / topics 不够——place_notes / place_votes 是更早那次
-- 改坐标键时摘的,老库上未必摘干净(变异验证就是这么抓出来的:把外键补回去后发现迁移不管它们)。
-- 摘掉之后「删数据集」不再带走任何站点自建的数据。
ALTER TABLE place_images DROP CONSTRAINT IF EXISTS place_images_dataset_id_fkey;
ALTER TABLE place_notes DROP CONSTRAINT IF EXISTS place_notes_dataset_id_fkey;
ALTER TABLE place_votes DROP CONSTRAINT IF EXISTS place_votes_dataset_id_fkey;
ALTER TABLE place_visitor_votes DROP CONSTRAINT IF EXISTS place_visitor_votes_dataset_id_fkey;
ALTER TABLE topics DROP CONSTRAINT IF EXISTS topics_dataset_id_fkey;
-- ---------- 附属数据改挂坐标键(一次性迁移)----------
-- 备注 / 结论 / 图片原本挂 (dataset_id, src_pk),换个数据集就全看不见了。