防复发:把「删数据集带走站点自建数据」做成结构性门禁 + 常驻体检
用户放弃继续救那批旧图,要求的是「别再发生同类事件」。所以不靠记性,靠结构:
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:
@@ -0,0 +1,36 @@
|
||||
# 2026-10-01
|
||||
|
||||
## 防复发:把「删数据集带走自建数据」做成结构性门禁(00:30)
|
||||
|
||||
用户放弃继续救那批旧图:「我只想避免类似的事件再次发生」。于是把这次的教训固化成三件东西
|
||||
(`internal/web/dataset_ownership_test.go` + `schema.sql` 抬头 + README 新章节):
|
||||
|
||||
1. **归属铁律**:判断标准只有一句——**这份记录能不能从备份重新导入?**
|
||||
能(places/visits/movements/weather/activities/tags/entity_raw)⇒ 可挂 CASCADE;
|
||||
不能(place_images/place_notes/place_votes/topics)⇒ 只留 dataset_id 当出处、不许挂外键。
|
||||
2. **两道门禁**:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ 静态扫 `schema.sql`
|
||||
(新建的库走 CREATE TABLE,只看运行时有盲点)。做变异验证时确认了静态那条**真的会响**。
|
||||
3. **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`),落单 >0 直接报警。
|
||||
事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**——让它当场可见。
|
||||
|
||||
变异验证的收获(比测试本身更有价值):
|
||||
- 运行时门禁单独看**不够**:夹具每次跑迁移,迁移自己就把坏外键摘了 ⇒ 它永远不会在不健康的库上报警。
|
||||
补了 `TestMigrationDropsStaleDatasetForeignKeys`:注入事故原样的外键再跑迁移,断言被摘掉。
|
||||
- 这条自愈用例又抓出缺口:`place_notes`/`place_votes` 上同类外键**迁移并不会摘**
|
||||
(原先只写了 place_images 与 topics 两句 DROP)⇒ 补成四张自建数据表全覆盖。
|
||||
**部署出去的库要能自愈,DROP 必须覆盖全部同类表。**
|
||||
- 静态扫描必须先跳过 `--` 注释行:规矩的说明文字里就写着 `REFERENCES datasets(id)`,
|
||||
不跳过会把自己写的文档当成违规。
|
||||
|
||||
顺带订正:后台「删除数据集」的二次确认文案在专题改完后已说过时话(说专题会一起删),改成实话。
|
||||
|
||||
### 补记:迁移块排序的老规矩我又踩了一次(00:55)
|
||||
|
||||
把「摘外键」那五句 ALTER 收成一段放在 `topics` 之后,而 `place_images` 的 CREATE 在这段**之后**
|
||||
⇒ 开发库(表已存在)毫无症状,**全新库起不来**:`ALTER TABLE place_images …` 报「关系不存在」。
|
||||
正是记忆里那条老规矩:「迁移块必须收敛且排在依赖表之后(插在 place_images 前:开发库无症、新库起不来)」。
|
||||
被 `TestFreshDatabaseServesAppQueries` 当场抓住(这条门存在的意义)。
|
||||
|
||||
改成整段排在 `place_images` 建表之后(它是五张里最后一张),并把这个坑写进 schema.sql 的注释里。
|
||||
教训:**凡是 ALTER 某个表的迁移块,先确认那张表在文件里已经建过了**——
|
||||
只在本机库上跑一遍是测不出来的,必须跑 fresh-database 用例。
|
||||
@@ -681,3 +681,47 @@ Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 采样点、`ZTRIP*
|
||||
|
||||
教训(可复用):**「能读出数据」不等于「读对了那一版」**。凡是从「可能被覆盖过的留档」里还原编号/坐标,
|
||||
都要把历史版本一起纳入,并且在失败信息里列出已查过的来源 —— 否则用户只能看到「认不出」,无从下手。
|
||||
|
||||
### 三十五、防复发:归属口径做成门禁(2026-10-01,用户「我只想避免类似的事件再次发生」)
|
||||
|
||||
用户放弃继续救那批旧图,要求的是**结构性防复发**。所以把这次的教训固化成三件东西:
|
||||
|
||||
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. **两道门禁(`internal/web/dataset_ownership_test.go`)**
|
||||
- 运行时 `TestOnlyBackupDerivedTablesCascadeFromDatasets`:查 `pg_constraint`,
|
||||
凡挂 `datasets` 的 FK 其表名必须在白名单里;**反向也查**(白名单里的表不许被顺手摘掉,
|
||||
否则删数据集会留成片孤儿行、或外键变 RESTRICT 之后干脆删不动)。
|
||||
- 静态 `TestSchemaDeclaresNoDatasetFKForUserData`:直接扫 `schema.sql` 的 `CREATE TABLE`。
|
||||
只看运行时有盲点——**新建的库走 CREATE TABLE**,开发库可能压根没跑过那次迁移。
|
||||
- 新加一张带 dataset_id 的表时,写 CREATE TABLE 的那一刻就会被拦住。
|
||||
|
||||
3. **体检常驻(真正的预防)**:`imageHealth` 每次进后台都算
|
||||
「库里 N 张 / 本地盘上 M 个 / 落单 K 个」,落单 > 0 直接在首页报警。
|
||||
那次事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**,
|
||||
等照片看不见了才追——让它当场可见,同样的漂移下次立刻亮
|
||||
(`TestAdminHomeReportsOrphanImages` 守着这个提示)。
|
||||
只扫本地盘(毫秒级);对象存储要列一次桶,放在「扫描并恢复」动作里,不在每次进后台的开销里。
|
||||
|
||||
顺带订正:后台「删除数据集」的二次确认文案在专题改完之后已经说过时话(说专题会一起删),改成实话。
|
||||
|
||||
### 三十五补记:变异验证又抓出一个真缺口(2026-10-01)
|
||||
|
||||
给两道门禁做**变异验证**时(按项目纪律,新回归测试必须证明它会响):
|
||||
- 静态门禁:把 `topics` 的 CASCADE 外键补回去 ⇒ 立刻红,还原后绿 ✓
|
||||
(顺带发现扫描必须先跳过 `--` 注释行:规矩的说明文字里就写着 `REFERENCES datasets(id)`,
|
||||
不跳过会把自己写的文档当成违规误报。)
|
||||
- 运行时门禁:手动给 `topics` 加上外键后**没响**——因为夹具每次都跑 `db.Migrate`,
|
||||
迁移自己就把它摘了。⇒ 运行时判据单独看不够:它永远不会在不健康的库上报警。
|
||||
真正的自愈靠迁移,于是补了 `TestMigrationDropsStaleDatasetForeignKeys`:
|
||||
把事故原样的外键**注入回去**再跑一次迁移,断言它们没了(并先确认变异真的生效,防假绿)。
|
||||
|
||||
而这条自愈用例又抓出一个**此前漏掉的缺口**:`place_notes` / `place_votes` 上的同类外键
|
||||
(更早那次改坐标键时摘的,老库/生产库上未必摘干净)**迁移并不会摘**——
|
||||
原来只写了 `place_images` 与 `topics` 两句 DROP。已补成四张自建数据表全覆盖
|
||||
(含 `place_visitor_votes` 兜底)。**部署出去的库要能自愈,DROP 必须覆盖全部同类表,不是只覆盖这次动过的那两张。**
|
||||
|
||||
@@ -52,6 +52,15 @@
|
||||
- 站内新建行 `entity_raw.raw IS NULL`;`ZCATEGORY_` 真机只有 `MKPOICategoryXxx` 枚举。
|
||||
- 类别候选:编辑/照片页=库里在用 `GROUP BY`;补全页=在用 ∪ 静态表;下拉留「其它(自己写)」,默认「不动」。
|
||||
|
||||
## 归属口径 / 防复发(details 卅五)
|
||||
- ⚠️ **铁律:这份记录能不能从备份重新导入?** 能 ⇒ 可挂 `dataset_id … ON DELETE CASCADE`
|
||||
(places/visits/movements/weather/activities/tags/entity_raw);**不能 ⇒ 只留 dataset_id 当出处、不许挂外键**
|
||||
(place_images/place_notes/place_votes/topics)。挂在备份里没有的表上 = 删数据集就永久丢失。
|
||||
- 两道门禁都钉着它:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ **静态扫 `schema.sql`**
|
||||
(新建的库走 CREATE TABLE,只看运行时会有盲点)。`dataset_ownership_test.go`
|
||||
- **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`)——
|
||||
事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。
|
||||
|
||||
## 跨备份 / 附属数据(details 十九~廿一、卅四)
|
||||
- 预演按 `VisitUUID`/`ActivityUUID` 对齐、只读;`raw #>> '{ZIDENTIFIER_,$blob}'` 是 base64 须转 hex 比;地点只认坐标(`PlaceCoordKey` 6 位)。
|
||||
- 附属数据(备注/投票/图片)主键=`place_key` 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。
|
||||
|
||||
Reference in New Issue
Block a user