"""unique name constraints on musicians and sources Revision ID: e5f6a2b3c4d1 Revises: d7e3b19f0a2c Create Date: 2026-06-09 """ from alembic import op import sqlalchemy as sa from sqlalchemy.sql import text revision = "e5f6a2b3c4d1" down_revision = "d7e3b19f0a2c" branch_labels = None depends_on = None def _dedup_musicians(conn): """Keep the lowest-id musician for each name; re-point FKs and delete dupes.""" rows = conn.execute(text( "SELECT MIN(id) AS keep_id, LOWER(name) AS lname " "FROM musicians GROUP BY LOWER(name) HAVING COUNT(*) > 1" )).fetchall() for row in rows: keep_id = row[0] lname = row[1] dupe_ids = [r[0] for r in conn.execute( text("SELECT id FROM musicians WHERE LOWER(name) = :n AND id != :k"), {"n": lname, "k": keep_id} ).fetchall()] for dupe_id in dupe_ids: # Re-point learned_from_id in tune_by_instrument conn.execute(text( "UPDATE tune_by_instrument SET learned_from_id = :k " "WHERE learned_from_id = :d" ), {"k": keep_id, "d": dupe_id}) # Re-point reference_musicians (skip if keep_id row already exists for that ref) conn.execute(text( "DELETE FROM reference_musicians WHERE musician_id = :d " "AND reference_id IN (" " SELECT reference_id FROM reference_musicians WHERE musician_id = :k" ")" ), {"d": dupe_id, "k": keep_id}) conn.execute(text( "UPDATE reference_musicians SET musician_id = :k WHERE musician_id = :d" ), {"k": keep_id, "d": dupe_id}) conn.execute(text("DELETE FROM musicians WHERE id = :d"), {"d": dupe_id}) def _dedup_sources(conn): """Keep the lowest-id source for each name; re-point tunes.source_id and delete dupes.""" rows = conn.execute(text( "SELECT MIN(id) AS keep_id, LOWER(name) AS lname " "FROM sources GROUP BY LOWER(name) HAVING COUNT(*) > 1" )).fetchall() for row in rows: keep_id = row[0] lname = row[1] dupe_ids = [r[0] for r in conn.execute( text("SELECT id FROM sources WHERE LOWER(name) = :n AND id != :k"), {"n": lname, "k": keep_id} ).fetchall()] for dupe_id in dupe_ids: conn.execute(text( "UPDATE tunes SET source_id = :k WHERE source_id = :d" ), {"k": keep_id, "d": dupe_id}) conn.execute(text("DELETE FROM sources WHERE id = :d"), {"d": dupe_id}) def upgrade() -> None: conn = op.get_bind() _dedup_musicians(conn) _dedup_sources(conn) op.create_unique_constraint("uq_musicians_name", "musicians", ["name"]) op.create_unique_constraint("uq_sources_name", "sources", ["name"]) def downgrade() -> None: op.drop_constraint("uq_musicians_name", "musicians", type_="unique") op.drop_constraint("uq_sources_name", "sources", type_="unique")