Migrations¶
Manage database schema changes with Aksara's migration system.
Overview¶
Aksara uses a project-wide migration system. All migrations live in a single migrations/ directory at your project root, regardless of how many models or modules you have.
# Create migrations from your models
aksara makemigrations --app app.models
# Apply migrations to the database
aksara migrate
Project Structure¶
myproject/
├── main.py
├── settings.py
├── app/
│ ├── models.py
│ ├── views.py
│ └── ...
└── migrations/ # All migrations go here
├── __init__.py
├── 0001_auto_initial.py
└── 0002_auto_add_email_verification.py
The migrations directory is configured via AKSARA_MIGRATIONS_DIR in your .env or settings.py (default: migrations).
Creating Migrations¶
makemigrations¶
Generate migration files from your models:
# Scan models from a module and generate a migration
aksara makemigrations --app app.models
# With a custom name
aksara makemigrations --app app.models --name add_email_verification
# Output to a custom directory
aksara makemigrations --app app.models --output my_migrations
# Preview without writing (print to stdout)
aksara makemigrations --app app.models --stdout
# Generate legacy SQL format instead of Python
aksara makemigrations --app app.models --sql
This creates a migration file in your migrations/ directory:
Migration File Structure¶
"""
Migration: add_email_verification
Generated: 2026-02-07T10:30:00
"""
from aksara.migrations import Migration
from aksara.migrations import operations as op
class Migration(Migration):
"""
Auto-generated migration for models: User
"""
dependencies = []
operations = [
op.AddField(
model_name="User",
name="email_verified",
field=fields.Boolean(default=False),
),
op.AddField(
model_name="User",
name="verification_token",
field=fields.String(max_length=100, nullable=True),
),
]
Applying Migrations¶
migrate¶
Apply pending migrations:
# Apply all pending migrations
aksara migrate
# Specify a database URL
aksara migrate --database-url postgresql://localhost/mydb
# Preview without applying
aksara migrate --dry-run
# Mark migrations as applied without running SQL
aksara migrate --fake
# Use a custom migrations directory
aksara migrate --migrations-dir my_migrations
Check Migration Status¶
aksara status
# Output:
# 📁 Migrations directory: migrations
# 📊 Applied migrations: 2
#
# [X] 0001_auto_initial
# [X] 0002_auto_add_email_verification
# [ ] 0003_auto_add_avatar # Not applied
Migration Operations¶
Aksara provides Python-based operations for schema changes.
CreateTable¶
Create a new database table:
from aksara.migrations.operations import CreateTable
from aksara import fields
CreateTable(
name="posts",
fields=[
("id", fields.UUID(primary_key=True)),
("title", fields.String(max_length=200)),
("content", fields.Text()),
("author_id", fields.UUID()),
("created_at", fields.DateTime(auto_now_add=True)),
],
)
DropTable¶
Remove a table:
AddField¶
Add a field to an existing table:
from aksara.migrations.operations import AddField
AddField(
model_name="User",
name="phone",
field=fields.String(max_length=20, nullable=True),
)
RemoveField¶
Remove a field:
from aksara.migrations.operations import RemoveField
RemoveField(
model_name="User",
name="phone",
)
AlterField¶
Modify field properties:
from aksara.migrations.operations import AlterField
# Change max_length
AlterField(
model_name="User",
name="name",
field=fields.String(max_length=200), # Was 100
)
# Make field nullable
AlterField(
model_name="Post",
name="category_id",
field=fields.UUID(nullable=True), # Was required
)
RenameField¶
Rename a field:
from aksara.migrations.operations import RenameField
RenameField(
model_name="User",
old_name="username",
new_name="handle",
)
AddIndex¶
Create an index:
from aksara.migrations.operations import AddIndex
AddIndex(
model_name="Post",
name="idx_posts_created_at",
fields=["created_at"],
)
# Composite index
AddIndex(
model_name="Post",
name="idx_posts_author_date",
fields=["author_id", "created_at"],
)
RemoveIndex¶
Drop an index:
from aksara.migrations.operations import RemoveIndex
RemoveIndex(
model_name="Post",
name="idx_posts_created_at",
)
AddConstraint¶
Add a database constraint:
from aksara.migrations.operations import AddConstraint
# Unique constraint
AddConstraint(
model_name="User",
name="unique_email",
type="unique",
fields=["email"],
)
# Check constraint
AddConstraint(
model_name="Product",
name="positive_price",
type="check",
expression="price > 0",
)
RemoveConstraint¶
Remove a constraint:
from aksara.migrations.operations import RemoveConstraint
RemoveConstraint(
model_name="User",
name="unique_email",
)
Data Migrations¶
Migrations can include data changes alongside schema changes.
RunPython¶
Execute Python code during migration:
from aksara.migrations import Migration
from aksara.migrations.operations import RunPython
async def populate_slugs(db):
"""Generate slugs for existing posts."""
rows = await db.fetch("SELECT id, title FROM posts WHERE slug IS NULL")
for row in rows:
slug = row["title"].lower().replace(" ", "-")
await db.execute("UPDATE posts SET slug = $1 WHERE id = $2", slug, row["id"])
async def reverse_slugs(db):
"""Reverse migration: clear slugs."""
await db.execute("UPDATE posts SET slug = NULL")
class Migration(Migration):
dependencies = ["0002_auto_add_slug"]
operations = [
RunPython(populate_slugs, reverse_slugs),
]
RunSQL¶
Execute raw SQL:
from aksara.migrations.operations import RunSQL
RunSQL(
sql="CREATE EXTENSION IF NOT EXISTS pg_trgm;",
reverse_sql="DROP EXTENSION IF EXISTS pg_trgm;",
)
Migration Dependencies¶
Declaring Dependencies¶
Migrations can declare dependencies on previous migrations to ensure correct ordering:
Dependencies are referenced by migration name (the filename stem). The migration executor builds a directed acyclic graph (DAG) from these dependencies and applies them in topological order.
Conflict Detection¶
Aksara detects migration conflicts when multiple developers create migrations from the same base.
What is a Conflict?¶
0001_auto_initial
│
├── 0002_auto_add_email (Developer A)
│
└── 0002_auto_add_phone (Developer B) ← CONFLICT: two heads
Resolving Conflicts¶
Create a merge migration:
This creates an empty migration that depends on both heads, linearizing the history:
# 0003_merge.py
class Migration(Migration):
"""Merge migration to resolve conflict."""
dependencies = [
"0002_auto_add_email",
"0002_auto_add_phone",
]
operations = [] # Just resolves the dependency graph
Best Practices¶
Keep Migrations Small¶
# Good: One logical change per migration
AddField(model_name="User", name="avatar_url", ...)
# Avoid: Multiple unrelated changes in one migration
Name Migrations Descriptively¶
aksara makemigrations --app app.models --name add_user_profile_fields
aksara makemigrations --app app.models --name rename_username_to_handle
Test Migrations¶
async def test_migration_applies():
"""Verify migration creates the expected schema."""
# Apply migration
await migrate()
# Verify schema via model
user = await User.objects.create(email="test@example.com")
assert user.email_verified == False
Don't Edit Applied Migrations¶
Once a migration has been applied to any environment:
- Don't modify it
- Create a new migration for further changes
Preview Before Applying¶
Migration Graph¶
Aksara maintains a directed acyclic graph (DAG) of migrations to determine execution order:
0001_auto_initial
│
└── 0002_auto_add_email
│
├── 0003_auto_add_profile
│
└── 0004_auto_add_avatar
│
└── 0005_auto_add_bio
The graph ensures migrations are applied in the correct order, even when dependencies branch and merge.
Environment Configuration¶
Configure the migrations directory in your .env:
Or override per-command:
aksara makemigrations --app app.models --output custom_migrations
aksara migrate --migrations-dir custom_migrations
Troubleshooting¶
Migration Not Detected¶
# Make sure you specify the models module
aksara makemigrations --app app.models
# Check model registration
aksara info --models
Migration Fails to Apply¶
# Preview the migration operations
aksara migrate --dry-run
# Check what's already applied
aksara status
Database Out of Sync¶
Complete Example¶
A full migration workflow:
# 1. app/models.py — Add new field
class User(Model):
email = fields.Email(unique=True)
name = fields.String(max_length=100)
avatar_url = fields.URL(nullable=True) # NEW FIELD
# 2. Generate migration
aksara makemigrations --app app.models --name add_avatar_url
# Created: migrations/0005_auto_add_avatar_url.py
# 3. Review generated migration
# migrations/0005_auto_add_avatar_url.py
from aksara.migrations import Migration
from aksara.migrations import operations as op
from aksara import fields
class Migration(Migration):
dependencies = ["0004_auto_add_bio"]
operations = [
op.AddField(
model_name="User",
name="avatar_url",
field=fields.URL(nullable=True),
),
]
# 4. Apply migration
aksara migrate
# Output:
# Applying 0005_auto_add_avatar_url... ✓ Applied successfully
Related Documentation¶
- Models — Model definition
- Fields — Field types
- CLI Reference — All CLI commands