"""drop the always-on tier: rules.tier, rulebooks.always_on, the exclusions table Revision ID: 0100 Revises: 0099 Create Date: 2026-09-11 Milestone 394. Every rule now reaches a session by retrieval — because something it is about to do made the rule relevant — and the machinery that delivered rules unconditionally goes with it. WHAT GOES, AND WHERE IT CAME FROM - ``rules.tier`` and its ``ck_rules_tier`` CHECK (migration 0088). Dropping the column takes the constraint with it. Rule 36 is about ADDING a value to a live whitelist, which needs DROP + ADD in the same migration; it does not speak to removing the column outright, and saying so here is cheaper than the next reader wondering whether it was forgotten. - ``rule_versions.tier`` (migration 0098). A version records what a rule SAID; with no tier on a rule there is nothing for a snapshot to carry. - ``rulebooks.always_on`` (migration 0058). A rulebook reaches a project by subscription now, and by nothing else. - ``project_rulebook_exclusions`` (migration 0085). It recorded a project's opt-out of an always-on rulebook. Opting out of something that no longer binds you is not a state that can exist — declining a rulebook is expressed by not subscribing to it. IRREVERSIBLE, AND THE DOWNGRADE SAYS SO RATHER THAN PRETENDING The downgrade recreates the columns and the table with their DEFAULTS. It cannot restore WHICH rules were always-on, which rulebooks bound every project, or which projects had opted out — that information is in what this drops. That distinction is the one this repo keeps insisting on: a value invented to fill a hole is not a measurement. So a downgraded database is structurally able to run the old code and is NOT the database the old code was running against — every rule comes back at the ``always_on`` default, which for the tier column happens to mean "binding", the safe direction to be wrong in. Anyone who needs the real prior state restores a backup taken before this ran. """ import sqlalchemy as sa from alembic import op revision = "0100" down_revision = "0099" branch_labels = None depends_on = None _TIERS = ("always_on", "conditional") def _in_list(column: str, values: tuple[str, ...]) -> str: return f"{column} IN (" + ", ".join(f"'{v}'" for v in values) + ")" def upgrade() -> None: op.drop_table("project_rulebook_exclusions") op.drop_column("rulebooks", "always_on") op.drop_column("rule_versions", "tier") # The CHECK goes with the column it constrains; naming it here would be a # second drop of the same object. op.drop_column("rules", "tier") def downgrade() -> None: """Structure only. See the module docstring — the values are gone.""" op.add_column( "rules", sa.Column("tier", sa.Text(), nullable=False, server_default="always_on"), ) op.create_check_constraint("ck_rules_tier", "rules", _in_list("tier", _TIERS)) op.add_column("rule_versions", sa.Column("tier", sa.Text(), nullable=True)) op.add_column( "rulebooks", sa.Column( "always_on", sa.Boolean(), nullable=False, server_default=sa.text("false"), ), ) op.create_table( "project_rulebook_exclusions", sa.Column( "project_id", sa.BigInteger(), sa.ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True, nullable=False, ), sa.Column( "rulebook_id", sa.BigInteger(), sa.ForeignKey("rulebooks.id", ondelete="CASCADE"), primary_key=True, nullable=False, ), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True, ), )