diff --git a/backend/alembic/versions/4d58345da04a_lowercase_user_emails.py b/backend/alembic/versions/4d58345da04a_lowercase_user_emails.py index 5434feafb..8ca9b1460 100644 --- a/backend/alembic/versions/4d58345da04a_lowercase_user_emails.py +++ b/backend/alembic/versions/4d58345da04a_lowercase_user_emails.py @@ -5,7 +5,10 @@ Revises: f1ca58b2f2ec Create Date: 2025-01-29 07:48:46.784041 """ +import logging +from typing import cast from alembic import op +from sqlalchemy.exc import IntegrityError from sqlalchemy.sql import text @@ -15,21 +18,45 @@ down_revision = "f1ca58b2f2ec" branch_labels = None depends_on = None +logger = logging.getLogger("alembic.runtime.migration") + def upgrade() -> None: - # Get database connection + """Conflicts on lowercasing will result in the uppercased email getting a + unique integer suffix when converted to lowercase.""" + connection = op.get_bind() - # Update all user emails to lowercase - connection.execute( - text( - """ - UPDATE "user" - SET email = LOWER(email) - WHERE email != LOWER(email) - """ - ) - ) + # Fetch all user emails that are not already lowercase + user_emails = connection.execute( + text('SELECT id, email FROM "user" WHERE email != LOWER(email)') + ).fetchall() + + for user_id, email in user_emails: + email = cast(str, email) + username, domain = email.rsplit("@", 1) + new_email = f"{username.lower()}@{domain.lower()}" + attempt = 1 + + while True: + try: + # Try updating the email + connection.execute( + text('UPDATE "user" SET email = :new_email WHERE id = :user_id'), + {"new_email": new_email, "user_id": user_id}, + ) + break # Success, exit loop + except IntegrityError: + next_email = f"{username.lower()}_{attempt}@{domain.lower()}" + # Email conflict occurred, append `_1`, `_2`, etc., to the username + logger.warning( + f"Conflict while lowercasing email: " + f"old_email={email} " + f"conflicting_email={new_email} " + f"next_email={next_email}" + ) + new_email = next_email + attempt += 1 def downgrade() -> None: