"""C11 upload retry — additive ``upload_attempts`` counter on tiles. Per ``_docs/02_tasks/todo/AZ-320_c11_idempotent_retry.md``. The migration: - adds the ``tiles.upload_attempts INTEGER NOT NULL DEFAULT 0`` column (per-tile retry budget for the C11 ``IdempotentRetryTileUploader``); - widens the ``tiles.voting_status`` CHECK to also accept the new ``upload_giveup`` enum value (terminal state for a tile that exhausted its per-tile retry budget — see ``VotingStatus`` v1.3.0). The migration is strictly additive on ``0002_c6_tile_identity_and_lru``. Existing rows default to ``upload_attempts = 0``; existing voting_status values (``pending``, ``trusted``, ``rejected``) remain valid. Reverse ``downgrade()`` drops the column and restores the legacy CHECK to its v1.2.0 vocabulary; downgrade is destructive for any rows that hold ``upload_giveup`` and is documented operator-only. Revision ID: 0003_c11_upload_attempts Revises: 0002_c6_tile_identity_and_lru Create Date: 2026-05-13 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "0003_c11_upload_attempts" down_revision: str | None = "0002_c6_tile_identity_and_lru" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None _VOTING_STATUS_LEGACY = ( "voting_status IS NULL OR voting_status IN ('pending','trusted','rejected')" ) _VOTING_STATUS_UNION = ( "voting_status IS NULL OR " "voting_status IN ('pending','trusted','rejected','upload_giveup')" ) _VOTING_STATUS_CHECK = "ck_tiles_voting_status" def upgrade() -> None: op.add_column( "tiles", sa.Column( "upload_attempts", sa.Integer(), nullable=False, server_default=sa.text("0"), ), ) op.create_check_constraint( "ck_tiles_upload_attempts_nonneg", "tiles", "upload_attempts >= 0", ) # Voting-status CHECK: drop-and-recreate. AZ-263's 0001 created the # constraint under the legacy vocabulary; widening to include # 'upload_giveup' keeps the constraint as the SQL-level enforcement # of the VotingStatus enum. op.execute(f"ALTER TABLE tiles DROP CONSTRAINT IF EXISTS {_VOTING_STATUS_CHECK}") op.create_check_constraint( _VOTING_STATUS_CHECK, "tiles", _VOTING_STATUS_UNION, ) def downgrade() -> None: op.execute(f"ALTER TABLE tiles DROP CONSTRAINT IF EXISTS {_VOTING_STATUS_CHECK}") op.create_check_constraint( _VOTING_STATUS_CHECK, "tiles", _VOTING_STATUS_LEGACY, ) op.drop_constraint( "ck_tiles_upload_attempts_nonneg", "tiles", type_="check" ) op.drop_column("tiles", "upload_attempts")