"""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 upgrade() -> None: # ── Enums (idempotent via DO/EXCEPTION) ────────────────────────────────── op.execute(""" DO $$ BEGIN CREATE TYPE queue_priority AS ENUM ('critical', 'high', 'medium', 'low'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) op.execute(""" DO $$ BEGIN CREATE TYPE queue_status AS ENUM ('pending', 'in_progress', 'completed', 'dismissed'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) op.execute(""" DO $$ BEGIN CREATE TYPE queue_reason AS ENUM ( 'validation_expired', 'infra_change', 'osint_alert', 'mitre_update', 'rule_modified', 'low_confidence', 'manual'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; """) # ── 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")