Skip to main content

gen_migration

Dev-time autogenerate helper for cache-type migrations.

The ORM (types/name/vN/schema.py) is the single authoring source of truth. A human only ever edits the ORM; this tool computes the delta from the previous version's ORM and emits types/name/vN/migrations.py — an Alembic op-API upgrade(op) / downgrade(op) pair. The committed file is a reviewed, deterministic snapshot of that delta; the runtime driver (implementations/sqlite.py) executes it, never this tool.

Usage:

python -m bitfount.cache.tools.gen_migration type version

e.g. python -m bitfount.cache.tools.gen_migration model_inferences 2 diffs the v1 ORM against the v2 ORM and writes types/model_inferences/v2/migrations.py.

This drives Alembic's autogenerate programmatically — there is deliberately no env.py / alembic.ini / alembic_version table, consistent with the cache's "op-API only" adoption of Alembic.

Autogenerate is imperfect: review the emitted ops before committing. It does not always infer server defaults or type changes correctly — hand-edit the emitted body for those, then keep the ORM in sync so a future regenerate stays a no-op.

Renames. Alembic cannot detect a rename: it diffs column presence, not identity, so a renamed column comes out as add_column + drop_column — which would drop the data. Two guards address this:

  • Any drop_column in the generated upgrade aborts the write unless you pass --allow-drop, so a rename can never silently reach a committed file.
  • Declare the intended rename with --rename table.old=new (repeatable). The tool then collapses the matching add+drop pair into op.alter_column(old, new_column_name=new), which under batch mode rebuilds the table preserving the column's data. The downgrade gets the inverse rename automatically.

Table renames. A changed __tablename__ is the same trap one level up: autogen emits create_table(new) + drop_table(old), orphaning the old table's rows. The same two guards apply, via --rename-table old=new (collapses the pair into op.rename_table(old, new), a data-preserving native SQLite rename) and --allow-drop-table (confirms a genuine table drop).

Module

Functions

main

def main(argv: list[str] | None = None)> int:

CLI entry point.

render_migration

def render_migration(    name: str,    version: int,    renames: list[tuple[str, str]] | None = None,    table_renames: list[tuple[str, str]] | None = None,    allow_drop: bool = False,    allow_drop_table: bool = False,)> str:

Render the migrations.py source for name at version (>= 2).

Builds a scratch in-memory SQLite DB holding the v(version-1) table (the "current" state), then diffs the vN ORM metadata against it to produce the additive upgrade (and its reverse), rendered in batch mode so drop/rename/ retype work on SQLite.

Rename intent must be declared — autogen can only see drop+add:

  • renames: (old, new) column pairs, collapsed into alter_column renames.
  • table_renames: (old, new) table pairs, collapsed into rename_table.

The inverse renames are applied to the downgrade automatically. A surviving drop_column raises unless allow_drop is set; a surviving drop_table raises unless allow_drop_table is set — so a rename can never silently degrade into a data-dropping migration.