Migrations¶
yara_orm ships a Django/Tortoise-style migration system for evolving your
database schema alongside your models. Migrations are operation-based
and auto-generated: makemigrations diffs your models against the recorded
state and writes a numbered migration file; upgrade and downgrade apply or
revert those files in this async Python ORM.
Crucially, migrations are backend-portable. A migration records operations (create table, add column, …), not raw DDL. The same operations render to the correct SQL for the active dialect at apply time, so one migration set runs unchanged on PostgreSQL or SQLite.
How state is tracked
The target schema is replayed from the migration files on disk
(Django-style) — each file's operations are applied in order to rebuild
the recorded schema state, which is then diffed against your current models.
Applied migrations are recorded per app in an orm_migrations table, which
is created automatically on first use.
The CLI¶
Run the tool as a module:
Global options¶
These flags belong to the top-level parser, so they come before the subcommand:
| Flag | Default | Purpose |
|---|---|---|
--dir |
migrations |
Migrations directory. |
--app |
models |
App/label recorded against migrations in orm_migrations. |
--db |
(none) | Database URL; opens a connection for commands that touch the DB. |
--models |
(empty) | Comma-separated model modules to import (and resolve relations) before running. |
Order matters
--dir, --app, --db, and --models must appear before the
subcommand. Subcommand flags (like --name or --empty) appear after
it. For example:
init¶
Create the migrations directory (and its __init__.py) if missing.
makemigrations¶
Generate a migration from the model diff. Takes --name (a label for the file)
and --empty (write a migration with no operations, for hand-written data
migrations). With no detected changes, it prints no changes detected.
python -m yara_orm --models myapp.models makemigrations --name add_age
python -m yara_orm --models myapp.models makemigrations --empty --name backfill
upgrade [version]¶
Apply pending migrations, recording each in orm_migrations. An optional
positional version stops after that migration; omit it to apply everything.
python -m yara_orm --db postgres://localhost/app --models myapp.models upgrade
python -m yara_orm --db postgres://localhost/app --models myapp.models upgrade 0002_add_age
downgrade [version] [--steps N]¶
Revert applied migrations. By default reverts --steps 1 (the most recent
migration). An optional positional version reverts everything after that
migration and takes precedence over --steps.
python -m yara_orm --db postgres://localhost/app --models myapp.models downgrade --steps 2
python -m yara_orm --db postgres://localhost/app --models myapp.models downgrade 0001_initial
history¶
List applied migrations for the app, with their timestamps.
heads¶
List every on-disk migration and whether it has been applied ([x] / [ ]).
sqlmigrate <version> [--backward]¶
Print a migration's SQL without running it. Pass --backward to render the
reverse SQL. The version argument is required here.
python -m yara_orm --models myapp.models sqlmigrate 0001_initial
python -m yara_orm --models myapp.models sqlmigrate 0001_initial --backward
Backend-portable SQL preview¶
Because operations render per dialect, sqlmigrate shows the same migration
as different DDL depending on the --db URL. Choose a backend by pointing
--db at PostgreSQL or SQLite:
The migration file is identical in both cases — only the rendered SQL differs.
Programmatic API¶
The same workflow is available through MigrationManager, exported from
yara_orm:
import asyncio
from yara_orm import MigrationManager, YaraOrm
from myapp.models import User, Post
async def main() -> None:
await YaraOrm.init("sqlite:///app.db")
manager = MigrationManager(
directory="migrations",
app="myapp",
models=[User, Post], # defaults to every registered model
)
manager.init() # ensure the directory exists
filename = manager.make_migrations(name="initial") # -> "0001_initial.py" or None
applied = await manager.upgrade() # -> ["0001_initial"]
for row in await manager.history(): # [{"name", "applied_at"}, ...]
print(row["name"], row["applied_at"])
for row in await manager.heads(): # [{"name", "applied"}, ...]
print(row["name"], row["applied"])
sql = manager.sqlmigrate("0001_initial") # list[str], no execution
await manager.downgrade(steps=1) # -> reverted names
await YaraOrm.close()
asyncio.run(main())
| Method | Signature | Notes |
|---|---|---|
init() |
init() -> None |
Create the directory and __init__.py. |
make_migrations() |
make_migrations(name=None, empty=False) -> str \| None |
Returns the new filename, or None when there are no changes. |
upgrade() |
await upgrade(target=None) -> list[str] |
Applies pending migrations up to an optional target. |
downgrade() |
await downgrade(steps=1, target=None) -> list[str] |
target reverts down to that migration and wins over steps. |
history() |
await history() -> list[dict] |
Applied migrations with applied_at. |
heads() |
await heads() -> list[dict] |
All on-disk migrations with an applied flag. |
sqlmigrate() |
sqlmigrate(name, backward=False) -> list[str] |
Render SQL without executing it. |
Async surface
upgrade, downgrade, history, and heads touch the database and are
coroutines — await them. init, make_migrations, and sqlmigrate are
synchronous (the last renders SQL but does not run it).
Operations reference¶
Migration files are plain Python: a dependencies list and an operations
list, built from yara_orm.migrations (imported in generated files as
from yara_orm import migrations as m).
| Operation | Purpose |
|---|---|
CreateTable |
Create a table with its columns, primary key, foreign keys, and indexes. |
DropTable |
Drop a table (keeps its spec so it can be reversed). |
AddColumn |
Add a column, optionally with a foreign-key spec. |
DropColumn |
Drop a column (keeps its spec so it can be reversed). |
CreateIndex |
Create an index on a column. |
DropIndex |
Drop an index on a column. |
RunSQL(sql, reverse_sql=None) |
Run literal SQL forward and, optionally, its reverse. |
RunPython(forward, backward=None) |
Run async Python callables (hand-written migrations only). |
CreateTable, DropTable, AddColumn, DropColumn, CreateIndex, and
DropIndex are generated automatically by makemigrations. RunSQL and
RunPython are for hand-written --empty migrations.
Data migration with --empty¶
Start from a blank migration:
Then fill in operations. With raw SQL via RunSQL (give reverse_sql to make
it reversible):
from yara_orm import migrations as m
dependencies = ["0001_initial"]
operations = [
m.RunSQL(
"UPDATE users SET active = TRUE WHERE active IS NULL",
reverse_sql="UPDATE users SET active = NULL WHERE active = TRUE",
),
]
Or with async Python via RunPython — handy for ORM-driven data changes
(backward is optional):
from yara_orm import migrations as m
from myapp.models import User
async def seed() -> None:
await User.objects.create(name="admin")
async def unseed() -> None:
await User.objects.filter(name="admin").delete()
dependencies = ["0001_initial"]
operations = [
m.RunPython(seed, unseed),
]
Reversibility
RunPython runs backward on downgrade and RunSQL runs reverse_sql.
If you omit them, the operation simply does nothing when reverted — leave a
note so reviewers know the step is one-way.
Typical workflow¶
- Edit your models — add a field, a model, or an index.
- Generate the migration:
- Review the generated
NNNN_add_age.pyfile and itsoperations. Optionally preview the SQL withsqlmigrate. - Apply it:
- Revert if needed: