Skip to content

Repair report, version, and branch indexes and remove orphaned rows - #922

Merged
epompeii merged 1 commit into
develfrom
reindex-report-version
Aug 23, 2026
Merged

Repair report, version, and branch indexes and remove orphaned rows#922
epompeii merged 1 commit into
develfrom
reindex-report-version

Conversation

@epompeii

@epompeii epompeii commented Jul 6, 2026

Copy link
Copy Markdown
Member

Migration: reindex_report_version

Long-lived databases can carry index corruption (wrong # of entries in index) that silently drops rows from index-driven queries, while the underlying table data remains intact. This migration rebuilds all indexes on the report, version, and branch tables. REINDEX is a no-op when the indexes are already consistent, so this is safe for every instance.

branch is new to the list since the first draft of this pull request. The corruption found in July reached the report indexes, sqlite_autoindex_version_1, and index_branch_head. index_branch_head hangs off the branch table, so neither REINDEX report nor REINDEX version ever touched it. Naming the table rather than the index rebuilds every index on it, including the implicit UNIQUE autoindexes: 9 indexes on report, 1 on version, 5 on branch. branch is a small table next to the ones it sits with, so the added cost is nil.

The migration also removes report_benchmark rows orphaned by deletes performed before ON DELETE CASCADE was enforced (see 2023-01-15-185835_perf_cascade), along with their dependent metric, boundary, and alert rows. Migrations run with foreign_keys = OFF, so cascades do not fire and the delete chain is explicit, bottom-up. The orphaned ids are collected into temp tables up front so that every delete works from one stable set, rather than each statement re-running the same multi-table join over tables an earlier delete has already changed.

Sequencing

This migration runs immediately before 2026-08-15-120000_benchmark_parameter. That migration rebuilds report_benchmark with a NOT NULL parameter_id filled through a LEFT JOIN on the backfilled parameter table, so a report_benchmark row whose benchmark no longer exists trips the NOT NULL and aborts the migration instead of being dropped silently. The rows this migration removes are exactly the rows that trip it. Diesel selects pending migrations as every embedded version not yet recorded, sorted ascending by version, so 2026-07-06-120000 runs first even on a database already past 2026-07-18-120000.

Validation

Validated against a production-shaped copy, taken copy-on-write so the source is never touched, with PRAGMA integrity_check clean before the run.

The copy carries a handful of orphaned report_benchmark rows, every one of them hanging off the same missing benchmark, each with one dependent metric row and no boundary or alert rows.

After the migration:

  • report_benchmark and metric each lose exactly those orphaned rows and nothing else
  • boundary, alert, benchmark, report, version, branch, and series_last_seen are unchanged, row for row
  • no orphaned report_benchmark rows remain
  • PRAGMA foreign_key_check returns no rows
  • PRAGMA integrity_check returns ok
  • index_branch_head is still present, and the temp tables leave nothing behind in the schema

The three benchmark-parameters migrations then apply on top of the repaired copy. 2026-08-15-120000_benchmark_parameter backfills one parameter row per benchmark, leaves no report_benchmark row with a null parameter_id, and holds report_benchmark at the repaired count.

Run against the same copy with this migration skipped, 2026-08-15-120000_benchmark_parameter instead fails with NOT NULL constraint failed: up_report_benchmark.parameter_id and rolls back. That abort is what this migration removes.

One derived value does go stale. metric_count_by_report caches a metric count per report and is not decremented here, so each affected report keeps a count one higher than the metrics it still has. That rollup feeds server stats rather than the ingest meter, and delete_report_results already accepts letting it drift. The precedent is not the same shape, though: there the drift is transient, since the rollup row cascades away when the report row is deleted, while these persist.

cargo nextest run -p bencher_schema --all-features passes 182 tests. Every one of them builds its database by running the full migration chain, so these statements are proven to apply under diesel inside a transaction.

Not in this pull request

An earlier version of this branch also removed stale Litestream files at API server startup: the generations/ state from Litestream 0.3 that 0.5+ no longer reads, and leftover <db>.tmp* files from an interrupted restore. That half is dropped by decision. Standing deletion code aimed at the data directory is a heavy way to reclaim dead bytes, and a variant that only detects and logs them may come back separately.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

ProjectBencher
Branchreindex-report-version
Testbedintel-v1
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
4.61 µs
(-1.61%)Baseline: 4.69 µs
4.99 µs
(92.48%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
4.54 µs
(-0.36%)Baseline: 4.55 µs
4.81 µs
(94.23%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
26.15 µs
(+1.25%)Baseline: 25.83 µs
27.11 µs
(96.46%)
Adapter::Rust📈 view plot
🚷 view threshold
3.45 µs
(-2.29%)Baseline: 3.53 µs
3.93 µs
(87.68%)
Adapter::RustBench📈 view plot
🚷 view threshold
3.45 µs
(-2.29%)Baseline: 3.53 µs
3.94 µs
(87.62%)
🐰 View full continuous benchmarking report in Bencher

@epompeii
epompeii force-pushed the reindex-report-version branch from 3acb9da to 95e0998 Compare August 23, 2026 00:10
@epompeii epompeii changed the title Repair report and version indexes and clean up stale Litestream files Repair report, version, and branch indexes and remove orphaned rows Aug 23, 2026
Add a migration that rebuilds all indexes on the report, version, and
branch tables. Long-lived databases can carry index corruption (wrong
number of entries in an index) that silently drops rows from
index-driven queries. The corruption found in July reached the report
indexes, the version unique index, and index_branch_head, so all three
tables are rebuilt. REINDEX is a no-op when the indexes are already
consistent, so this is safe for every instance.

The migration also removes report_benchmark rows (and their dependent
metric, boundary, and alert rows) orphaned by deletes performed before
ON DELETE CASCADE was enforced (see 2023-01-15-185835_perf_cascade).
Migrations run with foreign_keys = OFF, so the delete chain is explicit
and bottom-up, with the orphaned ids collected into temp tables up front
so every delete works from one stable set.

The Litestream startup cleanup this branch used to carry is dropped by
decision. Standing deletion code in the data directory is not something
to add here; a detect-and-log variant may return separately.
@epompeii
epompeii force-pushed the reindex-report-version branch from 95e0998 to 2dadf83 Compare August 23, 2026 01:01
@epompeii
epompeii marked this pull request as ready for review August 23, 2026 01:43
@epompeii
epompeii merged commit 53c02d0 into devel Aug 23, 2026
52 checks passed
@epompeii
epompeii deleted the reindex-report-version branch August 23, 2026 02:12
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