"""Support teams get contacts; drop the legacy appowners pair Restructures the application support model: supportteams keeps teamname (now unique) + teamurl (widened to TEXT for ServiceNow group deep links), gains a child supportteamcontacts table, and sheds its single-owner appownerid FK and the appowners table. Each legacy team's appowner is migrated into ONE contact. applications.supportteamid is unchanged (it already points at supportteams). Idempotent guards throughout so it is safe on a partially-migrated box. Revision ID: 7d18_supportteamcontacts Revises: 7d17_machines_rename Create Date: 2026-07-11 """ from alembic import op import sqlalchemy as sa revision = '7d18_supportteamcontacts' down_revision = '7d17_machines_rename' branch_labels = None depends_on = None def _has_table(insp, name): return insp.has_table(name) def _has_column(insp, table, column): return column in [c['name'] for c in insp.get_columns(table)] def _has_unique(insp, table, name): return name in [u['name'] for u in insp.get_unique_constraints(table)] def upgrade(): bind = op.get_bind() insp = sa.inspect(bind) # 1. New contacts table. if not _has_table(insp, 'supportteamcontacts'): op.create_table( 'supportteamcontacts', sa.Column('contactid', sa.Integer(), primary_key=True), sa.Column('supportteamid', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=100), nullable=False), sa.Column('sso', sa.String(length=50), nullable=True), sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'), sa.Column('createddate', sa.DateTime(), nullable=False), sa.Column('modifieddate', sa.DateTime(), nullable=False), sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'), sa.ForeignKeyConstraint( ['supportteamid'], ['supportteams.supportteamid'], ondelete='CASCADE'), ) # 2. Migrate each team's appowner into one contact. if _has_table(insp, 'appowners') and \ _has_column(insp, 'supportteams', 'appownerid'): bind.exec_driver_sql( "INSERT INTO supportteamcontacts " "(supportteamid, name, sso, sortorder, isactive, " " createddate, modifieddate) " "SELECT st.supportteamid, ao.appowner, ao.sso, 0, 1, " " NOW(), NOW() " "FROM supportteams st " "JOIN appowners ao ON st.appownerid = ao.appownerid " "WHERE st.appownerid IS NOT NULL") # 3. teamname becomes unique. if not _has_unique(insp, 'supportteams', 'uq_supportteams_teamname'): op.create_unique_constraint( 'uq_supportteams_teamname', 'supportteams', ['teamname']) # 4. Widen teamurl to TEXT. op.alter_column('supportteams', 'teamurl', existing_type=sa.String(length=255), type_=sa.Text(), existing_nullable=True) # 5. Drop the appownerid FK + column, then the appowners table. if _has_column(insp, 'supportteams', 'appownerid'): for fk in insp.get_foreign_keys('supportteams'): if 'appownerid' in fk['constrained_columns'] and fk.get('name'): bind.exec_driver_sql( f"ALTER TABLE supportteams DROP FOREIGN KEY {fk['name']}") op.drop_column('supportteams', 'appownerid') bind.exec_driver_sql("DROP TABLE IF EXISTS appowners") def downgrade(): bind = op.get_bind() insp = sa.inspect(bind) # Recreate appowners. if not _has_table(insp, 'appowners'): op.create_table( 'appowners', sa.Column('appownerid', sa.Integer(), primary_key=True), sa.Column('appowner', sa.String(length=100), nullable=False), sa.Column('sso', sa.String(length=50), nullable=True), sa.Column('email', sa.String(length=100), nullable=True), sa.Column('createddate', sa.DateTime(), nullable=False), sa.Column('modifieddate', sa.DateTime(), nullable=False), sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'), ) # Re-add the appownerid FK column. if not _has_column(insp, 'supportteams', 'appownerid'): op.add_column('supportteams', sa.Column('appownerid', sa.Integer(), nullable=True)) op.create_foreign_key( 'fk_supportteams_appownerid', 'supportteams', 'appowners', ['appownerid'], ['appownerid']) # Best-effort data reversal: each team's first active contact -> an owner. if _has_table(insp, 'supportteamcontacts'): rows = bind.execute(sa.text( "SELECT supportteamid, name, sso FROM supportteamcontacts " "WHERE isactive = 1 ORDER BY supportteamid, sortorder, contactid")) seen = set() for row in rows: if row.supportteamid in seen: continue seen.add(row.supportteamid) result = bind.execute(sa.text( "INSERT INTO appowners " "(appowner, sso, isactive, createddate, modifieddate) " "VALUES (:name, :sso, 1, NOW(), NOW())"), {'name': row.name, 'sso': row.sso}) bind.execute(sa.text( "UPDATE supportteams SET appownerid = :ownerid " "WHERE supportteamid = :teamid"), {'ownerid': result.lastrowid, 'teamid': row.supportteamid}) # Narrow teamurl back to VARCHAR(255). op.alter_column('supportteams', 'teamurl', existing_type=sa.Text(), type_=sa.String(length=255), existing_nullable=True) # Drop the unique constraint. if _has_unique(insp, 'supportteams', 'uq_supportteams_teamname'): op.drop_constraint('uq_supportteams_teamname', 'supportteams', type_='unique') op.drop_table('supportteamcontacts')