Files
Arcrun/kbdb/src/actions/relation-orphans.ts
T
uncle6me-web ceb7638d74 feat(kbdb): 樹狀 record 模型第一刀——record 有身分、關係是唯一機制、entry_values 拆表(v7 定稿實作)
規格:system-dev/docs/3-specs/pending-changes.md「record 要有身分」v7 定稿(leo 2026-08-15 confirm)。
模型一句話(leo):「真身在 pool 的 entry 裡,所有的虛擬表虛擬欄位都是指向這個 entry 的指標。」

- 0007 migration:池上型別化指標欄(src/rel/dst)+一對方向 partial index+啟動常數
  (sys_root/sys_belongs/sys_field_of)+templates 鏡射成 sheet/field entry+
  每筆 record 一顆身分 entry(id=原 record_id,引用不失效)+每格一條關係列
  (id 由舊儲存格列 id 衍生 ⇒ INSERT OR IGNORE 天然冪等)+拆 entry_values
  (0006 墊表→搬→拆手法)。純 INSERT、value entries 一列不動(向量索引不失效)。
- record-crud 整份改寫到關係列(#128 指標語意/共用保護/N+1 批次/租戶過濾全數保留,
  驗收測試 232→236 綠);library-map 四段縱轉橫 SQL、records triplet-stats 改查關係列。
- entry-crud:機制列隔離(未指定 entry_type 的列表/搜尋不回機制節點);deleteEntry
  接手舊 entry_values FK 的不變量(dst 被指著→拒刪)。
- 孤兒偵測重設計(v7 §5 點名):新模型孤兒=指標指向不存在 id 的關係列,
  LEFT JOIN 斷鏈掃描(承接 2026-06-24 清理事故的 FK 形狀),
  GET /maintenance/relation-orphans 唯讀巡檢。
- cli deploy.ts:0007 逐句套用+容錯 duplicate column(SQLite 無欄位級 IF NOT EXISTS,
  整檔送 /query 會在重跑時假紅)。
- 測試:tree-record-migration.test.ts 驗資料零漏/雙跑冪等/孤兒掃描;
  釘死三表的斷言依 confirm 後規格改口(execution-log/credential-legacy 兩處)。

遷移期雙軌(第二刀收):templates 表仍是欄位定義真相源;六種 metadata_json 打包型
與 §7 減法封鎖(拿掉 entry_type/metadata_json 欄)留待第二刀。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 21:34:48 +08:00

55 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 關係列孤兒巡檢(0007 樹狀 record 模型的配套,v7 §5 明訂「要寫成明確的巡檢項,不能默認」)。
//
// 為什麼要有這支(不是順手加的):舊模型的孤兒偵測靠 entry_values 的外鍵形狀——
// 2026-06-24 那次 11 萬筆誤寫的清理就是拿 FK LEFT JOIN 找斷鏈
// system-dev/docs/5-records/2026-06-24-official-kbdb-cleanup-leo-misdelete.md)。
// 0007 拆掉 entry_values 後,關係列的 src/rel/dst 指標**沒有 FK**(同一張表指自己,
// SQLite 自參照 FK 會把插入順序綁死,且 D1 逐句執行無交易可 defer)⇒ 斷鏈不再被
// 資料庫擋下,改由本巡檢主動找:**孤兒=指標指向不存在 id 的關係列**。
// 掃法與舊 FK 形狀同款(LEFT JOIN 找斷鏈),v7 押的「形狀可承接」在這裡兌現。
//
// 什麼情況會產生孤兒(誠實列):
// 1. DELETE /entries/:id 的舊資料時代殘骸(新 deleteEntry 已擋 dst 被指著的刪除)
// 2. 遷移時 template 已被刪但 entry_values 還留著格子(0007 保險網補 field entry
// 但 dst=template 的名冊關係可能指到不存在的 sheet)
// 3. 未來任何繞過牆的直接寫入(本巡檢就是抓它們的網)
import type { D1Database } from '@cloudflare/workers-types';
export interface RelationOrphan {
relation_id: string;
role: 'src' | 'rel' | 'dst';
missing_id: string;
}
export interface RelationOrphanReport {
orphans: RelationOrphan[];
count: number; // 本次回報筆數(受 limit 截斷)
truncated: boolean; // true = 還有更多,加大 limit 或先清這批再掃
}
export async function scanRelationOrphans(db: D1Database, limit = 200): Promise<RelationOrphanReport> {
const cap = Math.min(Math.max(limit, 1), 1000);
const res = await db
.prepare(
`SELECT relation_id, role, missing_id FROM (
SELECT r.id AS relation_id, 'src' AS role, r.src_id AS missing_id
FROM entries r LEFT JOIN entries t ON t.id = r.src_id
WHERE r.src_id IS NOT NULL AND t.id IS NULL
UNION ALL
SELECT r.id, 'rel', r.rel_id
FROM entries r LEFT JOIN entries t ON t.id = r.rel_id
WHERE r.rel_id IS NOT NULL AND t.id IS NULL
UNION ALL
SELECT r.id, 'dst', r.dst_id
FROM entries r LEFT JOIN entries t ON t.id = r.dst_id
WHERE r.dst_id IS NOT NULL AND t.id IS NULL
) LIMIT ?`,
)
.bind(cap + 1)
.all<RelationOrphan>();
const rows = res.results ?? [];
const truncated = rows.length > cap;
const orphans = truncated ? rows.slice(0, cap) : rows;
return { orphans, count: orphans.length, truncated };
}