Files
Arcrun/kbdb/migrations/0007_tree_record_model.sql
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

139 lines
9.5 KiB
SQL
Raw Permalink 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 有身分,關係是唯一機制(樹狀 record 模型第一刀)
-- 規格:system-dev/docs/3-specs/pending-changes.md「record 要有身分」v7 定稿(leo 2026-08-15 confirm
--
-- 模型一句話(leo 定案):「真身在 pool 的 entry 裡,所有的虛擬表虛擬欄位都是指向這個 entry 的指標。」
-- · record = 池中一顆有身分的 entry(沿用原 record_id 字串當 id,既有引用不失效)
-- · 欄位是關係、歸屬也是關係——同一種機制:一列關係 = src/rel/dst 三個型別化指標欄
-- (「池上型別化指標欄」,parent_id 是這族欄位的既有先例。紅線:不准用關係實作關係、
-- 不准把指標塞回 content/JSON——那是 D91 的位置換個門進來)
-- · 舊 entry_values 四欄的下場:record_id → record entry 自己的 identry_id → 指標終點(dst
-- slot_name → 指標謂詞(rel field entry)/template_id → 一條屬於關係(rel sys_belongs
-- · 那張表該死的理由:它是「關係」的第二套實作——同一件事兩個實作必然漂移(D92)
--
-- 資料遷移形狀(v7 §3):既有 value entries 一列都不動(id 穩定、向量索引不失效)、
-- 全部純 INSERT、零 UPDATE。以舊儲存格列 id 衍生關係列 id ⇒ INSERT OR IGNORE 天然冪等。
--
-- 冪等設計(三條套用路徑:官方 wrangler migrationscli deploy.ts 每次重跑/安裝器逐句重放):
-- · 除了三句 ADD COLUMN 之外,每一句都是語句級冪等(IF NOT EXISTSINSERT OR IGNORE
-- · SQLite 沒有「ADD COLUMN IF NOT EXISTS」——重跑時那三句會報 duplicate column
-- 套用端必須把「duplicate column」視為已套用(cli deploy.ts 的 applyD1MigrationTolerant、
-- 安裝器逐句 try/catch 本就容錯)。官方路徑走 wrangler migrations 追蹤表,只跑一次。
-- · 不走「重建整張 entries」的做法:那雖然能純語句冪等,但 deploy.ts 每次部署都重跑
-- migration ⇒ 每次部署全表複製一輪,直接吃掉 D1 每日列寫入額度。
-- · entry_values 拆表用 0006 的「墊表、搬、拆」手法:重跑時先墊一份空殼,搬 0 筆,再拆,無害。
--
-- 施工窗口注意:本檔跑完 entry_values 就不存在了,必須與讀寫端改版(kbdb/src 同一批)一起部署。
-- 舊 worker 碰新庫會炸 entry_values 不存在——這是刻意的(leo:「不做長期雙讀相容層,一次翻」)。
-- ============================================================
-- 1. 關係的物理載體:池上型別化指標欄 + 一對方向索引
-- ============================================================
ALTER TABLE entries ADD COLUMN src_id TEXT; -- kbdb-sql-ok: 牆內 migration 本體(v7 定案載體「池上型別化指標欄」),重跑由套用端容錯 duplicate column
ALTER TABLE entries ADD COLUMN rel_id TEXT; -- kbdb-sql-ok: 同上
ALTER TABLE entries ADD COLUMN dst_id TEXT; -- kbdb-sql-ok: 同上
-- 一對方向索引(v7 §2-1):src 端服務「r1 的所有欄位」,(dst,rel) 端服務
-- 「某張 sheet 的所有 record」(rel=屬於, dst=sheet)與「誰指到 e1」(dst=e1)。
-- partial index:內容 entry(指標欄全 NULL)不進索引,索引大小 ≈ 關係列數。
CREATE INDEX IF NOT EXISTS idx_entries_rel_src ON entries(src_id) WHERE src_id IS NOT NULL; -- kbdb-sql-ok: 牆內 migration 本體,普通欄位索引(0001 十條索引的同族)
CREATE INDEX IF NOT EXISTS idx_entries_rel_dst ON entries(dst_id, rel_id) WHERE dst_id IS NOT NULL; -- kbdb-sql-ok: 同上
-- ============================================================
-- 2. 啟動常數(v7 §2-3:一組、極小、只讀)
-- ============================================================
-- 保留根:屬於鏈的終點。某顆 entry ─屬於→ sys_root 它是一張 sheet。
INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES ('sys_root', 'root', 'system', NULL);
-- 「屬於」謂詞:record ─屬於→ sheet(歸屬關係的 rel)。
INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES ('sys_belongs', 'belongs', 'system', NULL);
-- 「欄位屬於表」謂詞:field ─field_of→ sheetsheet 的欄位名冊,與 record 歸屬分開,
-- 免得「某張 sheet 的所有 record」把欄位也撈進來)。
INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES ('sys_field_of', 'field_of', 'system', NULL);
-- ============================================================
-- 3. 墊表(0006 手法):本檔尾端會拆掉 entry_values,重跑時先墊空殼
-- 讓下面的搬遷語句永遠合法(搬 0 筆),最後再拆一次。
-- ============================================================
CREATE TABLE IF NOT EXISTS entry_values ( -- kbdb-sql-ok: 表退場施工步驟①保底存在(0006 同款),非資料存取違規
id TEXT PRIMARY KEY,
record_id TEXT NOT NULL,
template_id TEXT NOT NULL,
slot_name TEXT NOT NULL,
entry_id TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch()),
UNIQUE(record_id, slot_name)
);
-- ============================================================
-- 4. sheetfield 落池(templates 表遷移期雙軌:欄位定義的真相源暫仍在 templates 表,
-- sheet/field entry 是它在池中的身分——第二刀把 templates 表整個退役)
-- ============================================================
-- 每個 template 一顆 sheet entryid 沿用 template id,既有引用不失效)
INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id, created_at, updated_at)
SELECT t.id, t.name, 'sheet', NULL, t.created_at, t.updated_at FROM templates t;
-- sheet ─屬於→ 保留根(「什麼讓一顆 entry 成為 sheet」的答案:這條關係)
INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id)
SELECT 'relb_' || t.id, 'relation', t.id, 'sys_belongs', 'sys_root' FROM templates t;
-- 每個 slot 一顆 field entryid 決定性衍生:fld_<template>_<slot>,欄名=關係的謂詞)
INSERT OR IGNORE INTO entries (id, content, entry_type)
SELECT 'fld_' || t.id || '_' || j.value, j.value, 'field'
FROM templates t, json_each(t.slots_json) j;
-- field ─field_of→ sheet(欄位名冊)
INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id)
SELECT 'relf_' || t.id || '_' || j.value, 'relation', 'fld_' || t.id || '_' || j.value, 'sys_field_of', t.id
FROM templates t, json_each(t.slots_json) j;
-- 保險網:entry_values 裡實際用過、但 slots_json 沒宣告的 slot(宣告與實作漂移的實證),
-- 一樣補出 field entry 與名冊,資料一筆都不能掉。
INSERT OR IGNORE INTO entries (id, content, entry_type)
SELECT DISTINCT 'fld_' || ev.template_id || '_' || ev.slot_name, ev.slot_name, 'field' FROM entry_values ev;
INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id)
SELECT DISTINCT 'relf_' || ev.template_id || '_' || ev.slot_name, 'relation',
'fld_' || ev.template_id || '_' || ev.slot_name, 'sys_field_of', ev.template_id
FROM entry_values ev;
-- ============================================================
-- 5. record 有身分:每筆 record 一顆池中 entryid 原 record_id 字串)
-- 歸屬 owner 沿用「第一個非 NULL 的 slot entry owner」——與舊讀端的推導逐字同義。
-- INSERT OR IGNORE 的另一層意義:record_id 已經是池中既有 entrylibrary_map 的
-- block 即 record 慣例)時,那顆 entry 本人就是身分,不另建。
-- ============================================================
INSERT OR IGNORE INTO entries (id, entry_type, owner_id, created_at, updated_at)
SELECT ev.record_id, 'record',
(SELECT e2.owner_id FROM entry_values ev2 JOIN entries e2 ON ev2.entry_id = e2.id
WHERE ev2.record_id = ev.record_id AND e2.owner_id IS NOT NULL LIMIT 1),
MIN(ev.created_at), MIN(ev.created_at)
FROM entry_values ev GROUP BY ev.record_id;
-- record ─屬於→ sheet(舊 template_id 欄的下場:變成一條關係)
INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id, created_at)
SELECT 'relb_' || ev.record_id || '_' || ev.template_id, 'relation',
(SELECT e2.owner_id FROM entry_values ev2 JOIN entries e2 ON ev2.entry_id = e2.id
WHERE ev2.record_id = ev.record_id AND e2.owner_id IS NOT NULL LIMIT 1),
ev.record_id, 'sys_belongs', ev.template_id, MIN(ev.created_at)
FROM entry_values ev GROUP BY ev.record_id, ev.template_id;
-- ============================================================
-- 6. 每個儲存格 → 一條關係列(src=record、rel=欄位謂詞、dst=原 value entry
-- 關係列 id 以舊儲存格列 id 衍生(relv_<ev.id>)⇒ 重跑天然冪等。
-- ============================================================
INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id, created_at)
SELECT 'relv_' || ev.id, 'relation', e.owner_id,
ev.record_id, 'fld_' || ev.template_id || '_' || ev.slot_name, ev.entry_id, ev.created_at
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id;
-- ============================================================
-- 7. 拆表(本提案的完成證明:關係的第二套實作不再有位置)
-- ============================================================
DROP TABLE IF EXISTS entry_values; -- kbdb-sql-ok: 表退場施工(0006 同款),v7 定案「兩套機制合成一套」的落地