fix(insights): fall back to unpinned queries when the partial index is absent
The INDEXED BY pin is a hard dependency -- SQLite raises 'no such index' when the named index is missing. That happens in production: the web dashboard's usage analytics (_get_usage_analytics, _get_models_analytics) open state.db read_only=True, which skips _init_schema, so a DB last written by a pre-index version has no idx_messages_assistant_calls_by_session and every insights call crashes with OperationalError (reproduced E2E). Probe sqlite_master once in __init__ and strip the pin from the four prepared statements when absent -- identical rows, optimizer-chosen plan, no crash. Replaces the change-detector test that froze the crash as intended behavior with a fallback-equivalence test.
This commit is contained in:
parent
7f1d84fe7f
commit
401e054d59
|
|
@ -99,6 +99,29 @@ class InsightsEngine:
|
|||
"""
|
||||
self.db = db
|
||||
self._conn = db._conn
|
||||
# INDEXED BY is a hard dependency (SQLite errors on a missing index).
|
||||
# A read-only open of a state.db written by an older version skips
|
||||
# schema init and lacks the partial index — probe once and fall back
|
||||
# to the unpinned variants (identical rows, optimizer-chosen plan).
|
||||
try:
|
||||
self._has_assistant_calls_index = bool(
|
||||
self._conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='index' AND name=?",
|
||||
(self._MESSAGES_ASSISTANT_CALLS_INDEX,),
|
||||
).fetchone()
|
||||
)
|
||||
except sqlite3.Error:
|
||||
self._has_assistant_calls_index = False
|
||||
if not self._has_assistant_calls_index:
|
||||
_strip = f" INDEXED BY {self._MESSAGES_ASSISTANT_CALLS_INDEX}"
|
||||
self._GET_TOOL_CALLS_WITH_SOURCE = (
|
||||
self._GET_TOOL_CALLS_WITH_SOURCE.replace(_strip, "")
|
||||
)
|
||||
self._GET_TOOL_CALLS_ALL = self._GET_TOOL_CALLS_ALL.replace(_strip, "")
|
||||
self._GET_SKILL_CALLS_WITH_SOURCE = (
|
||||
self._GET_SKILL_CALLS_WITH_SOURCE.replace(_strip, "")
|
||||
)
|
||||
self._GET_SKILL_CALLS_ALL = self._GET_SKILL_CALLS_ALL.replace(_strip, "")
|
||||
|
||||
def generate(self, days: int = 30, source: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
@ -200,11 +223,14 @@ class InsightsEngine:
|
|||
# is deterministic on a freshly initialized state.db (before ANALYZE has
|
||||
# run) for BOTH the unfiltered and source-filtered branches — without the
|
||||
# hint the optimizer falls back to ``idx_messages_session_active`` for the
|
||||
# source-filtered probe and scans each session's non-tool-call rows. Safe
|
||||
# because the index is declared in ``SCHEMA_SQL`` (created by every
|
||||
# read-write ``SessionDB._init_schema``); every ``InsightsEngine`` caller
|
||||
# opens a read-write ``SessionDB`` — read-only attachments (which skip
|
||||
# schema init) are never used for insights.
|
||||
# source-filtered probe and scans each session's non-tool-call rows.
|
||||
#
|
||||
# The pin is a HARD dependency: SQLite raises ``no such index`` when the
|
||||
# named index is absent. That happens in practice — the web dashboard's
|
||||
# usage analytics open the DB ``read_only=True`` (skipping
|
||||
# ``_init_schema``), so a state.db created by an older writer has no
|
||||
# partial index yet. ``__init__`` probes for the index once and falls
|
||||
# back to the unpinned (still-correct, just optimizer-chosen) variants.
|
||||
_MESSAGES_ASSISTANT_CALLS_INDEX = "idx_messages_assistant_calls_by_session"
|
||||
_GET_TOOL_CALLS_WITH_SOURCE = (
|
||||
"SELECT m.tool_calls"
|
||||
|
|
|
|||
|
|
@ -452,16 +452,26 @@ class TestInsightsPopulated:
|
|||
assert any(t["tool_name"] == "search_files" for t in tools_cli)
|
||||
assert isinstance(skills, list) and isinstance(skills_cli, list)
|
||||
|
||||
def test_indexed_by_requires_the_index_to_exist(self, populated_db):
|
||||
"""INDEXED BY is a hard dependency: if the index were ever missing the
|
||||
query fails loudly rather than silently degrading. This documents why
|
||||
every InsightsEngine caller must open a read-write SessionDB."""
|
||||
def test_missing_index_falls_back_to_unpinned_queries(self, populated_db):
|
||||
"""INDEXED BY would be a hard error if the index is missing — which
|
||||
happens on read-only opens of a state.db written by an older version
|
||||
(web dashboard analytics). The engine must probe and fall back to the
|
||||
unpinned variants instead of crashing, returning identical rows."""
|
||||
engine_pinned = InsightsEngine(populated_db)
|
||||
tools_before = engine_pinned._get_tool_usage(0.0)
|
||||
|
||||
populated_db._conn.execute(f"DROP INDEX IF EXISTS {self._INDEX}")
|
||||
populated_db._conn.commit()
|
||||
with pytest.raises(sqlite3.OperationalError, match="no such index"):
|
||||
populated_db._conn.execute(
|
||||
InsightsEngine._GET_TOOL_CALLS_ALL, (0.0,)
|
||||
).fetchall()
|
||||
|
||||
engine = InsightsEngine(populated_db)
|
||||
assert engine._has_assistant_calls_index is False
|
||||
assert "INDEXED BY" not in engine._GET_TOOL_CALLS_ALL
|
||||
tools_after = engine._get_tool_usage(0.0)
|
||||
assert sorted(t["tool_name"] for t in tools_after) == sorted(
|
||||
t["tool_name"] for t in tools_before
|
||||
)
|
||||
# And with the index present, the pin stays.
|
||||
assert "INDEXED BY" in InsightsEngine._GET_TOOL_CALLS_ALL
|
||||
|
||||
|
||||
# =========================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue