migrify
Database migrations for Python: the autogenerate power of Alembic, the linear simplicity of Laravel.
Philosophy
Alembic is powerful but complex — branching, DAG graphs, multiple heads, down_revision chains.
Laravel migrations are simple — linear order by filename, a migrations table with a batch number, up() / down().
migrify takes the best of both:
| Feature | Source |
|---|---|
| Autogenerate from SQLAlchemy models | Alembic |
Full DDL operations API (op.*) |
Alembic |
| Multi-dialect support (PG, MySQL, SQLite...) | Alembic |
| Linear ordering by timestamp filename | Laravel |
migrations (id, migration, batch) tracking table |
Laravel |
| Batch-based rollback | Laravel |
Simple migrate / rollback / fresh CLI |
Laravel |
Installation
pip install migrify
Quickstart
1. Initialise (creates migrations/ and a ready-to-edit migrify.toml):
migrify init
Then open migrify.toml and set your db_url (use a sync driver):
db_url = "postgresql+psycopg2://user:pass@localhost/mydb"
migrations_dir = "migrations"
# models_module = "myapp.models" # for autogenerate
Note: migrify is sync-only. If your app uses an async driver (e.g.
asyncpg), migrify will auto-switch to the sync equivalent and print a warning.
2. Create a migration:
# Empty migration
migrify make create_users_table
# With autogenerate (compares your SQLAlchemy models with DB)
migrify make --autogenerate add_phone_to_users
Autogenerate requirement: the module set in
models_modulemust expose ametadataattribute of typesqlalchemy.MetaData. With declarative models, add one line to your models package:# myapp/models/__init__.py from .base import Base # your DeclarativeBase # ... other imports ... metadata = Base.metadata # ← required for --autogenerateThe attribute name can be changed via
models_metadata_attrin the config.
3. Edit the migration file:
# migrations/2024_01_15_143022_create_users_table.py
import sqlalchemy as sa
from migrify import op
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("email", sa.String(255), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
)
def downgrade() -> None:
op.drop_table("users")
4. Run migrations:
migrify migrate # apply all pending
migrify status # see what's applied
migrify rollback # undo last batch
migrify fresh # drop everything and re-migrate
CLI Reference
| Command | Description |
|---|---|
migrify init |
Scaffold migrations/ dir and migrify.toml template |
migrify migrate |
Apply all pending migrations |
migrify migrate --step |
Apply pending, each in its own batch |
migrify migrate --pretend |
Show SQL without executing |
migrify rollback |
Rollback last batch |
migrify rollback --batch 3 |
Rollback last 3 batches |
migrify reset |
Rollback all migrations |
migrify fresh |
Drop all tables + migrate |
migrify status |
Show migration status |
migrify make <name> |
Create empty migration |
migrify make --autogenerate <name> |
Create migration from model diff |
Configuration Reference
All options go in migrify.toml (or [tool.migrify] in pyproject.toml).
# ── Required ────────────────────────────────────────────────────────────────
# Database URL. Must use a sync driver.
# asyncpg / aiosqlite / aiomysql are auto-swapped to their sync equivalents.
db_url = "postgresql+psycopg2://user:pass@localhost/mydb"
# ── Optional ────────────────────────────────────────────────────────────────
# Directory where migration files are stored. Default: "migrations"
migrations_dir = "migrations"
# Name of the migrations tracking table. Default: "migrations"
migrations_table = "migrations"
# ── Autogenerate ─────────────────────────────────────────────────────────────
# Dotted Python path to the module exposing your SQLAlchemy MetaData.
# Required for `migrify make --autogenerate`.
models_module = "myapp.models"
# Attribute name of the MetaData inside models_module. Default: "metadata"
models_metadata_attr = "metadata"
# Whether to detect column type changes. Default: true
# Set to false if you have intentional model/DB type mismatches
# (e.g. Enum in model vs VARCHAR in DB — common when native_enum=False).
compare_types = true
# Tables to skip entirely during autogenerate comparison.
# Supports fnmatch patterns (*, ?, [seq]).
# Use for partition tables, legacy tables, or tables managed outside models.
exclude_tables = [
"history_changes_p*", # time-based partitions
"history_changes_default",
"alembic_version", # if migrating away from Alembic
]
# Index names to skip during autogenerate comparison.
# Supports fnmatch patterns.
# Use for manually-created indexes (GIN, trigram, etc.) not defined in models.
# Tip: once you add an index to __table_args__ in your model, remove it here.
exclude_indexes = [
"idx_products_*",
"idx_groups_*",
]
# Columns to skip when detecting dropped columns. Format: "table.column".
# Use for columns that exist in DB but are not mapped in models
# (e.g. computed columns accessed via @property, or legacy columns).
exclude_columns = [
"orders.extra_fee",
]
Autogenerate behaviour
migrify compares your SQLAlchemy models against the live database and generates the minimal set of DDL operations needed to bring the DB in sync with the models.
| What is compared | Default | Notes |
|---|---|---|
| Missing / extra tables | always | controlled by exclude_tables |
| Missing / extra columns | always | controlled by exclude_columns |
| Column type changes | compare_types = true |
disable for Enum↔VARCHAR mismatches |
| Column nullability | always | |
| Missing / extra indexes | always | controlled by exclude_indexes |
| Missing / extra foreign keys | always | matched by column signature, not name |
| Missing / extra unique constraints | always |
Indexes backed by UniqueConstraint or unique=True are automatically
recognised and never generate spurious drop_index operations.
Functional indexes (e.g. GIN, tsvector) declared via text() in
__table_args__ are rendered correctly with sa.literal_column(...).
Tracking Table
Unlike Alembic's alembic_version (which can have multiple rows for branches),
migrify uses a simple, always-linear migrations table:
CREATE TABLE migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration VARCHAR(255) NOT NULL, -- filename without .py
batch INTEGER NOT NULL -- group applied together
);
License
MIT
Release files for migrify 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| migrify-0.1.2.tar.gz | 35.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| migrify-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.5 kB
Release files / migrify-0.1.2.tar.gz
| Download URL | migrify-0.1.2.tar.gz |
|---|---|
| Size | 35.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b62e946003ede92e0592bf43f0315fd3dfc5a1494ff95da1e921f19f2fa6ca8e
|
|
BLAKE2b-256 checksum How to use checksums |
f2007c2dc5dcdfb9d4754f73c69a7fb7e2316f8da2d71da9cb978fe1c3a7e163
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency logRelease files / migrify-0.1.2-py3-none-any.whl
| Download URL | migrify-0.1.2-py3-none-any.whl |
|---|---|
| Size | 35.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7b9433b106626f6412848866ed739459bed1305013abdae89ac1adc4be5f29b
|
|
BLAKE2b-256 checksum How to use checksums |
f98e49c9411918e506cd4b976a44893084cbc110b9c5c4f1e4f4e7f17b534f59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency log