diff --git a/backend/alembic/versions/003_add_superseded.py b/backend/alembic/versions/003_add_superseded.py new file mode 100644 index 0000000..5e047cd --- /dev/null +++ b/backend/alembic/versions/003_add_superseded.py @@ -0,0 +1,55 @@ +"""Add superseded column to downloads + +Revision ID: 003 +Revises: 002 +Create Date: 2026-03-18 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = '003' +down_revision: Union[str, None] = '002' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add superseded column with default False + op.add_column( + 'downloads', + sa.Column('superseded', sa.Boolean(), nullable=False, server_default='false') + ) + + # Backfill: mark FAILED downloads as superseded where a later COMPLETED + # download exists for the same source. REQUIRED — without this, all + # previously-superseded failures reappear in the dashboard immediately. + op.execute(""" + UPDATE downloads d + SET superseded = TRUE + WHERE d.status = 'failed' + AND EXISTS ( + SELECT 1 FROM downloads d2 + WHERE d2.source_id = d.source_id + AND d2.status = 'completed' + AND d2.created_at > d.created_at + ) + """) + + # Partial index covering the dominant query pattern: status + superseded = FALSE. + # A single boolean column index would be ignored in favour of status indexes; + # this form matches the WHERE clauses at all three affected query sites. + op.create_index( + 'idx_downloads_not_superseded', + 'downloads', + ['status'], + postgresql_where=sa.text('superseded = FALSE') + ) + + +def downgrade() -> None: + op.drop_index('idx_downloads_not_superseded', table_name='downloads') + op.drop_column('downloads', 'superseded')