"""Close the SSO Apps catalog CSV-import race window. Round-2 audit (2026-05-07) flagged a check-then-insert race in ``upsert_app``: two parallel CSV imports sharing the same ``display_name`` could both SELECT-empty, both INSERT, leaving duplicate rows that share a display_name but have different ``id``s. The grant-linking code joins on display_name and would attach to whichever row it found first; the duplicate became orphan catalog junk. The pre-existing UNIQUE on ``(provider, external_id)`` doesn't catch this — admin-added rows have ``external_id=NULL`` and MySQL treats multiple NULLs as distinct. Fix: add a second UNIQUE on ``(provider, display_name)``. Now two parallel inserts race on the DB constraint instead of in app code; the loser raises IntegrityError and the application can re-SELECT to find the winner's row + reuse it. ``upsert_app`` already does SELECT-then-INSERT-OR-UPDATE keyed on this same pair, so the constraint just tightens what the code already assumes. Display-name uniqueness across an admin's catalog is a strong real-world invariant (Google admin's Apps page also rejects duplicates by name). No production rows existed before this PR (migration 126 + 127 + 128 ship together), so no backfill needed. """ from __future__ import annotations from alembic import op # revision identifiers, used by Alembic. revision = "128" down_revision = "127" branch_labels = None depends_on = None def upgrade() -> None: op.create_unique_constraint( "uq_sso_apps_provider_display_name", "sso_apps", ["provider", "display_name"], ) def downgrade() -> None: op.drop_constraint( "uq_sso_apps_provider_display_name", "sso_apps", type_="unique", )