Gate restore on the dump's data revision, not the code head
A restore could load a database dump whose schema was older than the running code. The manifest stamped schemarev from the code image on disk, so the compatibility check compared the code against itself and always passed. A set captured from a database at 7d37_import_batches_and_journal restored under code at 7d41_drop_guest_tools_secrets, leaving the data behind the code: every query on a table the newer migrations touched failed with 1054 Unknown column, which surfaced as empty configuration-policy dropdowns and a failure to load data on edit PC. The authoritative signal is the alembic_version of the database that was dumped, so manifests now carry datarev alongside schemarev, backup.sh reads it from the database and warns when it has drifted from the image, and the gate asks whether the running code knows that revision. Data behind the code is allowed because it can be rolled forward; data ahead of the code is refused because it cannot. Rolling it forward has to happen somewhere with an app context. The backup sidecar is mysql plus restic plus alembic only and has no Flask, so restore.sh loads the dump and parks the run in an upgrading phase behind an .upgrade-requested sentinel rather than declaring a success it cannot verify. The api container owns the Flask CLI and already runs these exact idempotent commands on every deploy, so a finalize endpoint there runs db upgrade plus plugin upgrade-all and stamps the terminal phase. The UI drives it on seeing the upgrading phase, so recovery needs no operator intervention. The live simulation runs one codebase against itself, so its dump is always at the same revision as the code restoring it and it can never reproduce this drift. The gate therefore gets a standalone tester that builds throwaway migration chains and dumps offline, invoked as a simulation preflight. It caught a real bug in the legacy dump-parse fallback, where the revision was matched loosely enough to return the alembic_version table name instead of the value it holds.
This commit is contained in:
@@ -103,6 +103,17 @@ wait_for_phase() {
|
||||
${COMPOSE} exec -T db sh -c 'true' >/dev/null 2>&1 \
|
||||
|| die "db container not reachable - is the stack up?"
|
||||
|
||||
# --- 0b. Datarev-gate preflight (external tester) --------------------------
|
||||
# This live round trip runs one codebase against itself, so it can never
|
||||
# reproduce the "data revision behind the running code" case the restore gate
|
||||
# now handles. Run the standalone datarev tester first so a pass here also
|
||||
# covers that offline contract (manifest datarev, known-vs-unknown revision
|
||||
# refusal, legacy fallback, and the api upgrade handoff).
|
||||
log "running datarev gate preflight (test-restore-datarev.sh)"
|
||||
bash "${SCRIPT_DIR}/test-restore-datarev.sh" \
|
||||
|| die "datarev gate preflight failed - see output above"
|
||||
log "datarev gate preflight passed"
|
||||
|
||||
# --- 1. Backup the current state, then fingerprint the RESTORE TARGET ------
|
||||
# The restore point is the database as it existed WHEN THE DUMP RAN, not some
|
||||
# earlier moment. Live-churn tables (apitokens last-used, sessions, auditlogs)
|
||||
|
||||
Reference in New Issue
Block a user