Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

## [Unreleased]
### Added
- An up run verifies that each already-applied migration still matches its recorded checksum
and aborts with `ChecksumMismatch` if one changed since it was applied. `apply` and `redo`
accept `--allow-checksum-mismatch` (library: `Migrator::allow_checksum_mismatch`) to apply
despite the drift. Programmatic migrations and rows with a null recorded checksum are not
checked
- Repeatable migrations: a migration whose up-SQL carries the `-- migrant:repeatable` directive
re-runs whenever its checksum changes, instead of applying exactly once. They run after all
pending versioned migrations, keep a single bookkeeping row updated in place, and are never
reverted by `apply --down` or `redo`. `new --repeatable <tag>` creates one (an `up.sql` seeded
with the directive, and no `down.sql`)
- `apply` and `redo` accept `--rerun-repeatable` to re-run every repeatable migration even if
its SQL has not changed, since editing the file is otherwise the only trigger
- `redo` prints a note naming the applied repeatable migrations it will not revert, since it
targets the most recent versioned migration instead
- `status` reports whether each migration is repeatable and whether it is stale (due to re-run),
with a `stale` summary count. Text output annotates repeatable rows and marks stale ones `[~]`;
the JSON rows gain `repeatable` and `stale` fields
- `apply` accepts `--step N` to apply exactly N migrations, in either direction
- `apply` and `redo` accept `--allow-unknown-tags` to permit a run when the database has an
applied tag not present in the defined migration set, and `--allow-out-of-order` to permit
Expand All @@ -12,11 +29,14 @@
- `apply` applies all pending migrations by default, instead of just the next one. Use
`--step 1`, or `--down` (which remains single-step by default), to move one migration at a
time
- A migration directory no longer needs a `down.sql`. A migration with no down file is a no-op
in the down direction: reverting it removes its bookkeeping row without running SQL
- The `__migrant_migrations` bookkeeping table is now multi-column (`id`, `tag`, `checksum`,
`applied_at`) instead of a single `tag` column. Each applied migration now records a sha256
checksum of its up-SQL (null for programmatic migrations) and an applied-at timestamp, and
applied order is tracked by `id` rather than inferred from file/tag order. `redo` and
`apply --down` now target the most-recently-applied migration by this recorded order.
`applied_at`, `is_repeatable`) instead of a single `tag` column. Each applied migration now
records a sha256 checksum of its up-SQL (null for programmatic migrations), an applied-at
timestamp, and whether it is repeatable, and applied order is tracked by `id` rather than
inferred from file/tag order. `redo` and `apply --down` now target the most-recently-applied
migration by this recorded order.

This is a one-time breaking change to the on-disk schema. If you don't need to preserve
applied history, the simplest upgrade is to drop the existing `__migrant_migrations` table
Expand All @@ -28,16 +48,19 @@
alter table __migrant_migrations add column id bigserial;
alter table __migrant_migrations add column checksum text;
alter table __migrant_migrations add column applied_at timestamptz not null default now();
alter table __migrant_migrations add column is_repeatable boolean not null default false;

-- sqlite
alter table __migrant_migrations add column id integer;
alter table __migrant_migrations add column checksum text;
alter table __migrant_migrations add column applied_at text not null default (datetime('now'));
alter table __migrant_migrations add column is_repeatable boolean not null default false;

