Migrations
There is no Alembic. Schema management is two mechanisms that run at every boot: create_all for new tables, and a hand-rolled column-adder for evolving tool_calls. Simple, idempotent, additive-only.
Mechanism 1 — Base.metadata.create_all
Runs first in main.py. Creates any table that doesn't exist, from the current model definitions. Guarantees a fresh checkout boots into a complete schema with zero commands.
Blind spot: create_all never alters existing tables — add a column to a model and existing databases won't get it. That is mechanism 2's job.
Mechanism 2 — run_startup_migrations()
In database.py, runs right after create_all:
tool_call_columns = {
"model_name": "VARCHAR(100)",
"estimated_tokens": "INTEGER",
"estimated_cost": "VARCHAR(30)",
}
with engine.begin() as connection:
existing = {row[1] for row in connection.execute(text("PRAGMA table_info(tool_calls)"))}
for name, type_ in tool_call_columns.items():
if name not in existing:
connection.execute(text(f"ALTER TABLE tool_calls ADD COLUMN {name} {type_}"))Properties:
| Property | How |
|---|---|
| Idempotent | PRAGMA table_info check before each ALTER — safe on every boot |
| Transactional | engine.begin() wraps all additions |
| SQLite-only | early-returns for any other DATABASE_URL |
| Additive-only | can only ADD COLUMN — no drops, renames, retypes, or backfills |
History: these three columns were added when cost tracking landed on ToolCall — the only schema evolution so far.
The rule for contributors
- Add the column in
models.py(nullable, or with a default — additive-only!). - Add a matching
"column_name": "SQL TYPE"entry to the appropriate dict inrun_startup_migrations()(create a new table-specific dict + PRAGMA block if it's nottool_calls). - If the API should expose it, update the response schema in
schemas.py. - Boot the server against an existing dev database and confirm no
no such columnerrors.
New tables need no migration entry — create_all handles them. Worked example: Add a Migration.
Failure symptom when the rule is broken
sqlite3.OperationalError: no such column: tool_calls.model_name (or similar) at query time — not at boot. Fresh databases work (they got the column from create_all), long-lived dev databases break. Fix: add the migration entry; the next boot repairs the DB. See Troubleshooting.
When to graduate to Alembic
Adopt a real migration tool when any of these appears: a second contributor regularly changing schema · a non-additive change you can't avoid · a Postgres deployment · the need for reversible/versioned migrations in CI. The move is mechanical (alembic init, autogenerate against the models, retire run_startup_migrations) and is on the Roadmap.
Related pages
- Database Layer — the hosting module
- Schema — the current end state
- Add a Migration — step-by-step