Skip to content

feat: add --apply flag to execute migrations directly - #11

Merged
leochong merged 1 commit into
mainfrom
feature/apply-flag
Aug 8, 2026
Merged

feat: add --apply flag to execute migrations directly#11
leochong merged 1 commit into
mainfrom
feature/apply-flag

Conversation

@leochong

@leochong leochong commented Aug 8, 2026

Copy link
Copy Markdown

Summary

  • Adds --apply, which executes the generated migration against dburl_from (the database being migrated) instead of only printing it — closing the gap called out in the README where MigraDiff only ever generated SQL and every user had to pipe it to psql themselves.
  • Execution runs inside a single transaction: any failing statement rolls back everything that ran before it, so nothing is ever partially applied.
  • On success, automatically records the migration in dburl_from's migradiff_history table — no need to also pass --record-history. On failure, nothing is recorded and the command exits with code 4, so migradiff_history only ever reflects migrations that were confirmed applied (resolves the "was this actually applied?" ambiguity noted in history.py's design and the README's known limitation).
  • Rejected up front, before anything runs, when combined with --from-file (there's no live database to apply to — the schema files load into ephemeral throwaway databases) or --promote (the chain's from/to direction isn't yet reconciled with which database --apply should execute against — tracked as a follow-up in PROJECT_PLAN.md rather than guessed at here).
  • --output json gets a new "apply" object with the outcome (applied, target, statement_count, error, history_recorded, history_error).

Design note: why dburl_from, not dburl_target

Traced this precisely rather than guessing: the README's own basic-usage example (psql postgres://db_production < migration.sql, where db_production is the first positional arg) and the existing Migration.apply() method both already establish that generated SQL is meant to be applied to dburl_from. Plain --record-history (unrelated, pre-existing behavior) records into dburl_target instead — that's left untouched; only --apply's own auto-recording targets dburl_from, since that's the database that was actually changed. Documented clearly in the README and CLAUDE.md so this doesn't cause confusion later.

Test plan

  • New tests/test_command_apply.py (11 tests, real Postgres, no mocks): happy path (table created + history recorded against dburl_from, not dburl_target), no-op on empty diff, safe-mode blocking (destructive changes never execute without --force-destructive), JSON output shape, both incompatible-flag rejections, and a direct atomicity test against _apply_migration() that forces a mid-migration failure and asserts full rollback + zero history recorded
  • Full suite: 353 passed, 2 skipped, no regressions
  • flake8 + black clean
  • Re-verified full suite against SQLAlchemy 2.0.51 in an isolated venv (learned this lesson the hard way in fix: wrap raw SQL in text() in test_history.py for SQLAlchemy 2.x #10) — no issues

🤖 Generated with Claude Code

Closes the gap where MigraDiff only ever generated SQL and left users
to pipe it to psql themselves, with no reliable way to know whether
migradiff_history reflected a migration that actually ran.

--apply executes the generated migration against dburl_from (the
database being migrated, matching the CLI's own help text and the
README's psql dburl_from < migration.sql convention) inside a single
transaction -- any failing statement rolls back everything, and
nothing is recorded, so migradiff_history only ever reflects migrations
that were confirmed applied. On success it auto-records history against
dburl_from without needing a separate --record-history call.

Rejected up front with --from-file (dburl_from would be an ephemeral
throwaway database) and --promote (the chain's from/to direction isn't
yet reconciled with which database --apply should execute against --
tracked as a follow-up in PROJECT_PLAN.md rather than guessed at here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@leochong
leochong merged commit a762dec into main Aug 8, 2026
13 of 14 checks passed
@leochong
leochong deleted the feature/apply-flag branch August 8, 2026 21:35
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