"""Phase 9: Ownership & Revalidation Queue Revision ID: b035ownerq Revises: b034dlm Create Date: 2026-05-19 """ from typing import Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = "b035ownerq" down_revision: Union[str, None] = "b034dlm" branch_labels = None depends_on = None def _table_exists(table_name: str) -> bool: conn = op.get_bind() return conn.dialect.has_table(conn, table_name) def _column_exists(table_name: str, column_name: str) -> bool: conn = op.get_bind() insp = sa.inspect(conn) cols = [c["name"] for c in insp.get_columns(table_name)] return column_name in cols def _enum_exists(enum_name: str) -> bool: conn = op.get_bind() result = conn.execute( sa.text("SELECT 1 FROM pg_type WHERE typname = :name"), {"name": enum_name} ).fetchone() return result is not None def upgrade() -> None: # ── Enums ──────────────────────────────────────────────────────────────── if not _enum_exists("queue_priority"): op.execute("CREATE TYPE queue_priority AS ENUM ('critical', 'high', 'medium', 'low')") if not _enum_exists("queue_status"): op.execute("CREATE TYPE queue_status AS ENUM ('pending', 'in_progress', 'completed', 'dismissed')") if not _enum_exists("queue_reason"): op.execute( "CREATE TYPE queue_reason AS ENUM (" "'validation_expired', 'infra_change', 'osint_alert', " "'mitre_update', 'rule_modified', 'low_confidence', 'manual')" ) # ── technique_ownerships ───────────────────────────────────────────────── if not _table_exists("technique_ownerships"): op.create_table( "technique_ownerships", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("technique_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("techniques.id", ondelete="CASCADE"), nullable=False, unique=True), sa.Column("owner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("backup_owner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("team", sa.String(200), nullable=True), sa.Column("notes", sa.Text, nullable=True), sa.Column("assigned_at", sa.DateTime, nullable=True), sa.Column("assigned_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("created_at", sa.DateTime, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now()), ) op.create_index("ix_techown_owner_id", "technique_ownerships", ["owner_id"]) op.create_index("ix_techown_technique_id", "technique_ownerships", ["technique_id"]) # ── revalidation_queue_items ────────────────────────────────────────────── if not _table_exists("revalidation_queue_items"): op.create_table( "revalidation_queue_items", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("technique_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("techniques.id", ondelete="CASCADE"), nullable=True), sa.Column("detection_asset_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("detection_assets.id", ondelete="CASCADE"), nullable=True), sa.Column("priority", sa.Enum("critical", "high", "medium", "low", name="queue_priority", create_type=False), nullable=False, server_default="medium"), sa.Column("reason", sa.Enum("validation_expired", "infra_change", "osint_alert", "mitre_update", "rule_modified", "low_confidence", "manual", name="queue_reason", create_type=False), nullable=False), sa.Column("reason_detail", sa.Text, nullable=True), sa.Column("status", sa.Enum("pending", "in_progress", "completed", "dismissed", name="queue_status", create_type=False), nullable=False, server_default="pending"), sa.Column("assigned_to", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("due_date", sa.DateTime, nullable=True), sa.Column("created_at", sa.DateTime, server_default=sa.func.now()), sa.Column("completed_at", sa.DateTime, nullable=True), sa.Column("dismissed_at", sa.DateTime, nullable=True), sa.Column("completed_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("extra", postgresql.JSONB, nullable=True), ) op.create_index("ix_rqueue_status", "revalidation_queue_items", ["status"]) op.create_index("ix_rqueue_priority", "revalidation_queue_items", ["priority"]) op.create_index("ix_rqueue_assigned_to", "revalidation_queue_items", ["assigned_to"]) op.create_index("ix_rqueue_technique_id", "revalidation_queue_items", ["technique_id"]) op.create_index("ix_rqueue_asset_id", "revalidation_queue_items", ["detection_asset_id"]) def downgrade() -> None: op.drop_table("revalidation_queue_items") op.drop_table("technique_ownerships") op.execute("DROP TYPE IF EXISTS queue_reason") op.execute("DROP TYPE IF EXISTS queue_status") op.execute("DROP TYPE IF EXISTS queue_priority")