Skip to content

fix(history): persist query_history rows by encoding result_json as jsonb#46

Merged
AttiR merged 1 commit into
mainfrom
bug/45-query-history-is-never-persisted-user-counter-increments-but-history-panel-stays-empty
May 19, 2026
Merged

fix(history): persist query_history rows by encoding result_json as jsonb#46
AttiR merged 1 commit into
mainfrom
bug/45-query-history-is-never-persisted-user-counter-increments-but-history-panel-stays-empty

Conversation

@AttiR

@AttiR AttiR commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes a silent data-loss bug where every query's row in query_history was dropped, while the per-user rate-limit counter still incremented. Users saw their budget badge decrement to 1 of 2 remaining but the History panel stayed empty and cached replays were impossible.

Root cause

_log_query_history in backend/app/agent/sql_agent.py passed a Python dict as the bind value for the result_json JSONB column. asyncpg has no built-in dict → jsonb encoder, so the INSERT raised DataError. The error was caught by a blanket except Exception: logger.exception("... non-fatal") block and never surfaced, so the regression was invisible in logs that weren't tailed carefully.
The earlier UPDATE users SET total_queries = ... (no JSON) committed normally — hence the asymmetry between budget and history.

Changes

  • backend/app/agent/sql_agent.py
    • Serialise the result cache with json.dumps({"rows": ..., "columns": ...}, default=str).
    • Add explicit $4::jsonb cast in the INSERT.
    • Keep audit-log write best-effort (a successful query must not 500 because the audit log failed) but log structured context (user_id, row_count) so future regressions are visible in Azure Container Apps logs.
  • tests/test_sql_agent.py — two new regression tests under TestLogQueryHistoryJsonbEncoding:
    • test_passes_json_string_and_jsonb_cast — asserts the bind value is a str and the SQL contains $4::jsonb.
    • test_db_failure_is_swallowed_not_propagated — verifies that a transient DB error during the audit-log write does not propagate to the user.
  • tests/conftest.pysetdefault JWT_SECRET_KEY and GOOGLE_CLIENT_ID so unit tests load without a fully populated .env.

Test plan

  • pytest tests/test_sql_agent.py -v — 27 passed (25 existing + 2 new)
  • Manual: sign in to production, run one query, confirm the row appears in query_history and the History panel shows it
  • Manual: click the history item, confirm result loads via GET /api/history/:id without a Claude call (from_cache: true)
  • Manual: confirm users.total_queries matches COUNT(*) FROM query_history WHERE user_id = me after the fix

Risk

Low — change is scoped to one function and adds an explicit cast that PostgreSQL accepts unconditionally. Existing history rows are unaffected; new rows will be inserted correctly.

Rollback

Revert this commit. The previous behaviour was data-loss but not a crash, so rollback is safe at any time.

Closes #45

…sonb

asyncpg cannot encode a Python dict into a JSONB column, so the
INSERT into query_history raised DataError on every successful query.
The blanket "non-fatal" except block hid the failure, but the
rate-limit counter on users.total_queries had already committed —
producing the user-facing bug where the budget badge decremented
("1 of 2 remaining") yet the History panel stayed empty.
Serialise the result cache with json.dumps(..., default=str) and add
an explicit ::jsonb cast on the bind parameter. Keep the audit-log
write best-effort (a successful query should not 500 because the
audit log failed) but log structured context so a regression is
visible in Azure logs.
Adds two regression tests:
- asserts result_json is bound as a string and SQL contains $4::jsonb
- asserts a transient DB failure does not propagate to the user
tests/conftest.py: setdefault JWT_SECRET_KEY and GOOGLE_CLIENT_ID so
unit tests load without a fully populated .env.
@AttiR
AttiR merged commit 60a75b0 into main May 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Query history is never persisted — user counter increments but History panel stays empty

1 participant