"""case-insensitive usernames (B-57) Revision ID: c1d4a97b5e10 Revises: be71fdac734e Create Date: 2026-08-03 18:10:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = 'c1d4a97b5e10' down_revision: Union[str, Sequence[str], None] = 'be71fdac734e' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" # The index cannot be created while two accounts differ only by case, and # nothing here may guess which of them is the "real" one: both are custodial # accounts that may hold funds, so merging or renaming one automatically would # be the migration silently deciding who owns what. Fail loudly instead, naming # the collisions, and let the operator rename one account (and tell that user) # before retrying. The container runs `alembic upgrade head` at startup, so this # surfaces as a refusal to start rather than as a half-applied schema. collisions = op.get_bind().exec_driver_sql( "SELECT group_concat(username, ', ') FROM users " "GROUP BY lower(username) HAVING count(*) > 1" ).fetchall() if collisions: groups = "; ".join(row[0] for row in collisions) raise RuntimeError( "cannot enforce case-insensitive usernames: these accounts differ only " f"by case and must be resolved by hand first — {groups}" ) op.create_index("ix_users_username_lower", "users", [sa.text("lower(username)")], unique=True) def downgrade() -> None: """Downgrade schema.""" op.drop_index("ix_users_username_lower", table_name="users")