docs: 复核 pi 5f3eb02d 三陷阱 —— 风险可达(实测静默丢 37 行、(a)(b) 全绿),两条理由订正

① ★★★ 陷阱1/3 成立且可达(真表名实测):
   重建若为 DDL 里 4 条无 BEGIN 语句(migrate 逐条 Exec ⇒ 各自 auto-commit),DROP 后崩
   ⇒ 次日启动建出空的新 PK 表、守卫见 PK 已新 ⇒ 跳过重建 ⇒ 实测 行=0/_new=3
   ⇒ 判据 (a) PK ✓ (b) 索引 ✓ **全绿**而镜像为 0(生产 = 37 行静默消失)⇒ (c) 必需
   ★ 我先用自造表名 aps 测,"次日启动"没跑真 DDL ⇒ 误报,换真表名后成立(模拟生产必须用真 DDL)

② ★★ 两条理由订正(结论对、机制错):
   ① "SQL BEGIN 不生效" ⇒ 实测 **BEGIN 生效**(BEGIN→DROP→ROLLBACK 表回来;崩溃后亦完好)
      真因: SetMaxOpenConns(1)(db.go:91)⇒ 池里仅一条连接 ⇒ BEGIN 恰在同连接
      ⇒ 巧合非保证(PG 分支 20)⇒ 用 BeginTx 对,理由应写"正确性依赖 MaxOpenConns(1)"
   ② "实际文本无空格" ⇒ 实际 PRIMARY KEY (agent_name, platform_id) **有**空格
      失配真因是**模式里 , 后少空格** ⇒ 用 pragma 对,理由应写"LIKE 依赖空白/换行/引号形态"

③ ✅ pi 自撤"第二次 Migrate 撞 PK"复核为真(4 语句版幂等,run1/2/3 rc=0)
④ ✅ 附带: main.go 两次 Migrate ⇒ 必须幂等;REFERENCES agent_platform_sessions = 0 ⇒ 无需管 FK
⑤ ④ 最终措辞 + 收 pi"顺序依赖可消掉"(同事务内重建索引)+ 判据加 (c)

边界: 只读 + 临时 Go 探针(已删、status 已验干净);未改产品代码
This commit is contained in:
2026-09-26 02:55:25 +08:00
parent 30079bf003
commit 18970479f9

View File

