From 726d155d04334b93f92980925cdfa562fa3b4bfb Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 10 May 2026 00:19:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(v0211):=20rebuildFtsTagsForNote=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC=20+=20tags=20=EB=B3=80=EA=B2=BD=20path=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20(single=20write)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/main/repository/NoteRepository.ts | 19 +++++++++++++++++ tests/unit/NoteRepository.test.ts | 30 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index 3567a45..7ed96b0 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -161,6 +161,7 @@ export class NoteRepository { linkTag.run(id, tagRow.id); } this.db.prepare(`DELETE FROM pending_jobs WHERE note_id=?`).run(id); + this.rebuildFtsTagsForNote(id); }); tx(); } @@ -390,6 +391,7 @@ export class NoteRepository { const row = getOrInsert.get(t) as { id: number }; link.run(id, row.id); } + this.rebuildFtsTagsForNote(id); } }); tx(); @@ -851,6 +853,23 @@ export class NoteRepository { .run(nextRunAt, lastError.slice(0, 500), noteId); } + /** + * v0.2.11 Cut D — note_tags 변경 후 notes_fts.tags 컬럼 (csv) 재구성. + * 단일 write path 패턴: tags 변경하는 모든 메서드가 같은 transaction 끝에서 호출. + */ + private rebuildFtsTagsForNote(noteId: string): void { + const row = this.db + .prepare( + `SELECT COALESCE(GROUP_CONCAT(t.name, ' '), '') AS csv + FROM note_tags nt JOIN tags t ON t.id = nt.tag_id + WHERE nt.note_id = ?` + ) + .get(noteId) as { csv: string }; + this.db + .prepare(`UPDATE notes_fts SET tags = ? WHERE note_id = ?`) + .run(row.csv, noteId); + } + private hydrate(row: Record): Note { const tags = this.db .prepare( diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 3fe82cd..6a59bab 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -1074,3 +1074,33 @@ describe('NoteRepository — note_revisions', () => { expect(rows[0]).toEqual({ raw_text: 'hello', edited_by: 'capture' }); }); }); + +describe('NoteRepository — notes_fts tags sync (v0.2.11 Cut D)', () => { + let db: Database.Database; + + beforeEach(() => { + db = new Database(':memory:'); + runMigrations(db); + }); + + it('updateAiResult 후 notes_fts.tags 가 csv 로 sync', () => { + const repo = new NoteRepository(db); + const { id } = repo.create({ rawText: '회의 본문' }); + repo.updateAiResult(id, { title: '제목', summary: '요약', tags: ['기획', '회의'], provider: 'p' }); + const row = db + .prepare(`SELECT tags FROM notes_fts WHERE note_id=?`) + .get(id) as { tags: string }; + expect(row.tags.split(' ').sort()).toEqual(['기획', '회의']); + }); + + it('updateUserAiFields tags 갱신 후 notes_fts.tags 동기', () => { + const repo = new NoteRepository(db); + const { id } = repo.create({ rawText: '본문' }); + repo.updateAiResult(id, { title: 't', summary: 's', tags: ['old'], provider: 'p' }); + repo.updateUserAiFields(id, { tags: ['new1', 'new2'] }); + const row = db + .prepare(`SELECT tags FROM notes_fts WHERE note_id=?`) + .get(id) as { tags: string }; + expect(row.tags.split(' ').sort()).toEqual(['new1', 'new2']); + }); +});