"""partial-unique topic/rule titles (ignore soft-deleted rows) Revision ID: 0061 Revises: 0060 Create Date: 2026-06-02 Topics and rules are soft-deleted (SoftDeleteMixin), but uq_topic_per_rulebook and uq_rule_per_topic were plain UNIQUE constraints. Trashing a topic/rule named "X" then creating a new "X" — or restoring into a reused title slot — collided with the dead row and raised an unhandled 500. Replace the full UNIQUE constraints with partial unique indexes that only consider live (deleted_at IS NULL) rows. """ from alembic import op import sqlalchemy as sa revision = "0061" down_revision = "0060" branch_labels = None depends_on = None def upgrade() -> None: op.drop_constraint("uq_topic_per_rulebook", "rulebook_topics", type_="unique") op.create_index( "uq_topic_per_rulebook", "rulebook_topics", ["rulebook_id", "title"], unique=True, postgresql_where=sa.text("deleted_at IS NULL"), ) op.drop_constraint("uq_rule_per_topic", "rules", type_="unique") op.create_index( "uq_rule_per_topic", "rules", ["topic_id", "title"], unique=True, postgresql_where=sa.text("deleted_at IS NULL"), ) def downgrade() -> None: op.drop_index("uq_rule_per_topic", table_name="rules") op.create_unique_constraint("uq_rule_per_topic", "rules", ["topic_id", "title"]) op.drop_index("uq_topic_per_rulebook", table_name="rulebook_topics") op.create_unique_constraint( "uq_topic_per_rulebook", "rulebook_topics", ["rulebook_id", "title"] )