"""replace interfaces with interface_type and add requires_port Revision ID: 0015_single_interface Revises: 0014_merge_heads Create Date: 2026-05-22 22:00:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "0015_single_interface" down_revision: Union[str, Sequence[str], None] = "0014_merge_heads" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def _get_dialect() -> str: """Get the current database dialect name.""" conn = op.get_bind() return conn.dialect.name def upgrade() -> None: dialect = _get_dialect() # Add new columns op.add_column('tool_types', sa.Column('interface_type', sa.String(20), nullable=True)) op.add_column('tool_types', sa.Column('requires_port', sa.Boolean(), nullable=False, server_default='true')) # Migrate data: take first element from interfaces JSON array if dialect == 'postgresql': op.execute(""" UPDATE tool_types SET interface_type = COALESCE( (SELECT elem FROM jsonb_array_elements_text(interfaces::jsonb) AS elem LIMIT 1), 'web' ), requires_port = CASE WHEN COALESCE( (SELECT elem FROM jsonb_array_elements_text(interfaces::jsonb) AS elem LIMIT 1), 'web' ) = 'web' THEN true ELSE false END """) else: # SQLite: interfaces is stored as JSON text, extract first array element op.execute(""" UPDATE tool_types SET interface_type = COALESCE( (SELECT json_extract(value, '$[0]') FROM json_each(interfaces) AS value WHERE json_valid(interfaces) LIMIT 1), 'web' ), requires_port = CASE WHEN COALESCE( (SELECT json_extract(value, '$[0]') FROM json_each(interfaces) AS value WHERE json_valid(interfaces) LIMIT 1), 'web' ) = 'web' THEN true ELSE false END """) # Make interface_type non-nullable after data migration op.alter_column('tool_types', 'interface_type', nullable=False) # Drop old interfaces column op.drop_column('tool_types', 'interfaces') # Add CHECK constraint for interface_type (only on PostgreSQL; SQLite supports it too) op.create_check_constraint('chk_interface_type', 'tool_types', sa.text("interface_type IN ('web', 'terminal')")) def downgrade() -> None: dialect = _get_dialect() # Drop CHECK constraint op.drop_constraint('chk_interface_type', 'tool_types', type_='check') # Add back interfaces column if dialect == 'postgresql': op.add_column('tool_types', sa.Column('interfaces', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='["web"]')) # Migrate data back: wrap interface_type in array op.execute(""" UPDATE tool_types SET interfaces = jsonb_build_array(interface_type) """) else: op.add_column('tool_types', sa.Column('interfaces', sa.JSON(), nullable=False, server_default='["web"]')) # Migrate data back: wrap interface_type in array for SQLite op.execute(""" UPDATE tool_types SET interfaces = json_array(interface_type) """) # Drop new columns op.drop_column('tool_types', 'requires_port') op.drop_column('tool_types', 'interface_type')