fix(state): self-heal FTS corruption on the SessionDB search path too

Complements #66296 (self-heal on the write path): search_messages()'s main
FTS5 MATCH query caught only sqlite3.OperationalError (a query-syntax error →
return empty). A corrupt FTS index raises the malformed / "fts5: corrupt
structure record" class, which is a sqlite3.DatabaseError — the parent of
OperationalError, so it was NOT caught and propagated straight out of
search_messages, crashing session/history search.

The write path now rebuilds and retries on that class, but a read-only
session (cron/CLI history search, or a search issued before any write) never
triggers a write, so its search stayed broken until the next process restart
ran the offline repair.

Catch the DatabaseError corruption class on the search MATCH read too and
route it through the existing one-shot _try_runtime_fts_rebuild(), then retry
the query. The catch is moved outside `with self._lock` so rebuild_fts() can
re-acquire the lock (mirrors _execute_write). The one-shot guard is shared
with the write path, so a single instance never loops on a genuinely
unrecoverable index. OperationalError syntax handling is unchanged (caught
first).

Adds a regression test: with a corrupted messages_fts and no post-corruption
write, search_messages() rebuilds in place and returns the match; without the
fix it raises DatabaseError.
This commit is contained in:
Frowtek
2026-07-17 19:54:38 +03:00
committed by Teknium
parent 96c1511495
commit 11710c51fc
2 changed files with 42 additions and 6 deletions
+19 -6
View File
@@ -5758,13 +5758,26 @@ class SessionDB:
like_cursor = self._conn.execute(like_sql, like_params)
matches = [dict(row) for row in like_cursor.fetchall()]
else:
with self._lock:
try:
try:
with self._lock:
cursor = self._conn.execute(sql, params)
matches = [dict(row) for row in cursor.fetchall()]
except sqlite3.OperationalError:
# FTS5 query syntax error despite sanitization — return empty
return []
except sqlite3.DatabaseError as exc:
# A corrupt FTS index raises the malformed / "fts5: corrupt
# structure record" class on the MATCH read, the same class the
# write path self-heals (#66296). OperationalError (query
# syntax) is a subclass caught above; this arm is the corruption
# parent. Rebuild the index in place once — the lock is released
# here, so rebuild_fts() can re-acquire it — and retry, so
# search self-heals for read-only sessions (cron/CLI history
# search) that never trigger a write to repair it first.
if not self._try_runtime_fts_rebuild(exc):
raise
with self._lock:
cursor = self._conn.execute(sql, params)
except sqlite3.OperationalError:
# FTS5 query syntax error despite sanitization — return empty
return []
else:
matches = [dict(row) for row in cursor.fetchall()]
# Add surrounding context (1 message before + after each match).