fix(state): catch corruption-class DatabaseError in optimize_fts() too

optimize_fts() runs the same per-index loop as rebuild_fts() and had the
same OperationalError-only arm. SQLITE_CORRUPT surfaces as
sqlite3.DatabaseError (a sibling, not a subclass), so one corrupt index
aborted the loop and skipped optimizing the healthy sibling indexes;
vacuum() then swallowed the escaped error and reported 0. Widen the arm
to DatabaseError (OperationalError is its subclass, so the existing
behaviour is unchanged) to close the whole class.

Extend the salvaged test with an optimize_fts parametrize row (red
without this change) and fix the 'is_caused' -> 'is_caught' typo in the
test name.

Refs #133375

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
kshitijk4poor
2026-10-06 09:44:19 +05:30
committed by kshitij
parent e9a981bb6c
commit a0b14df1bc
2 changed files with 11 additions and 10 deletions

View File

@@ -1287,7 +1287,7 @@ class SessionSearchMixin:
try:
self._conn.execute(f"INSERT INTO {tbl}({tbl}) VALUES('optimize')")
optimized += 1
except sqlite3.OperationalError as exc:
except sqlite3.DatabaseError as exc: # SQLITE_CORRUPT is not an OperationalError
logger.warning("FTS optimize failed for %s: %s", tbl, exc)
return optimized

View File

@@ -30,13 +30,13 @@ def db(tmp_path):
pass
def _corrupting_execute(match):
"""Wrap ``execute`` so ``'rebuild'`` commands whose SQL satisfies *match* raise the
def _corrupting_execute(match, command="rebuild"):
"""Wrap ``execute`` so FTS *command* statements whose SQL satisfies *match* raise the
corruption-class error; every other statement passes through."""
def wrap(real_execute):
def execute(sql, *args, **kwargs):
if "VALUES('rebuild')" in sql and match(sql):
if f"VALUES('{command}')" in sql and match(sql):
raise sqlite3.DatabaseError("database disk image is malformed")
return real_execute(sql, *args, **kwargs)
@@ -45,7 +45,7 @@ def _corrupting_execute(match):
return wrap
def test_corruption_class_error_is_caused_rolled_back_and_reported(
def test_corruption_class_error_is_caught_rolled_back_and_reported(
db, monkeypatch, caplog
):
"""The exact production failure (#133375): every index rebuild raises DatabaseError.
@@ -62,15 +62,16 @@ def test_corruption_class_error_is_caused_rolled_back_and_reported(
assert any("offline repair" in rec.message for rec in caplog.records)
def test_one_corrupt_index_does_not_stop_the_remaining_indexes(db, monkeypatch):
"""Only messages_fts is corrupt; trigram/cjk must still be rebuilt — the loop survives
@pytest.mark.parametrize("method,command", [("rebuild_fts", "rebuild"), ("optimize_fts", "optimize")])
def test_one_corrupt_index_does_not_stop_the_remaining_indexes(db, monkeypatch, method, command):
"""Only messages_fts is corrupt; trigram/cjk must still be processed — the loop survives
a corruption-class failure on one index. The trailing ``(`` keeps the match off
``messages_fts_trigram``/``_cjk``."""
``messages_fts_trigram``/``_cjk``. optimize_fts() shares the per-index loop shape."""
monkeypatch.setattr(
db._conn,
"execute",
_corrupting_execute(lambda sql: sql.startswith("INSERT INTO messages_fts("))(
_corrupting_execute(lambda sql: sql.startswith("INSERT INTO messages_fts("), command)(
db._conn.execute
),
)
assert db.rebuild_fts() >= 1
assert getattr(db, method)() >= 1