Skip to main content

Migrations

Ryx can introspect your database, detect schema changes, and generate DDL automatically โ€” with full multi-database routing and colored CLI output.

CLI in Actionโ€‹

The CLI uses ANSI colors on supported terminals (macOS, Linux). Colors auto-disable when NO_COLOR is set or output is piped:

  [ryx]  โœ“ 0001_initial              โ† bold blue prefix, green check
[ryx] โš  No migration files exist โ† yellow warning
[ryx] Skipped database blog โ† yellow "Skipped", magenta alias

Two Approachesโ€‹

Direct Migration (No Files)โ€‹

Best for prototyping and simple projects:

from ryx.migrations import MigrationRunner

await ryx.setup("sqlite:///app.db")
runner = MigrationRunner([Author, Post, Tag])
await runner.migrate()

# Preview without applying
await runner.migrate(dry_run=True)

File-Based Migrationsโ€‹

Best for production and team projects:

# Generate migration files
python -m ryx makemigrations \
--models myapp.models \
--dir migrations/

# Apply migrations
python -m ryx migrate \
--url postgres://user:pass@localhost/mydb \
--models myapp.models

# Preview SQL
python -m ryx sqlmigrate 0001_initial --dir migrations/

# Check status
python -m ryx showmigrations \
--url postgres://user:pass@localhost/mydb \
--dir migrations/

Migration File Formatโ€‹

Generated migration files are clean, multi-line Python with model class references for automatic database routing:

from myapp.models import Author, Post
from ryx.migrations.autodetect import CreateTable, AddField, ColumnState

Migration = [
CreateTable(
table='authors',
columns=[
ColumnState(name='id', db_type='INTEGER', primary_key=True, unique=True),
ColumnState(name='name', db_type='VARCHAR(100)'),
],
model=Author,
),
CreateTable(
table='posts',
columns=[
ColumnState(name='id', db_type='INTEGER', primary_key=True, unique=True),
ColumnState(name='title', db_type='VARCHAR(200)'),
ColumnState(name='author_id', db_type='INTEGER'),
],
model=Post,
),
AddField(
table='posts',
column=ColumnState(name='views', db_type='INTEGER', nullable=False),
model=Post,
),
]

Key features:

  • Each operation embeds the Model class (model=Author) so the runner knows which database it belongs to via model._meta.database
  • Models are imported at the top of the file (from myapp.models import Author)
  • Output is indented and readable, one argument per line
  • Backward compatible โ€” legacy files without model= still work

File Discoveryโ€‹

Migration files are discovered recursively under the migrations directory. All files matching [0-9]*.py are found, sorted globally by numeric prefix:

migrations/
โ”œโ”€โ”€ 0001_initial.py
โ”œโ”€โ”€ 0002_add_views.py
โ”œโ”€โ”€ blog/
โ”‚ โ”œโ”€โ”€ 0001_blog_tables.py
โ”‚ โ””โ”€โ”€ 0002_add_tags.py
โ””โ”€โ”€ shop/
โ””โ”€โ”€ 0001_shop_tables.py

Files in subdirectories are found automatically โ€” no per-alias configuration needed.

Multi-Database Routingโ€‹

When using multiple databases, each operation knows its target database from the embedded Model class. The runner:

  1. Discovers all migration files recursively
  2. For each database alias, filters operations whose model._meta.database matches
  3. Applies only relevant operations per alias
  4. Tracks applied state as alias|stem in ryx_migrations
# Apply only to the "blog" alias
python -m ryx migrate --database blog

# Target a specific PostgreSQL schema (schema-per-tenant)
python -m ryx migrate --schema tenant1

# Non-interactive mode (for CI/CD)
python -m ryx migrate --no-interactive

Interactive Fallbackโ€‹

When no migration files exist yet, ryx migrate offers an interactive menu:

  [ryx]  โš  No migration files exist for database default
3 model(s) are not yet tracked.

L)ive DDL โ€” apply changes directly (development only)
A)uto-generate migration files, then migrate
M)anual โ€” run ryx makemigrations first
S)kip this database for now

[ryx] Choice [S]:

Use --no-interactive to skip this prompt in scripts or CI/CD (exits with a hint instead).

Migration Trackingโ€‹

Ryx creates a ryx_migrations table to track applied migrations:

ColumnType
idINTEGER
nameTEXT (`alias
applied_atTIMESTAMP

Applied entries use alias|stem (e.g. default|0001_initial, blog|0001_blog_tables). Legacy bare-stem entries are still recognized for backward compatibility.

What Migrations Handleโ€‹

  • Creating and dropping tables
  • Adding, altering, and dropping columns
  • Creating and dropping indexes
  • Adding and dropping constraints
  • Creating ManyToMany join tables
  • Unique constraints and composite indexes

DDL Generationโ€‹

Generate backend-aware DDL programmatically:

from ryx.migrations import generate_schema_ddl, DDLGenerator

# All models at once
stmts = generate_schema_ddl([Author, Post], backend="postgres")
for sql in stmts:
print(sql)

# Fine-grained control
gen = DDLGenerator("sqlite")
print(gen.create_table(Post._meta_to_table_state()))
print(gen.add_column("posts", column_state))

Backend Differencesโ€‹

| Feature | PostgreSQL | MySQL | SQLite | |---|---|---|---|---| | ALTER COLUMN | Yes | Yes | No (recreate table) | | Native UUID | Yes | No | No | | SERIAL | Yes | No | No | | JSONB | Yes | No | No | | Array types | Yes | No | No | | Database schemas | "schema"."table" | No | No |

Next Stepsโ€‹

โ†’ Filtering โ€” Start querying your data โ†’ CLI Reference โ€” All migration commands