migrations: commit the plugin chains too, and bound what re-ran

Core's Alembic env got connection.commit() when the stamp bug was found; the
per-plugin template did not. MySQL commits DDL implicitly, which flushes
everything queued before it including the previous migration's version stamp,
and the LAST migration of a run has no DDL after it - so its stamp rolled back
at close while its schema change survived. flask plugin upgrade-all then exited
0 having silently re-run that migration, and re-ran it again on every deploy
after. Invisible for exactly as long as every plugin head happened to be
idempotent.

Two were not.

backups 0003 cleared lastseenat for EVERY row, which is correct once and
destroys evidence on each repeat. It is now scoped to the backfill's actual
signature, COALESCE(collectedat, createdat) - the expression 0002 wrote - plus a
date bound. Both conditions are needed. Matching on collectedat alone misses
every row whose collectedat is NULL, so precisely the rows carrying the most
invented value would have kept it forever; and value equality is not a signature
on MySQL, where db.DateTime is second-precision and the collector writes both
stamps in one statement, so a genuinely fresh revision would read as a backfill
and be wiped. SQLite keeps microseconds, which is why no test could show it.

geenforce 0003 added a column unconditionally, so it failed on a fresh database
built from the models and on any re-run. Guarded like network0003prefix.
This commit is contained in:
cproudlock
2026-08-14 13:46:23 -04:00
parent 0c574e0f49
commit 38deefe619
3 changed files with 58 additions and 1 deletions

View File

@@ -180,3 +180,15 @@ def run_migrations():
)
with context.begin_transaction():
context.run_migrations()
# SAME FIX AS migrations/env.py. MySQL DDL commits implicitly, which
# flushes everything queued before it - including the PREVIOUS
# migration's version stamp. The LAST migration of a run has no DDL
# after it, so its stamp is rolled back when the connection closes:
# the schema change survives, alembic_version stays one revision
# behind, and `flask plugin upgrade-all` exits 0 having silently
# re-run that migration - again on every deploy after.
#
# Core got this fix; the per-plugin chains run through THIS file and
# did not, so every plugin head has been re-running on MySQL. That is
# only invisible while every head happens to be idempotent.
connection.commit()