Skip to main content

ADR-0009: Migrations Are Hand-Written, One Statement per Call — No ORM Autogenerate

Status

Accepted

Context

Alembic supports --autogenerate, which diffs an ORM's declared models against the live database schema to produce a migration automatically. Azan360's data layer is raw asyncpg with no ORM — there are no models to diff against.

Decision

Migrations are written by hand with op.execute(...), never with --autogenerate. Each migration uses exactly one SQL statement per op.execute() call, because SQLAlchemy's asyncpg dialect prepares each statement it's given, and a prepared statement cannot hold more than one command.

Alternatives Considered

Using --autogenerate against an ORM layer — not viable without first introducing an ORM, which the project does not have and which is treated as out of scope by this decision (implicitly: the raw-asyncpg data-layer choice is prior to and outside this ADR's scope).

Consequences

  • Without an ORM to diff against, --autogenerate would produce a confident, empty diff — actively worse than no tooling, since it looks like verification without providing any. Hand-writing migrations is the correct choice given the existing data-layer architecture, not a shortcut.
  • Every migration author must know the exact SQL being applied — there is no generated starting point to review and adjust.
  • The one-statement-per-call constraint is not a style preference; batching multiple statements in one op.execute() will fail at the driver level because of how the asyncpg dialect prepares statements.
  • Destructive statements can still be reviewed before touching production via alembic upgrade head --sql, which emits SQL instead of running it.