"""Phase 9: Ownership & Revalidation Queue Revision ID: b035ownerq Revises: b034dlm Create Date: 2026-05-19 Uses raw SQL for all DDL to avoid SQLAlchemy before_create hook issues with existing enum types. """ from typing import Union from alembic import op import sqlalchemy as sa revision: str = "b035ownerq" down_revision: Union[str, None] = "b034dlm" branch_labels = None depends_on = None def upgrade() -> None: conn = op.get_bind() # ── Enums (idempotent) ──────────────────────────────────────────────────── conn.execute(sa.text(""" DO $$ BEGIN CREATE TYPE queue_priority AS ENUM ('critical', 'high', 'medium', 'low'); EXCEPTION WHEN duplicate_object THEN NULL; END $$ """)) conn.execute(sa.text(""" DO $$ BEGIN CREATE TYPE queue_status AS ENUM ('pending', 'in_progress', 'completed', 'dismissed'); EXCEPTION WHEN duplicate_object THEN NULL; END $$ """)) conn.execute(sa.text(""" 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 ────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS technique_ownerships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), technique_id UUID NOT NULL UNIQUE REFERENCES techniques(id) ON DELETE CASCADE, owner_id UUID REFERENCES users(id) ON DELETE SET NULL, backup_owner_id UUID REFERENCES users(id) ON DELETE SET NULL, team VARCHAR(200), notes TEXT, assigned_at TIMESTAMP, assigned_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() ) """)) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_techown_owner_id ON technique_ownerships (owner_id)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_techown_technique_id ON technique_ownerships (technique_id)" )) # ── revalidation_queue_items ────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS revalidation_queue_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), technique_id UUID REFERENCES techniques(id) ON DELETE CASCADE, detection_asset_id UUID REFERENCES detection_assets(id) ON DELETE CASCADE, priority queue_priority NOT NULL DEFAULT 'medium', reason queue_reason NOT NULL, reason_detail TEXT, status queue_status NOT NULL DEFAULT 'pending', assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, due_date TIMESTAMP, created_at TIMESTAMP DEFAULT now(), completed_at TIMESTAMP, dismissed_at TIMESTAMP, completed_by UUID REFERENCES users(id) ON DELETE SET NULL, extra JSONB ) """)) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_rqueue_status ON revalidation_queue_items (status)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_rqueue_priority ON revalidation_queue_items (priority)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_rqueue_assigned_to ON revalidation_queue_items (assigned_to)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_rqueue_technique_id ON revalidation_queue_items (technique_id)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_rqueue_asset_id ON revalidation_queue_items (detection_asset_id)" )) def downgrade() -> None: conn = op.get_bind() conn.execute(sa.text("DROP TABLE IF EXISTS revalidation_queue_items")) conn.execute(sa.text("DROP TABLE IF EXISTS technique_ownerships")) conn.execute(sa.text("DROP TYPE IF EXISTS queue_reason")) conn.execute(sa.text("DROP TYPE IF EXISTS queue_status")) conn.execute(sa.text("DROP TYPE IF EXISTS queue_priority"))