"""lesson_rule_links — a lesson points at the rule it is an instance of (milestone 440 step 1, #4196) Revision ID: 0111 Revises: 0110 Create Date: 2026-10-01 One row per (lesson, rule) pair with a state: `suggested` while evidence accumulates, `confirmed` or `rejected` once a judgment is made. Only a confirmed link changes what surfaces; a rejected one is kept so the pair is not proposed again. CASCADE on both ends — a link to a record that no longer exists says nothing. No backfill: no lesson has ever been linked, and inventing a link would assert a judgment nobody made. """ import sqlalchemy as sa from sqlalchemy.dialects import postgresql from alembic import op revision = "0111" down_revision = "0110" branch_labels = None depends_on = None # One place, so the CHECK and the model's LINK_STATES cannot drift (rule 36: # a new value later means DROP + ADD CONSTRAINT in the same migration). _STATES = ("suggested", "confirmed", "rejected") def upgrade() -> None: op.create_table( "lesson_rule_links", sa.Column("id", sa.BigInteger(), primary_key=True), sa.Column("lesson_id", sa.Integer(), sa.ForeignKey("notes.id", ondelete="CASCADE"), nullable=False), sa.Column("rule_id", sa.BigInteger(), sa.ForeignKey("rules.id", ondelete="CASCADE"), nullable=False), sa.Column("state", sa.Text(), nullable=False, server_default="suggested"), sa.Column("note", sa.Text(), nullable=True), sa.Column("evidence", postgresql.JSONB(), nullable=True), sa.Column("judged_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.UniqueConstraint("lesson_id", "rule_id", name="uq_lesson_rule_links_pair"), ) op.create_check_constraint( "ck_lesson_rule_links_state", "lesson_rule_links", "state IN (" + ", ".join(f"'{s}'" for s in _STATES) + ")", ) op.create_index("ix_lesson_rule_links_lesson_id", "lesson_rule_links", ["lesson_id"]) op.create_index("ix_lesson_rule_links_rule_id", "lesson_rule_links", ["rule_id"]) def downgrade() -> None: op.drop_index("ix_lesson_rule_links_rule_id", table_name="lesson_rule_links") op.drop_index("ix_lesson_rule_links_lesson_id", table_name="lesson_rule_links") op.drop_constraint("ck_lesson_rule_links_state", "lesson_rule_links", type_="check") op.drop_table("lesson_rule_links")