-- mysql
alter table __migrant_migrations add column id bigint unsigned auto_increment unique;
alter table __migrant_migrations add column checksum text;
alter table __migrant_migrations add column applied_at timestamp not null default current_timestamp;
alter table __migrant_migrations add column is_repeatable boolean not null default false;
```

Since applied order was not previously tracked, `id` will not necessarily reflect the
Expand Down
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ The `postgres` feature needs `libpq-dev` at build time on linux.

## Running Tests

The CLI integration tests use the repo's `Migrant.toml` (sqlite) and `migrations/` directory:

```bash
cargo test --features sqlite,integration_tests
```

The CLI integration tests copy the repo's `Migrant.toml` (sqlite) and `migrations/` directory
into a tempdir and run there, so each run starts from an empty database and nothing is written
to your working tree. There is no dev database to set up or reset.

`db/migrant.db` is only created if you run `migrant` against the repo yourself, and is
gitignored. It is a scratch playground: delete it any time, and `migrant setup` recreates it.
Nothing in the test suite reads it, so a stale one cannot break a run.

Library tests live in `migrant_lib/` (see its CONTRIBUTING for postgres/mysql setup).


Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ When run interactively (without `--no-confirm`), `setup` will be run automatical

`migrant setup` - Verify database info/credentials and setup a `__migrant_migrations` table if missing.

`migrant new <tag>` - Generate new up & down files with the given `<tag>` under the specified `migration_location`.
`migrant new <tag> [--repeatable]` - Generate new up & down files with the given `<tag>` under the specified `migration_location`. `--repeatable` generates only an `up.sql`, carrying the `-- migrant:repeatable` directive, for a migration that re-runs whenever its SQL changes.

`migrant edit <tag> [--down]` - Edit the `up` [or `down`] migration file with the given `<tag>`.

`migrant list` - Display all available .sql files and mark those applied.

`migrant status [--format <text|json>]` - Report every managed migration's applied/pending state with summary counts, as pretty text (default) or JSON.
`migrant status [--format <text|json>]` - Report every managed migration's applied/pending state with summary counts, as pretty text (default) or JSON. Repeatable migrations are annotated, and marked stale when due to re-run.

`migrant apply [--down, --step N, --force, --fake, --no-sync]` - Apply all pending migrations. `--down` reverses direction and applies a single step by default; `--step N` applies exactly N steps in either direction.
`migrant apply [--down, --step N, --force, --fake, --no-sync, --rerun-repeatable, --allow-unknown-tags, --allow-out-of-order, --allow-checksum-mismatch]` - Apply all pending migrations. `--down` reverses direction and applies a single step by default; `--step N` applies exactly N steps in either direction. `--rerun-repeatable` re-runs every repeatable migration even if its SQL is unchanged. The `--allow-*` flags each bypass one otherwise-fatal consistency check.

`migrant redo [--all, --force, --no-sync]` - Re-apply the latest migration (down then up).
`migrant redo [--all, --force, --no-sync, --rerun-repeatable]` - Re-apply the latest migration (down then up). Repeatable migrations are forward-only, so `redo` does not revert them and says so.

`migrant tui` - Open an interactive terminal UI for viewing and applying migrations.

Expand Down
56 changes: 36 additions & 20 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ run from anywhere inside the project; migrant searches upward for the config.

## Migrations

`migrant new <tag>`
`migrant new <tag> [--repeatable]`
: Generate a timestamped `<stamp>_<tag>/` directory with empty `up.sql` and
`down.sql`. Tags may contain `[a-z0-9-]`.
`down.sql`. Tags may contain `[a-z0-9-]`. `--repeatable` instead writes only an
`up.sql`, seeded with the `-- migrant:repeatable` directive, for a migration
that re-runs whenever its SQL changes (see
[Writing migrations](migrations.md)).

`migrant edit <tag> [--down]`
: Open the `up.sql` (or `down.sql` with `--down`) for a migration matching
Expand All @@ -38,25 +41,38 @@ run from anywhere inside the project; migrant searches upward for the config.

`migrant status [--format <text|json>]`
: Report every managed migration with its applied/pending state and summary
counts. `--format text` (the default) prints a summary line plus a `[✓]`/`[ ]`
row per migration; `--format json` prints the same data as JSON
(`{ total, applied, pending, migrations: [{ tag, applied }] }`) for scripting.

`migrant apply [--down] [--all] [--force[=<mode>]] [--fake] [--no-sync]`
: Apply the next migration. `--down` reverts instead of applying. `--all` runs
every remaining migration in the chosen direction. `--force` continues past a
failed migration: bare `--force` (or `--force=accept-failures`) records the
failed migration as applied anyway, so it is not retried on later runs;
`--force=skip-failures` leaves it unrecorded and retries it on the next run.
`--fake` records the migration as (un)applied without running its SQL.
`--no-sync` disables the cross-process advisory lock that is otherwise on by
default for PostgreSQL/MySQL; use it when migrations are already serialized
by an external mechanism.

`migrant redo [--all] [--force[=<mode>]] [--no-sync]`
counts. `--format text` (the default) prints a summary line plus a
`[✓]`/`[ ]`/`[~]` row per migration; `--format json` prints the same data as
JSON
(`{ total, applied, pending, stale, migrations: [{ tag, applied, repeatable, stale }] }`)
for scripting. A repeatable migration is annotated, and marked `[~]` when it is
due to re-run. The summary `stale` count covers applied migrations that will
re-run; migrations with no row yet are counted in `pending`.

`migrant apply [--down] [--step <N>] [--force[=<mode>]] [--fake] [--no-sync] [--rerun-repeatable] [--allow-unknown-tags] [--allow-out-of-order] [--allow-checksum-mismatch]`
: Apply every pending migration. `--down` reverts instead of applying, and
defaults to a single migration. `--step N` limits either direction to N.
`--force` continues past a failed migration: bare `--force` (or
`--force=accept-failures`) records the failed migration as applied anyway, so
it is not retried on later runs; `--force=skip-failures` leaves it unrecorded
and retries it on the next run. `--fake` records the migration as (un)applied
without running its SQL. `--no-sync` disables the cross-process advisory lock
that is otherwise on by default for PostgreSQL/MySQL; use it when migrations
are already serialized by an external mechanism. `--rerun-repeatable` re-runs
every repeatable migration even if its SQL is unchanged. The three `--allow-*` flags
each bypass one otherwise-fatal consistency check: an applied tag missing from
the migration set, a migration applied out of order, and an already-applied
migration whose SQL has changed since it was recorded.

`migrant redo [--all] [--force[=<mode>]] [--no-sync] [--rerun-repeatable] [--allow-unknown-tags] [--allow-out-of-order] [--allow-checksum-mismatch]`
: Shortcut for the latest `down` then `up`. Useful while iterating on a migration
you are still writing. `--no-sync` disables the advisory lock for both the
down and up runs.
you are still writing. `--all` redoes every applied migration. `--no-sync`
disables the advisory lock for both the down and up runs. Repeatable
migrations are forward-only, so the down phase skips them and targets the most
recent versioned migration instead; the up phase then re-runs a repeatable
migration only if its SQL changed, or unconditionally with
`--rerun-repeatable`. `redo` prints a note naming the repeatable migrations it
will not revert.

## Inspect and connect

Expand Down
29 changes: 29 additions & 0 deletions docs/src/migration-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ FnMigration::with_tag("seed-users")
# }
```

