Skip to content

Database Migrations

Numi uses Alembic for schema changes and SQLite by default. Run these commands from the repository root with the same DB_URL as the instance you intend to migrate.

Apply migrations

For an existing instance:

  1. Stop the application and any other database writers.
  2. Make a restorable database backup. For SQLite, use a SQLite backup operation that includes committed WAL data; copying only a live .db file is not a complete backup.
  3. Inspect and apply the pending revisions:
make db-current
make db-history
make db-migrate
make db-current
  1. Start the application and verify that an existing account can open its chats and files. Keep the backup until this check passes.

The local default is ./chats.db. Docker Compose mounts that same host file at /app/state/chats.db; it does not use ./state/chats.db on the host. If you override the database location, back up the actual configured database. Uploads and generated files are separate data; see Run with Docker for storage paths.

For the Compose service, stop it with docker compose stop numi-chat, back up the host database, then use make docker-migrate and make docker-up. Startup also applies pending migrations, so take the backup before starting updated application code.

Roll back a revision

Review the target revision's downgrade() before running:

make db-downgrade

This rolls back one schema revision. A downgrade can drop columns and their data; it is not a replacement for a backup.

Add a schema change

  1. Update the table in src/numi_chat/data/models.py. Alembic loads SQLModel metadata through src/numi_chat/data/db.py in migrations/env.py. If a new model lives in another module, ensure that module is imported before Alembic reads the metadata.
  2. Bring a disposable development database to the current migration head.
  3. Generate a candidate revision against that database:
DB_URL="sqlite:///migration-check.db" uv run alembic upgrade head
DB_URL="sqlite:///migration-check.db" uv run alembic revision --autogenerate -m "Describe the schema change"
  1. Review the generated file in migrations/versions/. Check defaults, nullable columns, indexes, foreign keys, backfills, and SQLite table rebuilds.
  2. Test the revision on empty and populated databases before applying it to an instance.

For a revision written by hand, use uv run alembic revision -m "Describe the data change". Do not edit an already-applied revision; add a new one.

Data migrations

Autogeneration cannot infer a data backfill or safely recognize every rename. Write those operations explicitly with Alembic and SQLAlchemy. Populate new required fields before enforcing their constraints, and check that existing accounts, chats, messages, and memory rows survive.

Use tests/test_migrations.py as the reference: its tests upgrade to an older revision, insert representative records, upgrade to head, and check the preserved values. Run them with:

uv run pytest tests/test_migrations.py

When a change is reversible, test downgrade followed by another upgrade. For changes to ownership relationships, also check deletion behavior in tests/test_deletion_integrity.py.

SQLite considerations

migrations/env.py enables Alembic batch mode. Changes to constraints, types, or defaults can rebuild a table; allow disk space for the temporary copy. Review generated batch operations rather than assuming an ordinary ALTER COLUMN works on SQLite.

A failed SQLite migration can leave partial schema changes or a temporary table. The revision recorded by Alembic is not sufficient proof that every operation rolled back.

Troubleshooting

Database is behind the code

Compare make db-current with make db-history, then follow the backup and migration steps above. Do not delete the database or stamp it to head to hide a mismatch.

Migration fails

Keep the application stopped. Preserve the failing database, its backup, and the migration output. Inspect both the schema and the recorded revision. Test recovery on a disposable copy before retrying on the original.

Fresh disposable database

Choose a filename that is not used by your instance:

DB_URL="sqlite:///migration-check.db" make db-migrate

This is for migration experiments. Removing it is safe only if it contains no data you need.