"""comfort_pins — the shelf you fall back to when you cannot decide Revision ID: 0083_comfort_pins Revises: 0082_invite_lang Create Date: 2026-09-06 E24's one new table. The shelf holds five different things — a video, a YouTube channel, a Plex show, a Plex movie, a playlist — because comfort media is usually "put on X", a series or a channel, and almost never one particular video. Three shapes were possible and two are wrong here: - a boolean on `video_states` / `plex_states` cannot ORDER a shelf, and has nowhere to put a channel or a show (neither has a per-user state row at all); - a polymorphic `(kind, ref)` string pair orders and holds anything, but drops the foreign key — and a Plex item removed from the library, or a channel the user unsubscribes and prunes, would then leave a dead tile on the one surface whose entire job is to be reliable when nothing else is. So: one nullable CASCADE-ing FK per kind, plus `num_nonnulls(...) = 1`. The union invariant lives in the schema rather than in a comment at every insert site, which is the same rule that the `cancelled`/`canceled` bug taught this codebase. NULLs are DISTINCT in a Postgres unique index, so the five per-kind `(user_id, )` uniques each stop a double-pin without colliding with the other four kinds' NULL rows. No backfill: an empty shelf is the honest state for every existing account. The surface renders its own empty state and invites the first pin. """ import sqlalchemy as sa from alembic import op revision: str = "0083_comfort_pins" down_revision: str | None = "0082_invite_lang" branch_labels: str | None = None depends_on: str | None = None def upgrade() -> None: op.create_table( "comfort_pins", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column("video_id", sa.String(length=16), sa.ForeignKey("videos.id", ondelete="CASCADE")), sa.Column( "channel_id", sa.String(length=32), sa.ForeignKey("channels.id", ondelete="CASCADE") ), sa.Column("plex_item_id", sa.Integer(), sa.ForeignKey("plex_items.id", ondelete="CASCADE")), sa.Column("plex_show_id", sa.Integer(), sa.ForeignKey("plex_shows.id", ondelete="CASCADE")), sa.Column("playlist_id", sa.Integer(), sa.ForeignKey("playlists.id", ondelete="CASCADE")), sa.Column("position", sa.Integer(), nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.CheckConstraint( "num_nonnulls(video_id, channel_id, plex_item_id, plex_show_id, playlist_id) = 1", name="ck_comfort_pin_one_ref", ), sa.UniqueConstraint("user_id", "video_id", name="uq_comfort_pin_video"), sa.UniqueConstraint("user_id", "channel_id", name="uq_comfort_pin_channel"), sa.UniqueConstraint("user_id", "plex_item_id", name="uq_comfort_pin_plex_item"), sa.UniqueConstraint("user_id", "plex_show_id", name="uq_comfort_pin_plex_show"), sa.UniqueConstraint("user_id", "playlist_id", name="uq_comfort_pin_playlist"), ) op.create_index("ix_comfort_pins_user_id", "comfort_pins", ["user_id"]) def downgrade() -> None: op.drop_index("ix_comfort_pins_user_id", table_name="comfort_pins") op.drop_table("comfort_pins")