"""Durable notification provider-delivery claims. Revision ID: 443 Revises: 442 Create Date: 2026-08-02 #2408: commit a lease and stable provider keys before non-transactional Feishu/SMTP I/O. A stale lease becomes an explicit uncertain outcome instead of entering the automatic retry pool and possibly sending a duplicate. """ from __future__ import annotations import sqlalchemy as sa from alembic import op from app.core.sql_types import GUID revision = "443" down_revision = "442" branch_labels = None depends_on = None def upgrade() -> None: op.alter_column( "notification_outbox", "status", existing_type=sa.String(16), type_=sa.String(24), existing_nullable=False, existing_server_default="pending", ) op.add_column( "notification_outbox", sa.Column("delivery_claim_token", GUID(), nullable=True), ) op.add_column( "notification_outbox", sa.Column("delivery_claimed_at", sa.DateTime(timezone=True), nullable=True), ) op.add_column( "notification_outbox", sa.Column( "delivery_lease_expires_at", sa.DateTime(timezone=True), nullable=True, ), ) op.add_column( "notification_outbox", sa.Column("delivery_claimed_channels", sa.JSON(), nullable=True), ) op.add_column( "notification_outbox", sa.Column( "delivery_claim_count", sa.Integer(), nullable=False, server_default="0", ), ) op.add_column( "notification_outbox", sa.Column("provider_delivery_keys", sa.JSON(), nullable=True), ) op.create_index( "ix_notification_outbox_delivery_lease", "notification_outbox", ["status", "delivery_lease_expires_at"], ) def downgrade() -> None: # The old VARCHAR(16) cannot represent ``delivery_uncertain``. Preserve # safety on rollback: live claims re-enter retry, uncertain outcomes # become dead letters (never an automatic duplicate). op.execute( "UPDATE notification_outbox SET status = 'failed' " "WHERE status = 'delivering'" ) op.execute( "UPDATE notification_outbox " "SET status = 'dead', last_error = CONCAT(" "'migration 443 rollback: delivery outcome remained uncertain | ', " "COALESCE(last_error, '')) " "WHERE status = 'delivery_uncertain'" ) op.drop_index( "ix_notification_outbox_delivery_lease", table_name="notification_outbox", ) op.drop_column("notification_outbox", "provider_delivery_keys") op.drop_column("notification_outbox", "delivery_claim_count") op.drop_column("notification_outbox", "delivery_claimed_channels") op.drop_column("notification_outbox", "delivery_lease_expires_at") op.drop_column("notification_outbox", "delivery_claimed_at") op.drop_column("notification_outbox", "delivery_claim_token") op.alter_column( "notification_outbox", "status", existing_type=sa.String(24), type_=sa.String(16), existing_nullable=False, existing_server_default="pending", )