"""sub-sections inside each vertical (e.g. Real Estate → Dubai / Moscow) A channel now belongs to exactly one section, and each section to exactly one vertical. The migration auto-creates a `Общий` section per vertical that has at least one channel and pins all existing channels there, so the service keeps working without manual reclassification after upgrade. Revision ID: 0008 Revises: 0007 Create Date: 2026-05-20 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0008" down_revision: Union[str, None] = "0007" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "sections", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("vertical", sa.String(length=32), nullable=False), sa.Column("slug", sa.String(length=64), nullable=False), sa.Column("title", sa.String(length=255), nullable=False), sa.Column("emoji", sa.String(length=8), nullable=True), sa.Column("description", sa.Text(), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.UniqueConstraint("vertical", "slug", name="uq_section_vertical_slug"), ) op.create_index("ix_sections_vertical", "sections", ["vertical"]) # Auto-create a `default` section for each vertical that already has channels, # so the backfill below has somewhere to point. op.execute( """ INSERT INTO sections (vertical, slug, title, emoji) SELECT DISTINCT c.vertical, 'default', CASE c.vertical WHEN 'hr' THEN 'Общий HR' ELSE 'Общий' END, CASE c.vertical WHEN 'hr' THEN '👥' ELSE '🏠' END FROM channels c ON CONFLICT (vertical, slug) DO NOTHING """ ) # Add nullable section_id first so the backfill can populate it. op.add_column( "channels", sa.Column("section_id", sa.Integer(), nullable=True), ) op.create_foreign_key( "fk_channels_section", "channels", "sections", ["section_id"], ["id"], ondelete="RESTRICT", ) op.create_index("ix_channels_section_id", "channels", ["section_id"]) op.execute( """ UPDATE channels c SET section_id = s.id FROM sections s WHERE s.vertical = c.vertical AND s.slug = 'default' """ ) # Now we can safely require section_id. op.alter_column("channels", "section_id", nullable=False) # Per-section LLM prompt keys are longer than 64 chars # (`llm_system_prompt:real_estate:some-long-slug`), so widen the key column. op.alter_column( "app_settings", "key", existing_type=sa.String(length=64), type_=sa.String(length=128), existing_nullable=False, ) def downgrade() -> None: op.alter_column( "app_settings", "key", existing_type=sa.String(length=128), type_=sa.String(length=64), existing_nullable=False, ) op.drop_index("ix_channels_section_id", table_name="channels") op.drop_constraint("fk_channels_section", "channels", type_="foreignkey") op.drop_column("channels", "section_id") op.drop_index("ix_sections_vertical", table_name="sections") op.drop_table("sections")