Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions migrations/V4__fix_fts_stale_on_edit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- V4: ノート編集時に FTS 索引が旧テキストのまま残るバグの修正。
-- これまで upsert が text 列を更新せず、FTS にも AFTER UPDATE トリガが
-- なかったため、編集されたノートは検索で取りこぼされていた。

-- external content FTS5 の delete + insert パターン。
-- FTS 側にエントリが存在するのは text IS NOT NULL の行のみ(V1 の insert
-- トリガの不変条件)で、存在しないエントリへの 'delete' は database corrupt
-- 扱いになるため、INSERT ... SELECT ... WHERE で条件付き実行にする。
-- 2 トリガに分割しないのは発火順が未定義なため(delete → insert の順を保証する)。
CREATE TRIGGER IF NOT EXISTS notes_fts_au
AFTER UPDATE OF text ON notes_cache BEGIN
INSERT INTO notes_fts(notes_fts, rowid, text)
SELECT 'delete', old.rowid, old.text WHERE old.text IS NOT NULL;
INSERT INTO notes_fts(rowid, text)
SELECT new.rowid, new.text WHERE new.text IS NOT NULL;
END;

-- 既存 DB では text 列が note_json と乖離している可能性があるため修復する。
-- この UPDATE は上のトリガを発火させ、FTS 索引も追随する。
UPDATE notes_cache
SET text = json_extract(note_json, '$.text')
WHERE json_valid(note_json)
AND text IS NOT json_extract(note_json, '$.text');
34 changes: 34 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ impl Database {
"INSERT INTO notes_cache (note_id, account_id, server_host, created_at, text, note_json, cached_at, timeline_type, uri)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
ON CONFLICT(note_id, account_id) DO UPDATE SET
text = excluded.text,
note_json = excluded.note_json,
cached_at = excluded.cached_at,
timeline_type = excluded.timeline_type,
Expand Down Expand Up @@ -1551,6 +1552,39 @@ mod tests {
assert_eq!(results[0].id, "n1");
}

#[test]
fn fts_search_reflects_note_edit() {
let (_dir, db) = temp_db();
db.cache_notes(&[sample_note("n1", "before edit text")], "home")
.unwrap();

// 同じノートが編集後のテキストで再キャッシュされる(Misskey のノート編集)
db.cache_notes(&[sample_note("n1", "after edit text")], "home")
.unwrap();

let hit_new = db.search_cached_notes("acc-1", "after", 10).unwrap();
assert_eq!(hit_new.len(), 1, "編集後テキストで検索できること");
assert_eq!(hit_new[0].id, "n1");

let hit_old = db.search_cached_notes("acc-1", "before", 10).unwrap();
assert!(hit_old.is_empty(), "編集前テキストの索引が残らないこと");
}

#[test]
fn fts_search_reflects_edit_from_null_text() {
let (_dir, db) = temp_db();
// text が null のノート(renote 等)が後からテキスト付きで再キャッシュされる
let mut no_text = sample_note("n1", "");
no_text.text = None;
db.cache_notes(&[no_text], "home").unwrap();

db.cache_notes(&[sample_note("n1", "now has text")], "home")
.unwrap();

let results = db.search_cached_notes("acc-1", "now has", 10).unwrap();
assert_eq!(results.len(), 1);
}

#[test]
fn cache_date_range() {
let (_dir, db) = temp_db();
Expand Down
Loading