Reporter IndividualGhost1905 upgraded to 0.2.4.1 (which shipped the
per-event aggregation rewrite from #1378) and saw Quick Stats split
between consistent values (Total Prints, Print Time, Filament Used,
Energy, Success Rate matched the archive list) and zero-or-empty ones
(Filament Cost, Time Accuracy).
Root cause: #1378's migration added six columns to print_log_entries
- archive_id, cost, energy_kwh, energy_cost, failure_reason,
created_by_id - but never backfilled them. Pre-upgrade rows kept NULL
on all six. The new Quick Stats query sums PrintLogEntry.cost (gets 0
on legacy data); the time-accuracy query JOINs PrintArchive ON
archive_id (drops every legacy run from the average). Counts and the
pre-existing per-row fields (status, duration_seconds,
filament_used_grams) kept working - which is why some panels looked
right and others didn't.
Two-step backfill added inside run_migrations next to the existing
column-add block, as DML inside begin_nested() (not _safe_execute,
which is documented DDL-only):
Step 1: link each orphan log entry to its archive via
print_name + printer_id (highest archive id wins on
tiebreak - newest matches the overwrite-then-stop shape
pre-#1378 reprints left behind).
Step 2: copy archive.cost / energy_kwh / energy_cost onto the
latest matching log entry per archive, BUT only for
archives where no log entry yet carries a cost. That
second clause is the idempotency anchor and the
double-count guard for users running this after #1378
has already written cost-bearing rows for new runs -
those archives are left untouched.
Earlier reprints stay NULL, matching the "first/latest writes, rest
stay NULL" convention #1378 introduced for new prints. Sum across the
legacy reprint chain reproduces sum-of-archive-cost exactly, so Quick
Stats Filament Cost matches the pre-upgrade total instead of dropping
to zero.
SQL is plain ANSI - correlated UPDATE with LIMIT 1 in the SET
subquery, WHERE id IN (SELECT MAX(id) ... GROUP BY archive_id HAVING
SUM(CASE WHEN cost IS NOT NULL THEN 1 ELSE 0 END) = 0). Verified
end-to-end on SQLite (4 new unit tests in
test_print_log_backfill_migration.py: link-via-name, latest-run-gets-
cost, idempotent, skip-archives-with-any-costed-run) and against a
live postgres:16-alpine + asyncpg container (first-pass and second-
pass produce identical state).
The other widgets the reporter listed (Printer Stats, Filament
Trends, By Material, Success by Material, Color Distribution) iterate
the archives list on the frontend rather than calling /stats - they
read consistent pre-upgrade data and aren't part of this fix; the
inconsistency between them and Quick Stats resolves once the backfill
brings Quick Stats in line.