From aa49b4eeb215a72563b3d1a8f6e2e80290383105 Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Sat, 15 Aug 2026 14:44:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(kbdb):=20=E5=88=AA=20record=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=88=AA=E6=8E=89=E5=88=A5=E4=BA=BA=E9=82=84=E6=8C=87?= =?UTF-8?q?=E8=91=97=E7=9A=84=20entry=EF=BC=88Arcrun#128=20=E7=9A=84?= =?UTF-8?q?=E5=BF=85=E7=84=B6=E9=85=8D=E5=A5=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一筆讓 slot 可以指向既有 entry 之後,同一條 entry 會同時被別的 record 指著。 deleteRecord 舊寫法是「這筆 record 的每個 entry_id 都刪掉」,在共用的情況下會: · 把**別人還在用**的那條水池資料一起刪掉(他的 slot 從此指向不存在的列),或 · 撞上 `entry_values.entry_id REFERENCES entries(id)` 的 FK 而整個刪除失敗 ⇒ 條件改成「已經沒有任何 entry_values 指著它」才刪 (`DELETE FROM entries WHERE id = ? AND NOT EXISTS (SELECT 1 FROM entry_values WHERE entry_id = ?)`) **沒有共用時行為與舊版完全相同**:這筆 record 的關聯列已先刪掉,若沒有別人指著, NOT EXISTS 恆為真 ⇒ 照樣刪。差別只出現在真的被共用的那一條上。 驗(同檔 +2 條,真 SQLite 且 `PRAGMA foreign_keys = ON`) · 兩筆 record 共用一段、刪掉其中一筆:entries 2 → 1(只少掉那筆自己專屬的 title), 共用那條還在,另一筆 record 讀回來的 slot 值不受影響 · 沒有共用時:3 個 slot 的 entry 全刪、entry_values 歸零、getRecord 回 null(同舊版) kbdb 全套 232 綠 Co-Authored-By: Claude Opus 5 --- kbdb/src/actions/record-crud.ts | 19 +++++++++++-- kbdb/tests/record-entry-ref.test.ts | 43 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/kbdb/src/actions/record-crud.ts b/kbdb/src/actions/record-crud.ts index b9279b6..682099e 100644 --- a/kbdb/src/actions/record-crud.ts +++ b/kbdb/src/actions/record-crud.ts @@ -325,7 +325,19 @@ export async function searchByTemplate(db: D1Database, template: string, owner_i return ids.map((id) => byId.get(id)).filter((r): r is RecordResult => !!r); } -/** 刪除一筆 record:先刪 entry_values(FK),再刪底層 entries。回 false 表示 record 不存在。 */ +/** + * 刪除一筆 record:先刪 entry_values(FK),再刪底層 entries。回 false 表示 record 不存在。 + * + * 🔴 **還有別人指著的 entry 不刪**(Arcrun#128 的必然配套,不是順手加的): + * slot 值可以指向既有 entry 之後,同一條 entry 會同時被別的 record 指著。 + * 舊寫法「這筆 record 的每個 entry_id 都刪掉」在那種情況下會: + * · 把**別人還在用**的那條水池資料一起刪掉(他的 slot 從此指向不存在的列),或 + * · 撞上 `entry_values.entry_id REFERENCES entries(id)` 的 FK 而整個刪除失敗。 + * ⇒ 條件改成「已經沒有任何 entry_values 指著它」才刪。 + * + * **沒有共用時行為與舊版完全相同**:這筆 record 的關聯列已先刪掉,若沒有別人指著, + * `NOT EXISTS` 恆為真 ⇒ 照樣刪。差別只出現在「真的被共用」的那條上。 + */ export async function deleteRecord(db: D1Database, recordId: string): Promise { const evRes = await db .prepare('SELECT entry_id FROM entry_values WHERE record_id = ?') @@ -335,7 +347,10 @@ export async function deleteRecord(db: D1Database, recordId: string): Promise { + it('共用的 entry 在另一筆 record 刪掉後仍在,且那筆 record 讀得到;沒共用的照舊被刪', async () => { + const { db, raw } = makeSqliteD1(); + await createTemplate(db, { name: 'wiki', slots: WIKI_SLOTS, created_by: 'system' }); + const shared = await createEntry(db, { content: '兩張卡共用的段落', entry_type: 'block', owner_id: 'leo' }); + + const a = await createRecord(db, { + template: 'wiki', + values: { title: 'A 專屬' }, + entry_ids: { gloss: shared.id }, + owner_id: 'leo', + }); + const b = await createRecord(db, { template: 'wiki', entry_ids: { gloss: shared.id }, owner_id: 'leo' }); + const aTitleEntry = entryIdOfSlot(raw, a.record_id, 'title')!; + + const before = countEntries(raw); + expect(await deleteRecord(db, a.record_id)).toBe(true); + const after = countEntries(raw); + console.log(`[#128 配套] 刪掉 A 之後 entries: ${before} → ${after}(只該少掉 A 專屬那一筆)`); + + expect(after).toBe(before - 1); // 只有 A 專屬的 title entry 被刪 + expect(await getEntry(db, aTitleEntry)).toBeNull(); + expect(await getEntry(db, shared.id)).not.toBeNull(); // 🔴 共用那條還在 + const rb = await getRecord(db, b.record_id); + expect(rb!.values.gloss).toBe('兩張卡共用的段落'); // B 沒被波及 + }); + + it('沒有共用時,deleteRecord 行為與舊版相同(底層 entries 全刪)', async () => { + const { db, raw } = makeSqliteD1(); + await createTemplate(db, { name: 'triplet', slots: ['subject', 'predicate', 'object'], created_by: 'system' }); + const rec = await createRecord(db, { + template: 'triplet', + values: { subject: 'A', predicate: 'r', object: 'B' }, + owner_id: 'leo', + }); + const before = countEntries(raw); + expect(await deleteRecord(db, rec.record_id)).toBe(true); + expect(countEntries(raw)).toBe(before - 3); + expect(countEntryValues(raw)).toBe(0); + expect(await getRecord(db, rec.record_id)).toBeNull(); + }); +});