Coming from Alembic or hand-written scripts¶
From Alembic¶
MongoMig deliberately feels like Alembic. Most concepts map one to one:
| Alembic | MongoMig | Notes |
|---|---|---|
alembic init |
mongomig init |
|
alembic.ini |
mongomig.yaml |
plus mongomig.<env>.yaml overlays (--env) |
env.py + target_metadata = Base.metadata |
env.py + target_metadata = MongoMetadata.default() |
models register via @collection, register() or register_beanie() |
revision -m / --autogenerate |
revision -m / --autogenerate |
|
op.add_column, op.create_index, ... |
ctx.ops.backfill, ctx.ops.create_index, ... |
ctx.collection(name) for plain PyMongo |
upgrade head, downgrade -1 |
upgrade, downgrade (one step) or downgrade --steps N |
|
heads, history, current, merge, stamp |
same | |
alembic check |
mongomig diff --check (or validate) |
fails CI when models changed without a migration |
| (no built-in squash) | mongomig squash |
existing databases adopt the squash automatically |
alembic_version table |
__mongomig_migrations collection |
also stores status, checksum, who/where/commit |
--sql (offline mode) |
plan / --dry-run |
recorded against live data, with document estimates and risk |
What's different, because MongoDB is different:
- Autogenerate compares models with a committed snapshot, not with the database. SQL has a catalog to reflect; MongoDB only has documents, and a sample of them isn't a reliable schema. The snapshot makes diffs deterministic and reviewable.
- Data changes are first-class. Adding a column with a default is DDL in SQL; in MongoDB existing documents must be backfilled. Autogenerate writes those backfills, batched and re-runnable.
- No DDL transactions. A failed migration isn't rolled back automatically, so
ctx.opsoperations are idempotent and a re-run continues where it stopped. - Storage profiles. The stored BSON type depends on how the app serialises models
(
model_dump()vsjsonable_encoder), so you declare it. - Safety extras: a distributed lock, confirmation before destructive migrations,
restorable backups, checksums of applied files,
inspectfor the real data.
From hand-written scripts¶
Many MongoDB projects evolve their schema with ad-hoc scripts (scripts/fix_users.py,
update_many in a shell). That works until it doesn't:
| Hand-written scripts | MongoMig |
|---|---|
| "Was this script run on production?" | mongomig current: applied / pending / failed per environment |
| two people run it at once | distributed lock; the second runner waits or fails |
| script dies halfway through 4M documents | failure recorded; batched ctx.ops resume on re-run |
| someone edits a script after it ran | checksum mismatch stops the next upgrade |
| model changed, nobody wrote the backfill | mongomig validate fails CI |
| "how many documents will this touch?" | mongomig plan: estimates, collection scans, index-build failures |
| rollback is another ad-hoc script | downgrade, generated alongside upgrade; backups for deletions |
| indexes created by the app at startup | indexes versioned in migrations, created once, reviewed |
| "does production data still match the code?" | mongomig drift --check on a schedule |
| a long list of scripts every new environment replays | mongomig squash |
Adopting MongoMig on an existing database takes three commands: mongomig init, register
your models in env.py, then mongomig baseline. Nothing in the database changes. Future
changes go through mongomig diff and revision --autogenerate.
When MongoMig is not the right tool¶
- Schemaless by design. If documents are intentionally free-form, there's little to diff; plain versioned scripts may be enough (you still get tracking and locking by writing migrations by hand).
- Non-Python services. MongoMig reads Pydantic/Beanie models; other languages can still use hand-written migrations, but not autogenerate.
- Online resharding and cluster operations. Those belong to MongoDB's own tooling; MongoMig flags sharded collections but doesn't manage shard keys.