fix(sessions): restore trigram after deferred bootstrap
This commit is contained in:
+18
-1
@@ -547,10 +547,12 @@ class SessionSchemaMixin:
|
||||
"""Body of :meth:`_recover_stale_fts`; caller holds rebuild authority. One write
|
||||
transaction, so no canonical writer slips between rebuild and trigger restoration."""
|
||||
try:
|
||||
include_trigram = self._fts_table_probe(cursor, "messages_fts_trigram") is True
|
||||
trigram_present = self._fts_table_probe(cursor, "messages_fts_trigram") is True
|
||||
except (sqlite3.DatabaseError, UnicodeDecodeError):
|
||||
# A corrupt vtable may fail even a LIMIT 0 probe; still include it in the drop-and-recreate.
|
||||
include_trigram = True
|
||||
else:
|
||||
include_trigram = trigram_present or (not legacy and self._trigram_tokenizer_available(cursor))
|
||||
|
||||
drop_sql = "".join(f"DROP TRIGGER IF EXISTS {trigger};" for trigger in _FTS_TRIGGERS)
|
||||
if include_trigram:
|
||||
@@ -590,6 +592,21 @@ class SessionSchemaMixin:
|
||||
logger.warning("Rebuilt stale state.db FTS indexes from canonical messages and restored sync triggers.")
|
||||
return True
|
||||
|
||||
def _trigram_tokenizer_available(self, cursor: sqlite3.Cursor) -> bool:
|
||||
"""Probe trigram support without publishing a persistent FTS object."""
|
||||
probe = "temp.hermes_fts5_trigram_probe"
|
||||
cursor.execute(f"DROP TABLE IF EXISTS {probe}")
|
||||
try:
|
||||
cursor.execute(f"CREATE VIRTUAL TABLE {probe} USING fts5(content, tokenize='trigram')")
|
||||
except sqlite3.OperationalError as exc:
|
||||
if not self._is_trigram_unavailable_error(exc):
|
||||
raise
|
||||
self._warn_trigram_unavailable(exc)
|
||||
return False
|
||||
finally:
|
||||
cursor.execute(f"DROP TABLE IF EXISTS {probe}")
|
||||
return True
|
||||
|
||||
# ── Declarative column reconciliation ──────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Fresh state.db FTS bootstrap must honor cross-process rebuild admission."""
|
||||
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -22,6 +23,17 @@ with fts_rebuild_admission(Path(sys.argv[1]), timeout_seconds=0) as acquired:
|
||||
"""
|
||||
|
||||
|
||||
def _trigram_supported() -> bool:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
conn.execute("CREATE VIRTUAL TABLE trigram_probe USING fts5(content, tokenize='trigram')")
|
||||
except sqlite3.OperationalError:
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
def test_fresh_fts_bootstrap_does_not_publish_schema_without_admission(tmp_path, monkeypatch):
|
||||
"""A fresh opener that loses admission leaves no partially initialized FTS surface."""
|
||||
db_path = tmp_path / "state.db"
|
||||
@@ -53,6 +65,11 @@ def test_fresh_fts_bootstrap_does_not_publish_schema_without_admission(tmp_path,
|
||||
try:
|
||||
assert recovered._fts_stale is False
|
||||
assert recovered._fts_enabled is True
|
||||
if _trigram_supported():
|
||||
assert recovered._trigram_available is True
|
||||
assert recovered._conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE name = 'messages_fts_trigram'"
|
||||
).fetchone() is not None
|
||||
recovered.create_session("s1", source="test")
|
||||
recovered.append_message("s1", "user", "fresh bootstrap recovered")
|
||||
assert recovered.search_messages("recovered")
|
||||
|
||||
Reference in New Issue
Block a user