"""add debug_sessions + debug_screenshots, and index log_events.debug_session_id (E16) Revision ID: 0089_debug_sessions Revises: 0088_channels_feed_fallback_at Create Date: 2026-09-19 A debug session is one recording: Start → reproduce → Stop (app/debugsession.py). Every log line of that user's requests while it runs carries its id in `log_events.debug_session_id` — the column E22 created and left NULL — and the issue it ends in is built from exactly those rows, so the column gets its index now. Partial: nearly every row has no session, and an index of NULLs would be most of the table for nothing. At most one OPEN session per user, held by a partial unique index rather than by the start handler alone: two tabs pressing Start at once would otherwise both pass a "none open" check. Screenshots are rows, not files: bounded (size and count per session, app/debugsession.py), gone with their session by cascade, and a self-host has no file volume to configure for them. """ import sqlalchemy as sa from alembic import op revision: str = "0089_debug_sessions" down_revision: str | None = "0088_channels_feed_fallback_at" branch_labels: str | None = None depends_on: str | None = None def upgrade() -> None: op.create_table( "debug_sessions", sa.Column("id", sa.String(16), primary_key=True), sa.Column( "user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ), sa.Column("level", sa.String(8), nullable=False), sa.Column("started_at", sa.DateTime(timezone=True), nullable=False), sa.Column("until", sa.DateTime(timezone=True), nullable=False), sa.Column("ended_at", sa.DateTime(timezone=True), nullable=True), sa.Column("ended_reason", sa.String(16), nullable=True), sa.Column("issue_url", sa.String(512), nullable=True), ) op.create_index( "uq_debug_sessions_open_per_user", "debug_sessions", ["user_id"], unique=True, postgresql_where=sa.text("ended_at IS NULL"), ) op.create_table( "debug_screenshots", sa.Column("id", sa.Integer, primary_key=True), sa.Column( "session_id", sa.String(16), sa.ForeignKey("debug_sessions.id", ondelete="CASCADE"), nullable=False, index=True, ), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("mime", sa.String(16), nullable=False), sa.Column("size", sa.Integer, nullable=False), sa.Column("data", sa.LargeBinary, nullable=False), ) op.create_index( "ix_log_events_debug_session", "log_events", ["debug_session_id"], postgresql_where=sa.text("debug_session_id IS NOT NULL"), ) def downgrade() -> None: op.drop_index("ix_log_events_debug_session", table_name="log_events") op.drop_table("debug_screenshots") op.drop_index("uq_debug_sessions_open_per_user", table_name="debug_sessions") op.drop_table("debug_sessions")