"""project-scoped rules Revision ID: 0059 Revises: 0058 Create Date: 2026-06-01 Rules can now belong to either a rulebook topic (cross-project standard) or a single project (project-scoped). Adds `rules.project_id`, makes `topic_id` nullable, and adds a CHECK constraint enforcing exactly-one. The previous unique constraint on (topic_id, title) still applies because PostgreSQL treats NULL as distinct — two project-scoped rules with the same title and NULL topic_id remain unique. """ from alembic import op import sqlalchemy as sa revision = "0059" down_revision = "0058" branch_labels = None depends_on = None def upgrade() -> None: op.add_column( "rules", sa.Column( "project_id", sa.BigInteger(), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=True, ), ) op.alter_column("rules", "topic_id", nullable=True) op.create_index("ix_rules_project_id", "rules", ["project_id"]) op.create_check_constraint( "ck_rule_topic_xor_project", "rules", "(topic_id IS NULL) <> (project_id IS NULL)", ) def downgrade() -> None: op.drop_constraint("ck_rule_topic_xor_project", "rules", type_="check") op.drop_index("ix_rules_project_id", table_name="rules") # Any rule with NULL topic_id will block re-tightening. Operator must # migrate or delete project-scoped rules before downgrading. op.alter_column("rules", "topic_id", nullable=False) op.drop_column("rules", "project_id")