@ -7456,3 +7456,61 @@ window.__AGENTMAIL_TOKEN__ = '<user_key>'; // 省略则走 Cookie
⇒ ★ 记法: **"变化了"与"向违规变化了"是两个量** ——
只看"文件变了"会让任何无关修改都通过(**场景 B 就是这么漏的**)。
```
---
- ★★★ **复核 pi `5f3eb02d`(把 ④ 当实现写时撞出的三陷阱):风险**成立且可达**(我实测复现"静默丢 37 行、判据(a)(b)全绿"),但其中**两条理由需订正**(结论对、机制错)
## (A) ★★★ 陷阱1/3: **成立且可达** —— 我用**真表名**实测复现
```
⚠️ 我先犯了一个测法错误: 第一次用自造表名 `aps` 测,"次日启动"那步**没跑真 DDL** ⇒
得不出孤儿态、误报 (a) 不绿。★ 换成真表名 `agent_platform_sessions` + 真 DDL 后成立。
(教训: 模拟"次日启动"就必须**用真的那份 DDL**,否则模拟的不是生产路径。)
⇒ 形态(若把重建写成 DDL 里 **4 条无 BEGIN 的语句**):
`migrate` 是**逐条 Exec**(`migrate.go:33`,注释明写"modernc 不接受多语句")⇒ 每条**各自 auto-commit**
⇒ `DROP TABLE` 之后崩溃 ⇒ 实测: `agent_platform_sessions` 行=**0**、`_new` 行=**3**
⇒ 次日启动: `CREATE TABLE IF NOT EXISTS` **建出空的新 PK 表** + `CREATE INDEX IF NOT EXISTS` 补索引
⇒ 守卫查"PK 是否已含 workspace" ⇒ **答"是"** ⇒ **跳过重建** ⇒ 数据**永远**躺在 `_new` 里
⇒ ★★★ 而判据 **(a)(b) 全绿**: (a) PK = `agent_name+workspace+platform_id+` ✓ ; (b) 索引存在 ✓
⇒ **镜像行数 = 0**(本表放生产 = **37 行会话静默消失**)⇒ **(a)(b) 抓不到** ⇒ pi 的 (c) **必需** ✓
```
## (B) ★★ 但 pi 的两条**理由**要订正(结论对、机制错)
```
① pi: "migrate 逐条 Exec ⇒ SQL 里 BEGIN **不生效**,故必须用 Go 层 BeginTx"
⇒ ★ 我实测 **BEGIN 生效**: `BEGIN`→`DROP`→`ROLLBACK` ⇒ 表**回来了**(行数不变);
连**崩溃**(未 COMMIT 直接 `Close`)后再打开 ⇒ `aps` **2 行完好、`_new` 不存在**
⇒ SQLite **回滚了未提交的 DROP**。
⇒ 真因: 本仓 SQLite 走 **`SetMaxOpenConns(1)`**(`db.go:91`)+ `Migrate` 用裸 `DB.ExecContext`
⇒ 池里只有**一条**连接 ⇒ 裸 `BEGIN` 恰好落在同一连接 ⇒ **能用**。
⇒ ★ 但这是**巧合、不是保证**: 连接数只要 >1(**PG 分支就是 20**,`db.go:85`),
跨连接的 `BEGIN` 就**不再构成事务** ⇒ 与 PG 侧 `DO $$…$$` 必须整体提交(`migrate.go:24-28`)同坑。
⇒ **结论**: 用 `BeginTx` **对**(显式、不依赖池大小);但**理由**应写成
"**裸 BEGIN 的正确性依赖 `MaxOpenConns(1)`**",而不是"BEGIN 不生效"。
② pi: "守卫不能用 LIKE(我写 `'…PRIMARY KEY (…'` 带空格,而**实际无空格**)"
⇒ ★ 我实测本仓实际文本 = `PRIMARY KEY (agent_name, platform_id)` —— **有**空格。
⇒ 它失配**真因**是**它模式里 `,` 后少了空格**(`'…agent_name,workspace%'` vs 实际 `'…agent_name, workspace'`):
我两种模式都试 ⇒ 无空格模式**不命中**、带空格模式**命中**。
⇒ ⇒ 结论(该用 `pragma_table_info` 的 pk 列)**对**,但理由应写成
"**LIKE 依赖 DDL 的空白/换行/引号形态,任一变即失配**"——
而不是"实际无空格"(那是把**自己模式的错**归给**被匹配的文本**;pi 这封自己也在讲同族坑)。
```
## (C) ✅ pi 自撤那条我也复核为真 + 两条附带事实
```
· 它撤回"第二次 Migrate 会撞 PK 崩溃" ⇒ ★ 我实测 4 语句版**天然幂等**: run1/2/3 均 rc=0、行数不变
(因 `RENAME aps_new→aps` 后 `aps_new` 名字**又空出来**)⇒ 撤回**正确** ✓
★ 真正的崩点是**陷阱1**(DROP 与 RENAME 之间崩),与它一致。
· `main.go` **两次** `db.Migrate`(:39/:48)⇒ 重建**必须幂等** ✓
· 全仓 `REFERENCES agent_platform_sessions` = **0** ⇒ 重建**不需要**处理 FK ✓
(`PRAGMA foreign_keys` 在事务内本就是 no-op)
· `migrate` 内**无**任何 `RENAME TO`/`DROP TABLE`/`_new` ⇒ 今天**无**重建代码 ✓(与我早前结论一致)
```
## (D) ★ ④ 的最终措辞(收 pi §四 的"顺序依赖可消掉")
```
④ = 用 **Go 层事务**重建(`BeginTx`→新表→`INSERT SELECT`→`DROP`→`RENAME`
→**同事务内显式重建 `idx_platform_sessions_ws`**);守卫用 `pragma_table_info` 的 pk 列;
并处理孤儿 `<表>_new`。
⇒ ★ 在同一事务内重建索引 ⇒ 与 DDL 批次前后**无关** ⇒ **顺序依赖消失**(pi 这点对,我收)
⇒ 判据: **(a) 迁移路径断言 PK + (b) 断言索引存在 + (c) 断言无 `<表>_new` 残留**
★ (c) 是**必需**而非可选 —— 否则 (A) 那个"37 行静默消失"的形态**无人发现**(我实测 (a)(b) 全绿)
```
## (E) 边界: 只读 + 临时 Go 探针(均在 `internal/db/` 下建、**已删**,`git status` 已验干净);未改产品代码;本文件仅追加本段