Skip to content

feat(db): maintain statistics rollups of query_logs - #280

Merged
henry40408 merged 1 commit into
mainfrom
feat/query-stats-rollups
Sep 13, 2026
Merged

henry40408 merged 1 commit into
mainfrom
feat/query-stats-rollups

Conversation

@henry40408

Copy link
Copy Markdown
Owner

Summary

Every dashboard and Statistics reading is a count, a sum or a histogram over a time window. With the default seven-day retention that window is the whole table, so answering one from query_logs reads an index entry per logged query, whichever index it uses. On a 1.48 M-row database the dashboard's summary alone is 9 475 pages, every tick.

This PR adds the data structure that fixes that, and keeps it correct. Nothing reads it yet. The dashboard and the Statistics page move onto it in follow-up PRs, and the indexes that only they use are dropped after that.

  • Migration 16 creates five WITHOUT ROWID rollup tables. In each, one row stands for every query that shares a key within a unit of time:
    • query_stats_quarter: blocked and cached, with count and summed response time per quarter hour.
    • query_stats_domain_hour, query_stats_client_hour, query_stats_upstream_hour, query_stats_metrics_hour: the grain the outcome, query-type and latency folds read.
  • Inserts: kept by an AFTER INSERT trigger, query_logs_maintain_stats, rather than by the logger, so rows written any other way are counted too. The e2e fixtures write theirs with the sqlite3 CLI.
  • prune_logs_before: calls unwind_stats_rollups in its transaction. Whole units before the cutoff are dropped. The quarter and hour the cutoff falls inside have the rows before the cutoff recounted and subtracted, so the prune keeps its exact cutoff instead of rounding retention to the hour.
  • Clear All: empties the rollups. This is deliberately not a DELETE trigger, which would unwind a prune row by row and turn off SQLite's truncate optimisation.
  • Backfill: the migration fills the rollups from existing rows with a replacing upsert, so an interrupted migration can run again.

Measurements

All numbers are cold page counts. The rollups were replayed through the sqlite3 CLI with the same trigger and unwind statements, over the real 370 677-row week and a 1 482 708-row, 31-day database built by replaying it, in the logger's 500-row batches.

no rollups trigger (this PR) one grouped upsert per batch
pages written, 370 677 rows 186 991 196 976 196 803
pages written, 1 482 708 rows 846 079 886 480 885 858

On both databases, the trigger and the per-batch upsert produced identical contents in all five tables (row-for-row hash match).

On the 1 482 708-row database:

  • One-day prune, cutoff inside a quarter and an hour: 5 151 pages written and 13 971 missed, against 5 020 and 12 282 without rollups. Afterwards both tables still agree with a recount.
  • Migration 16 fill: 101 420 pages read, 3 714 written.
  • Rollup size: 3 631 pages of a 111 282-page file.

Tests

  • rollups_follow_every_write_that_changes_query_logs: compares every rollup with its recount from query_logs after each of these steps:

    • two batches into the same units;
    • a row inserted with plain SQL;
    • a prune inside a quarter and an hour;
    • a prune that matches nothing;
    • a prune on an hour boundary;
    • Clear All, and a batch afterwards.

    Before the implementation it failed with no such table: query_stats_quarter. With unwind_stats_rollups removed from the prune it fails at query_stats_quarter disagrees with query_logs after a prune inside a quarter and an hour.

  • migration_v16_backfills_the_rollups: winds a populated database back to version 15, reopens it, and checks the fill, then checks that an insert afterwards is still counted. Before the implementation it failed with no such trigger: query_logs_maintain_stats.

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo nextest run: 683 passed, 8 skipped
  • CI e2e (the fixtures now fire the trigger)

🤖 Generated with Claude Code

Every dashboard and Statistics reading is a count, sum or histogram over
a window. With the default retention that window is the whole table,
so answering it from query_logs reads an index entry per logged query
whichever index it uses. Migration 16 adds five rollup tables in which
one row stands for every query sharing a key within a unit of time:
query_stats_quarter, and per hour domain, client, upstream and metrics.
Nothing reads them yet; the dashboard and the Statistics page move onto
them in follow-up changes.

- Inserts are maintained by an AFTER INSERT trigger, so rows written
  outside the logger (the e2e fixtures use the sqlite3 CLI) are counted
  too. Replaying 1.48 M queries in 500-row batches, the trigger writes
  886 480 pages, against 885 858 for one grouped upsert per batch and
  846 079 with no rollups.
- prune_logs_before unwinds the rollups in its transaction: whole units
  before the cutoff are dropped, and the rows before the cutoff in the
  quarter and hour it falls inside are recounted and subtracted, so the
  prune keeps its exact cutoff. A one-day prune writes 5 151 pages
  against 5 020.
- Clear All empties the rollups.
- The migration fills them from existing rows (101 420 pages read,
  3 714 written on the 1.48 M-row database) with a replacing upsert, so
  an interrupted migration can run again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.39572% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.30%. Comparing base (ea08a65) to head (04475db).

Files with missing lines Patch % Lines
src/db.rs 98.39% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #280      +/-   ##
==========================================
+ Coverage   91.16%   91.30%   +0.13%     
==========================================
  Files          31       31              
  Lines       11400    11587     +187     
==========================================
+ Hits        10393    10579     +186     
- Misses       1007     1008       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

1 participant