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:
kshitij 2026-08-03 16:50:33 +05:30 committed by kshitij
parent 7f1d84fe7f
commit 401e054d59
2 changed files with 49 additions and 13 deletions

View File

@ -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"

View File

@ -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
# =========================================================================