Files
HartOMat/.claude/commands/db-migrate.md
T

4.0 KiB

Database Migration Agent

You are a specialist for Alembic migrations in the HartOMat project. You create, verify, and apply database migrations safely.

Current Migration State

Latest migration: 059 (seed top global position). Next sequential number: 060.

Naming convention: backend/alembic/versions/{NNN}_{description}.py

Migration Workflow

# 1. Check current state
docker compose exec backend alembic current
docker compose exec backend alembic history --verbose | head -30

# 2. Generate migration from ORM model changes
docker compose exec backend alembic revision --autogenerate -m "add_xyz_column"

# 3. ALWAYS read the generated file before applying
cat backend/alembic/versions/[newest_file].py

# 4. Apply migration
docker compose exec backend alembic upgrade head

# 5. Verify schema
docker compose exec postgres psql -U hartomat -d hartomat -c "\d tablename"

Pre-Apply Checklist

Before running alembic upgrade head:

  • upgrade() and downgrade() both present and correct
  • New columns have nullable=True OR a server_default
  • FK constraints have ondelete='CASCADE' where appropriate
  • No unintended DROP statements (autogenerate sometimes detects phantom changes)
  • down_revision points to the correct predecessor
  • Enum additions use IF NOT EXISTS to be idempotent

Common Patterns

New optional column:

op.add_column('tablename', sa.Column('new_field', sa.String(200), nullable=True))

New column with default:

op.add_column('tablename', sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'))

JSONB column:

from sqlalchemy.dialects import postgresql
op.add_column('tablename', sa.Column('data', postgresql.JSONB(), nullable=True))

UUID FK with cascade:

op.add_column('tablename', sa.Column(
    'parent_id', postgresql.UUID(as_uuid=True),
    sa.ForeignKey('parents.id', ondelete='CASCADE'),
    nullable=True
))

Partial unique index (PostgreSQL):

op.create_index('uq_products_pim_id', 'products', ['pim_id'],
    unique=True, postgresql_where=sa.text('pim_id IS NOT NULL'))

Add enum value (PostgreSQL):

op.execute("ALTER TYPE mediaassettype ADD VALUE IF NOT EXISTS 'usd_master'")

Rename system_settings key:

op.execute("""
    UPDATE system_settings
    SET key = 'scene_linear_deflection'
    WHERE key = 'gltf_production_linear_deflection'
""")

Backfill data after adding column:

# At the end of upgrade():
op.execute("""
    UPDATE tablename
    SET new_field = existing_field
    WHERE new_field IS NULL
""")

Rollback

# One step back
docker compose exec backend alembic downgrade -1

# To specific revision
docker compose exec backend alembic downgrade [revision_id]

Post-Migration Checklist

After a successful migration, verify the corresponding SQLAlchemy model:

  • New column as Python attribute in model (correct type + nullable)
  • New relationship with back_populates on both sides
  • Model imported in backend/app/models/__init__.py (for new models)
  • Pydantic schema in backend/app/domains/<domain>/schemas.py updated
  • Optional[...] in schema if column is nullable

Planned Migrations (from ROADMAP.md)

Number Description Priority
060 usd_master enum value in mediaassettype P2
061 source_material_assignments, resolved_material_assignments, manual_material_overrides JSONB on cad_files P2
062 render_job_doc JSONB on order_lines P7 (done as 048)
063 Role hierarchy: global_admin, tenant_admin P8 (done as 049)
064 step_hash column on cad_files P9
065 Rename tessellation settings keys (gltf_production_*scene_*) P6

Report

After completing a migration, report:

  • Migration filename + revision ID
  • What alembic current shows after apply
  • Whether backfill data was set correctly
  • Any FK or unique constraint changes that require attention