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:
+19
-6
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user