"""Feed Swipe, stan per urządzenie: clip_seen, clip_feed_events, clip_affinity, clip_favorites, clip_blacklist. Plus GIN na clips.niches pod skan po niszy. Uzasadnienie w docstringu `app/models/clip_feed.py`. Revision ID: 0037_clip_feed Revises: 0036_clips Create Date: 2026-09-14 """ from __future__ import annotations import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision = "0037_clip_feed" down_revision: str | None = "0036_clips" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "clip_seen", sa.Column("device_id", sa.String(64), nullable=False), sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("outcome", sa.String(16), nullable=False, server_default="served"), sa.Column("seen_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.ForeignKeyConstraint(["clip_id"], ["clips.id"], name="fk_clip_seen_clip_id_clips", ondelete="CASCADE"), sa.PrimaryKeyConstraint("device_id", "clip_id", name="pk_clip_seen"), ) op.create_table( "clip_feed_events", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("device_id", sa.String(64), nullable=False), sa.Column("session_id", sa.String(32), nullable=True), sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("kind", sa.String(16), nullable=False), sa.Column("dwell_ms", sa.Integer(), nullable=True), sa.Column("reason", sa.String(160), nullable=True), sa.Column("slot", sa.String(12), nullable=True), sa.Column("position", sa.SmallInteger(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.PrimaryKeyConstraint("id", name="pk_clip_feed_events"), ) op.create_index("ix_clip_feed_events_device_created", "clip_feed_events", ["device_id", "created_at"]) op.create_index("ix_clip_feed_events_created", "clip_feed_events", ["created_at"]) op.create_table( "clip_affinity", sa.Column("device_id", sa.String(64), nullable=False), sa.Column("tag_weights", postgresql.JSONB(), nullable=False, server_default="{}"), sa.Column("niche_weights", postgresql.JSONB(), nullable=False, server_default="{}"), sa.Column("creator_weights", postgresql.JSONB(), nullable=False, server_default="{}"), sa.Column("events_seen", sa.Integer(), nullable=False, server_default="0"), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.PrimaryKeyConstraint("device_id", name="pk_clip_affinity"), ) op.create_table( "clip_favorites", sa.Column("device_id", sa.String(64), nullable=False), sa.Column("clip_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.ForeignKeyConstraint(["clip_id"], ["clips.id"], name="fk_clip_favorites_clip_id_clips", ondelete="CASCADE"), sa.PrimaryKeyConstraint("device_id", "clip_id", name="pk_clip_favorites"), ) op.create_table( "clip_blacklist", sa.Column("device_id", sa.String(64), nullable=False), sa.Column("kind", sa.String(8), nullable=False), sa.Column("value", sa.String(128), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.PrimaryKeyConstraint("device_id", "kind", "value", name="pk_clip_blacklist"), ) # Skan kandydatów po niszy (`niches && ARRAY[...]`) bez pełnego przebiegu tabeli. op.execute("CREATE INDEX IF NOT EXISTS ix_clips_niches_gin ON clips USING gin (niches)") def downgrade() -> None: op.execute("DROP INDEX IF EXISTS ix_clips_niches_gin") op.drop_table("clip_blacklist") op.drop_table("clip_favorites") op.drop_table("clip_affinity") op.drop_index("ix_clip_feed_events_created", table_name="clip_feed_events") op.drop_index("ix_clip_feed_events_device_created", table_name="clip_feed_events") op.drop_table("clip_feed_events") op.drop_table("clip_seen")