## Repeatable migrations

`Migratable::is_repeatable()` marks a migration that re-runs whenever its up-SQL
checksum changes, instead of applying exactly once. The default is `false`.

`FileMigration` and `EmbeddedMigration` declare it with the `repeatable()`
builder method, or with a `-- migrant:repeatable` directive in the up-SQL (the
form the CLI reads off disk). Either declares it.

```rust
use migrant_lib::EmbeddedMigration;

# fn run() {
EmbeddedMigration::with_tag("seed-roles")
.repeatable()
.up("insert into roles (name) values ('admin') on conflict do nothing;")
.boxed();
# }
```

A repeatable migration must have up-SQL to hash and must not define a down
direction; `use_migrations` rejects either with `Error::Migration`. `FnMigration`
has no SQL to hash, so it cannot be repeatable.

Within a run they apply after every pending versioned migration, at most once
each. `Report::repeatable_tags()` lists the ones a run re-ran, and
`MigrationStatus::repeatable()`/`stale()` report the state of each. See
[Writing migrations](migrations.md) for the full rules.

## Transactions per migration

`Migratable::use_transaction(direction)` decides whether migrant wraps a
Expand Down
54 changes: 50 additions & 4 deletions docs/src/migrations.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Writing migrations

A CLI migration is a directory holding an `up.sql` and a `down.sql`, named with a
timestamp and a tag:
A CLI migration is a directory holding an `up.sql` and, optionally, a
`down.sql`, named with a timestamp and a tag:

