ceb7638d74
規格: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>
139 lines
9.5 KiB
SQL
139 lines
9.5 KiB
SQL
-- 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 自己的 id/entry_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 migrations/cli deploy.ts 每次重跑/安裝器逐句重放):
|
||
-- · 除了三句 ADD COLUMN 之外,每一句都是語句級冪等(IF NOT EXISTS/INSERT 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→ sheet(sheet 的欄位名冊,與 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. sheet/field 落池(templates 表遷移期雙軌:欄位定義的真相源暫仍在 templates 表,
|
||
-- sheet/field entry 是它在池中的身分——第二刀把 templates 表整個退役)
|
||
-- ============================================================
|
||
|
||
-- 每個 template 一顆 sheet entry(id 沿用 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 entry(id 決定性衍生: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 一顆池中 entry(id = 原 record_id 字串)
|
||
-- 歸屬 owner 沿用「第一個非 NULL 的 slot entry owner」——與舊讀端的推導逐字同義。
|
||
-- INSERT OR IGNORE 的另一層意義:record_id 已經是池中既有 entry(library_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 定案「兩套機制合成一套」的落地
|