Skip to content

checksum drift detection and repeatable migrations - #35

Merged
jaemk merged 2 commits into
mainfrom
260815.checksum-drift-check
Aug 18, 2026
Merged

jaemk merged 2 commits into
mainfrom
260815.checksum-drift-check

Conversation

@jaemk

@jaemk jaemk commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Checksum drift detection and repeatable migrations

Two related features built on the checksum bookkeeping column: enforcing that an applied
migration's SQL has not changed, and its inverse, migrations that re-run when it does.

Drift detection

  • Verify that every already-applied migration still matches its recorded checksum before an up
    run applies anything, aborting with Error::ChecksumMismatch. The check runs even when
    nothing is pending, and skips rows where either side is null (programmatic or legacy rows).
  • Opt out with --allow-checksum-mismatch (Migrator::allow_checksum_mismatch), independent
    of the existing --allow-unknown-tags / --allow-out-of-order.

Repeatable migrations

  • Declare one with a -- migrant:repeatable directive in the up-SQL or the repeatable()
    builder method; Migratable::is_repeatable() reports it.
  • Re-run whenever the up-SQL checksum changes, instead of applying exactly once. Unchanged
    migrations are skipped.
  • Run them after all pending versioned migrations, in definition order, at most once per run.
  • Keep one bookkeeping row per tag, updated in place, so recorded application order is
    preserved.
  • Exempt them from the drift, unknown-tag, and out-of-order checks. The available migration set
    is authoritative on kind for a tag it still defines; the is_repeatable column decides only
    for tags no longer in the set.
  • Forward-only: a down run never selects them, and registering one with no checksum or with a
    down direction is an Error::Migration.
  • Add --rerun-repeatable (Migrator::rerun_repeatable) to re-run them regardless of
    checksum, since editing the file is otherwise the only trigger.
  • Warn from redo naming the repeatable migrations it will not revert, since it targets the
    most recent versioned migration instead.
  • Add migrant new --repeatable, report repeatable/stale from status and list, and add
    Report::repeatable_tags().

Breaking changes

  • __migrant_migrations gains an is_repeatable column. This folds into the existing
    unreleased bookkeeping schema change; see the CHANGELOG for the upgrade SQL.
  • down.sql is now optional for file-discovered migrations. A migration with no down file is a
    no-op in that direction.
  • NewMigration::down_path() returns Option<&Path>.

Coverage

Unit and integration tests for both features, run against sqlite plus postgres and mysql via
migrant_lib/test.sh. Specs in spec/checksum-drift-detection.md and
spec/repeatable-migrations.md; guide pages and both changelogs updated.

jaemk added 2 commits August 17, 2026 08:43
Drift detection: an up run verifies that every already-applied migration still
matches its recorded checksum before applying anything, aborting with
`Error::ChecksumMismatch`. Opt out with `Migrator::allow_checksum_mismatch` /
`--allow-checksum-mismatch`.

Repeatable migrations: a migration declared with `repeatable()` or a
`-- migrant:repeatable` directive re-runs whenever its up-SQL checksum changes
instead of applying once. They run after the pending versioned migrations, at
most once per run, keep a single bookkeeping row updated in place, and are
exempt from the drift, unknown-tag, and out-of-order checks. They are
forward-only: a down run never selects them, and declaring one with no checksum
or with a down direction is rejected at registration.
`Migrator::rerun_repeatable` / `--rerun-repeatable` re-runs them regardless of
checksum, and `redo` warns when it will not revert one.

The bookkeeping table gains an `is_repeatable` column, and `down.sql` is now
optional for file-discovered migrations. See the changelogs for the on-disk
schema upgrade note.
The test ran in the repo root against `db/migrant.db`, which persists between
runs. A change to the bookkeeping schema then breaks the next run against that
stale file, which CI never reproduces because it starts from a fresh checkout.
It now copies the repo's `Migrant.toml` and `migrations/` into a tempdir and
runs there, like the other CLI tests, so every run starts from an empty database
and nothing is written to the working tree.
@jaemk
jaemk merged commit a25b8ec into main Aug 18, 2026
5 checks passed
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