```
migrations/
Expand All @@ -22,6 +22,9 @@ way up, newest-first on the way down. Tags may contain `[a-z0-9-]`.
`up.sql` moves the schema forward; `down.sql` reverses it. Keep them inverses so
`apply --down` cleanly undoes `apply`.

`down.sql` is optional. A migration with no down file is a no-op in the down
direction: reverting it removes its tracking-table row without running any SQL.

```sql
-- 20260714101500_add-users-email/up.sql
alter table users add column email text;
Expand All @@ -47,15 +50,58 @@ Current Migration Status:
-> [ ] 20260714101500_add-users-email
```

`apply` runs the next unapplied migration in timestamp order; `apply --all` runs
the rest. `apply --down` reverts the most recently applied one.
`apply` runs every pending migration in timestamp order; `apply --step N` limits
a run to N. `apply --down` reverts the most recently applied one.

## Editing and iterating

- `migrant edit <tag>` opens `up.sql` in `$EDITOR`; add `--down` for `down.sql`.
- `migrant redo` re-runs the latest migration (down then up) so you can iterate
on SQL you are still writing.

Editing a migration that has already been applied is drift: the next run aborts
with a checksum mismatch rather than silently building on changed SQL. Either
revert the edit and write a new migration, or pass `--allow-checksum-mismatch`.
Repeatable migrations invert this, see below.

## Repeatable migrations

A repeatable migration re-runs whenever its `up.sql` changes, instead of applying
exactly once. Use them for idempotent data work (seeding, backfills, refreshing
views) rather than schema versioning.

`migrant new --repeatable <tag>` creates one: an `up.sql` carrying the directive,
and no `down.sql`.

```sql
-- migrant:repeatable
insert into roles (name) values ('admin') on conflict do nothing;
```

The rules:

- They run after every pending versioned migration in a run, in timestamp order
among themselves, and at most once per run.
- A checksum change is the signal to re-run, not drift, so editing the file is
how you make it run again. An unchanged file is skipped.
- They keep one row in the tracking table, updated in place.
- They are forward-only: they must not have a `down.sql`, and `apply --down`
never reverts them. `redo` reverts and re-applies the most recent *versioned*
migration, which may not be the one you just edited; its up phase then re-runs
a repeatable migration only if its SQL changed, like any other run. `redo`
prints a note when it skips one.
- To run one whose SQL has not changed, pass `--rerun-repeatable` to `apply` or
`redo`. It re-runs every repeatable migration, still after the versioned ones
and still at most once per run.

`migrant status` marks one that is due to re-run:

```
Migration status: 2 applied, 0 pending, 1 stale (2 total)
[✓] 20260713094500_create-roles
[~] 20260714101500_seed-roles (repeatable, will re-run)
```

## Non-transactional DDL

Some statements cannot run inside a transaction (for example PostgreSQL
Expand Down
28 changes: 27 additions & 1 deletion migrant_lib/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,36 @@
has an applied tag not present in the defined migration set, instead of erroring
- `Migrator::allow_out_of_order(bool)` (default `false`) lets a run apply migrations out of
their defined order, instead of erroring
- `Migrator::allow_checksum_mismatch(bool)` (default `false`) lets a run proceed when an
already-applied migration's current checksum no longer matches the recorded one, instead of
erroring with the new `Error::ChecksumMismatch`
- `Error::ChecksumMismatch` and its `is_checksum_mismatch()` predicate
- Repeatable migrations: `Migratable::is_repeatable()` (default `false`) marks a migration that
re-runs whenever its up-SQL checksum changes instead of applying once. `EmbeddedMigration` and
`FileMigration` declare it with a `repeatable()` builder method or a `-- migrant:repeatable`
directive in their up-SQL (either form declares it). Repeatable migrations run after all
pending versioned ones, are exempt from the unknown-tag, ordering, and drift checks, keep a
single bookkeeping row updated in place, and are never selected by a `Down` run. Registering
one with no checksum, or with a down direction, is an `Error::Migration`
- `Migratable::defines_down()` (default `false`) reports whether a migration has a down direction
to run, used to reject a down on a repeatable migration
- `Migrator::rerun_repeatable(bool)` (default `false`) re-runs every repeatable migration on an
`Up` run regardless of checksum, so re-running an unedited one does not require touching its
SQL. Still at most once per run, and it never re-applies a versioned migration
- `Report::repeatable_tags()` lists the repeatable tags a run re-ran, a subset of `tags()`
- `MigrationStatus::repeatable()` and `MigrationStatus::stale()` report whether a migration is
repeatable and whether it will run on the next `Up` run
- `create_repeatable_migration` creates a migration directory with only an `up.sql`, seeded with
the `-- migrant:repeatable` directive

### Changed
- `migrant_lib::new` is renamed to `migrant_lib::create_migration` and now returns a
`NewMigration` (with `dir()`/`up_path()`/`down_path()` accessors) instead of `()`
`NewMigration` (with `dir()`/`up_path()`/`down_path()` accessors) instead of `()`.
`NewMigration::down_path()` returns `Option<&Path>`, since a repeatable migration has none
- A file-discovered migration no longer requires a `down.sql`. A migration with no down file
reports `defines_down() == false` and is a no-op in the down direction
- `pending_migrations` now lists stale repeatable tags after the pending versioned ones, in the
order a run would apply them
- `migrant_lib::list` moved to `migrant_lib::cli::list`
- `SqliteSettingsBuilder`/`PostgresSettingsBuilder`/`MySqlSettingsBuilder` setters
`database_path`/`migration_location` are now infallible: they take and return `Self` instead
Expand Down
Loading