Skip to content
3 min read · 507 words

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:

python
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:

PropertyHow
IdempotentPRAGMA table_info check before each ALTER — safe on every boot
Transactionalengine.begin() wraps all additions
SQLite-onlyearly-returns for any other DATABASE_URL
Additive-onlycan 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

  1. Add the column in models.py (nullable, or with a default — additive-only!).
  2. Add a matching "column_name": "SQL TYPE" entry to the appropriate dict in run_startup_migrations() (create a new table-specific dict + PRAGMA block if it's not tool_calls).
  3. If the API should expose it, update the response schema in schemas.py.
  4. Boot the server against an existing dev database and confirm no no such column errors.

New tables need no migration entry — create_all handles them. Worked example: Add a Migration.

Rendering diagram…

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.