12. Apply, roll back, and reapply¶
A reviewed bundle still changes real rows. This page walks the execution lifecycle: preview, apply, roll back, and start a new epoch when you want the data back.
What you'll learn¶
- How to preview and apply a frozen data bundle, and when
--forceis required - What the execution journal records and which states it moves through
- How rollback follows the frozen rollback plan and refuses unsafe reversals
- Why an ordinary
migratewill not replay a rolled-back migration - How
migrate --reapply-datastarts a new linked epoch - How
--baselinerecords history without executing the plan
Prerequisites¶
- Page 11 completed: a generated bundle you have reviewed.
- The lifecycle example to follow along, or an equivalent project.
Step 1: Preview, then apply¶
migrate --dry-run --data inspects pending frozen bundles after version and
severity selection. It verifies and renders the bundle without writing:
DRY RUN - No changes applied
0001: acknowledgement required. hint: review the plan and pass --force
Preflight checks would abort this migration.
Dry-run summary: 1 versioned, 0 runs-always, 0 runs-on-change migrations would be applied.
The acknowledgement required line is the severity gate: data operations that
change rows are reported as WARN or CRITICAL, so --force is needed to
acknowledge them. Apply under the normal migration lock:
[APPLIED] Completed migration: primary__0001_… (version: 0001)
Migrations completed successfully: 1 migrations applied.
Use --force only for severity you have reviewed. It acknowledges operation risk;
it does not raise a severity ceiling or bypass drift.
Step 2: The execution journal¶
Execution records state in _dbwarden_data_runs and _dbwarden_data_events.
A run records the migration ID, application epoch, bundle checksum, run ID, status,
last completed operation, sanitized error, baseline flag, and timestamps.
Nontransactional backends also persist per-statement checkpoints.
Migration states are:
NOT_STARTED
APPLYING
FAILED_RETRYABLE
FAILED_FINAL
UNKNOWN_REQUIRES_RECONCILIATION
APPLIED_SUCCESS
ABANDONED
ROLLING_BACK
ROLLED_BACK
The happy path is NOT_STARTED → APPLYING → APPLIED_SUCCESS. A checksum-matched
repeat returns as already applied and performs no migration writes.
Step 3: Roll back¶
rollback uses the frozen rollback plan in the bundle; it never re-imports the
declarations and never invents reverse SQL. It refuses irreversible declarations
and stops when owned values have changed since apply:
A completed rollback records ROLLED_BACK. A capture transformation restores
the recorded typed preimage, but only while the current value still matches the
recorded postimage — later application edits cause a conflict instead of being
overwritten. Irreversible data operations refuse rollback outright; a comment or
empty SQL section is not a successful reversal.
Step 4: Reapply starts a new epoch¶
Ordinary migrate does not silently replay rolled-back data. Reapply is a
separate operator decision that creates a new epoch linked to the previous one:
--reapply-data explains the intent to reapply, but --force is still needed to
acknowledge WARN/CRITICAL data operations:
--force does not authorize reapply on its own, and --reapply-data does not
rerun an already successful migration. Successful migrations stay skipped.
Step 5: Baseline without executing¶
Baseline records history without executing the data plan. It requires an exact target version and stores an explicit acknowledgement, reason, and the skipped checks. It does not prove current data convergence:
Step 6: Walk the lifecycle example¶
The lifecycle example ends with a rollback/reapply/convergence script. It rolls back its two data migrations, inspects the restored rows, reapplies, and checks convergence:
=== 04: Rollback, reapply, converge ===
Rollback completed successfully: 2 migration(s) reverted.
restored item: [(1, 7, 3), (2, 10, 4), (3, 20, 5)]
restored country: []
Migrations completed successfully: 2 migrations applied.
reapplied item: [(1, 7, 8), (2, 10, 11), (3, 20, 21)]
country: [('UY', 'Uruguay')]
country_archive: [('AR', 'Argentina')]
Read the transitions in that output:
- The capture transformation restored
item.valueto its pre-write preimage(3, 4, 5)after rollback. - Rollback runs in reverse: it first rolls back the archive revision (restoring
ARfromcountry_archive), then rolls back the seed revision, which removes the rows it owned. That is whyrestored countryends up empty. - Reapply recomputed
value = source + 1and re-archivedAR.
The example README notes that reapplying re-registers the schema snapshot and may log a benign "Snapshot ID already registered" warning during replay; the data outcome is what matters.
Recap¶
migrate --dry-run --datapreviews;migrate --forceapplies when severity requires acknowledgement.- The journal records runs and events; states move through
APPLYING,APPLIED_SUCCESS,ROLLED_BACK, and the failure states. rollbackfollows the frozen plan and refuses irreversible or changed state.- Ordinary
migratenever replays rolled-back data;migrate --reapply-datastarts a new linked epoch and still needs--forceforWARN/CRITICAL. --baseline --to-versionrecords history without executing the plan.
What's next¶
When an apply fails with uncertain durable effects, you must reconcile before you retry. Continue with 13. Reconcile and convergence.