"""dashboarddefaults: add fqdn, make ipaddress nullable. Map a display PC to its role/location by its STABLE FQDN (derived from the BIOS serial, F.) instead of only its DHCP-changeable IP. IP stays as a nullable fallback. Idempotent. """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '7d31_dashboarddefault_fqdn' down_revision = '7d30_apitoken_resourcescopes' branch_labels = None depends_on = None def _has_column(bind, table, column): inspector = sa.inspect(bind) if table not in inspector.get_table_names(): return False return column in {c['name'] for c in inspector.get_columns(table)} def _col_nullable(bind, table, column): inspector = sa.inspect(bind) for c in inspector.get_columns(table): if c['name'] == column: return c.get('nullable', True) return True def _has_index(bind, table, index): inspector = sa.inspect(bind) if table not in inspector.get_table_names(): return False return index in {i['name'] for i in inspector.get_indexes(table)} def upgrade(): bind = op.get_bind() if not _has_column(bind, 'dashboarddefaults', 'fqdn'): op.add_column('dashboarddefaults', sa.Column('fqdn', sa.String(191), nullable=True)) if not _has_index(bind, 'dashboarddefaults', 'idx_dashboarddefaults_fqdn'): op.create_index('idx_dashboarddefaults_fqdn', 'dashboarddefaults', ['fqdn'], unique=True) # IP is now optional (an FQDN-only mapping has no IP). if not _col_nullable(bind, 'dashboarddefaults', 'ipaddress'): op.alter_column('dashboarddefaults', 'ipaddress', existing_type=sa.String(50), nullable=True) def downgrade(): bind = op.get_bind() if _has_index(bind, 'dashboarddefaults', 'idx_dashboarddefaults_fqdn'): op.drop_index('idx_dashboarddefaults_fqdn', table_name='dashboarddefaults') if _has_column(bind, 'dashboarddefaults', 'fqdn'): op.drop_column('dashboarddefaults', 'fqdn') # ipaddress is left nullable: reverting to NOT NULL could fail on FQDN-only # rows that legitimately have no IP.