7ce5bb8450
Drift-audit Group 8 (lifecycle gaps): - change_password no longer 500s for OAuth-only users: short-circuit when password_hash is None (verify_password would crash on None) so the route returns a clean 4xx instead of a 500. - register_with_invitation no longer locks the invitee out on a username collision: create the user FIRST, then mark the token used, so a failed creation (409) leaves the single-use invite valid for retry. - update_event re-arms reminder_sent_at when start_dt/reminder_minutes change, so a rescheduled event fires again instead of being permanently suppressed. - Migration 0061: uq_topic_per_rulebook / uq_rule_per_topic become PARTIAL unique indexes (WHERE deleted_at IS NULL). Trashing 'X' then recreating it no longer 500s on the dead row's title. Model __table_args__ updated to match. Deferred: per-occurrence reminders for recurring events (event_scheduler) — needs a per-occurrence reminder-state design, not a one-line gate tweak. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.5 KiB
Python
50 lines
1.5 KiB
Python
"""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"]
|
|
)
|