"""add project create idempotency indexes Revision ID: add_project_create_idempotency_indexes Revises: add_work_item_draft_confirmation_fields Create Date: 2026-03-24 """ from __future__ import annotations from alembic import op import sqlalchemy as sa revision = "add_project_create_idempotency_indexes" down_revision = "add_work_item_draft_confirmation_fields" branch_labels = None depends_on = None def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def _duplicate_active_titles(bind) -> list[tuple[int, str, int]]: rows = bind.execute( sa.text( """ SELECT tenant_id, lower(coalesce(nullif(title, ''), nullif(name, ''))) AS normalized_title, count(*) AS duplicate_count FROM projects WHERE status <> 'inactive' AND coalesce(nullif(title, ''), nullif(name, '')) IS NOT NULL GROUP BY tenant_id, lower(coalesce(nullif(title, ''), nullif(name, ''))) HAVING count(*) > 1 ORDER BY duplicate_count DESC, tenant_id ASC LIMIT 10 """ ) ).fetchall() return [ (int(row[0]), str(row[1] or ""), int(row[2])) for row in rows ] def upgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "projects"): return duplicates = _duplicate_active_titles(bind) if duplicates: sample = ", ".join( f"tenant={tenant_id}, title={title}, count={count}" for tenant_id, title, count in duplicates[:3] ) raise RuntimeError( "cannot create uq_projects_tenant_active_title_ci because duplicate active project titles exist: " + sample ) op.execute( sa.text( """ CREATE UNIQUE INDEX IF NOT EXISTS uq_projects_tenant_active_title_ci ON projects ( tenant_id, lower(coalesce(nullif(title, ''), nullif(name, ''))) ) WHERE status <> 'inactive' AND coalesce(nullif(title, ''), nullif(name, '')) IS NOT NULL """ ) ) def downgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "projects"): return op.execute(sa.text("DROP INDEX IF EXISTS uq_projects_tenant_active_title_ci"))