Alembic Migrations¶
Vectis uses Alembic for schema migrations against a single PostgreSQL schema. All modules share one migration history in vectis/backend/alembic/versions/.
Generating a Migration¶
After changing SQLAlchemy models, auto-generate a revision:
Always review the generated file — autogenerate misses data migrations, custom constraints, and partial indexes.
Warning
Import every model module in alembic/env.py so Base.metadata includes all tables. Missing imports produce empty migrations.
Running Migrations¶
alembic upgrade head # apply all pending
alembic upgrade +1 # one revision at a time
alembic current # check current revision
alembic history --verbose # show history
Conventions¶
- Descriptive names —
add_loyalty_points_table, notupdate_schema - Always include downgrade — every
upgrade()needs a matchingdowngrade() - Test both directions —
upgrade head,downgrade -1,upgrade head - Server defaults for new NOT NULL columns — existing rows need a value
- BigInteger PKs — all new tables must use
sa.BigInteger()primary keys
Example: Multi-Currency Migration¶
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade() -> None:
op.add_column('channels', sa.Column(
'supported_currencies', postgresql.JSONB(), nullable=True, server_default='["USD"]',
))
op.create_table('exchange_rates',
sa.Column('id', sa.BigInteger(), primary_key=True),
sa.Column('base_currency', sa.String(3), nullable=False, index=True),
sa.Column('target_currency', sa.String(3), nullable=False, index=True),
sa.Column('rate', sa.Numeric(18, 8), nullable=False),
sa.Column('effective_from', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()')),
sa.UniqueConstraint('base_currency', 'target_currency', 'effective_from',
name='uq_er_pair_effective'),
)
op.add_column('orders', sa.Column(
'currency', sa.String(3), nullable=False, server_default='USD',
))
def downgrade() -> None:
op.drop_column('orders', 'currency')
op.drop_table('exchange_rates')
op.drop_column('channels', 'supported_currencies')
Note
When adding NOT NULL columns to existing tables, always include server_default so existing rows are populated automatically.
Seeding After Migrations¶
vectis/core/seed.py creates the default channel, roles, permissions, and an admin user. Run after applying migrations to a fresh database:
The seed is idempotent — safe to run on a populated database.
Rolling Back¶
alembic downgrade -1 # revert last migration
alembic downgrade a3e2f1b8c9d4 # revert to specific revision
alembic downgrade base # revert everything
Warning
Downgrade in production requires caution. Dropping columns/tables is irreversible once committed. Always back up first.
Schema-Drift Guard¶
make check runs a schema-drift guard that diffs the SQLAlchemy metadata graph against alembic upgrade head. The check fails if the live schema and the model definitions disagree — either because a model was edited without a migration or because a migration drifted from the model.
The guard is part of every local check; CI is currently disabled in favor of running make check inside the API container before pushing.
Troubleshooting¶
- "Target database is not up to date" — run
alembic upgrade headfirst - Empty migration — model module not imported in
alembic/env.py - Merge conflicts —
alembic merge heads -m "merge_branches" - Schema-drift failure in
make check— either generate the missing migration withalembic revision --autogenerate -m "..."or fix the model so it matches the live schema. Don't bypass the gate.
Notable Recent Migrations¶
| Revision | Purpose |
|---|---|
zen5o6p7q8r9 | Packaging UoM ladder, products.track_inventory, MMOQ caps on product_variants |
zfo6p7q8r9s0 | MMOQ display unit |
zir9s0t1u2v3 | Channel timezone for 30-day MMOQ window |
zjs0t1u2v3w4 | Variant.external_stock_snapshot for untracked products |
zkt1u2v3w4x5 | OrderLineItem.tracking_enabled_at_checkout snapshot |
zlu2v3w4x5y6 | Product.inventory_state_version race guard |
zmv3w4x5y6z7 | product_packages.sku for cart bulk lookup |
zqz7a8b9c0d1 | Order external-fulfillment handoff columns |
zra8b9c0d1e2 | CartRejectionEvent audit table |
zsb9c0d1e2f3 | cart_approved_blocked_inventory status enum value (B2B inventory revalidation gate) |
ztc0d1e2f3g4 | Cart.cart_approved_grand_total (pricing-drift gate) |
zud1e2f3g4h5 | payment_tender.source provenance column |