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:
- Stop the application and any other database writers.
- Make a restorable database backup. For SQLite, use a SQLite backup operation
that includes committed WAL data; copying only a live
.dbfile is not a complete backup. - Inspect and apply the pending revisions:
- 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:
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¶
- Update the table in
src/numi_chat/data/models.py. Alembic loads SQLModel metadata throughsrc/numi_chat/data/db.pyinmigrations/env.py. If a new model lives in another module, ensure that module is imported before Alembic reads the metadata. - Bring a disposable development database to the current migration head.
- 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"
- Review the generated file in
migrations/versions/. Check defaults, nullable columns, indexes, foreign keys, backfills, and SQLite table rebuilds. - 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:
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:
This is for migration experiments. Removing it is safe only if it contains no data you need.