"""Phase 7S.1E email queue and retry metadata Revision ID: 20260610_phase_7s1e_email_queue Revises: 20260609_phase_7s1c_email_preferences Create Date: 2026-06-10 """ from __future__ import annotations from alembic import op import sqlalchemy as sa revision = "20260610_phase_7s1e_email_queue" down_revision = "20260609_phase_7s1c_email_preferences" branch_labels = None depends_on = None def _existing_columns(table_name: str) -> set[str]: bind = op.get_bind() inspector = sa.inspect(bind) try: return {col["name"] for col in inspector.get_columns(table_name)} except Exception: return set() def _add_column_if_missing(table_name: str, column: sa.Column) -> None: if column.name not in _existing_columns(table_name): op.add_column(table_name, column) def upgrade() -> None: _add_column_if_missing("email_logs", sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0")) _add_column_if_missing("email_logs", sa.Column("max_attempts", sa.Integer(), nullable=False, server_default="3")) _add_column_if_missing("email_logs", sa.Column("queue_priority", sa.Integer(), nullable=False, server_default="100")) _add_column_if_missing("email_logs", sa.Column("is_retryable", sa.Boolean(), nullable=False, server_default=sa.true())) _add_column_if_missing("email_logs", sa.Column("queued_at", sa.DateTime(timezone=True), nullable=True)) _add_column_if_missing("email_logs", sa.Column("next_retry_at", sa.DateTime(timezone=True), nullable=True)) _add_column_if_missing("email_logs", sa.Column("last_attempt_at", sa.DateTime(timezone=True), nullable=True)) _add_column_if_missing("email_logs", sa.Column("processing_started_at", sa.DateTime(timezone=True), nullable=True)) existing_indexes = {idx["name"] for idx in sa.inspect(op.get_bind()).get_indexes("email_logs")} for name, cols in { "ix_email_logs_queue_priority": ["queue_priority"], "ix_email_logs_is_retryable": ["is_retryable"], "ix_email_logs_queued_at": ["queued_at"], "ix_email_logs_next_retry_at": ["next_retry_at"], "ix_email_logs_last_attempt_at": ["last_attempt_at"], "ix_email_logs_processing_started_at": ["processing_started_at"], }.items(): if name not in existing_indexes: op.create_index(name, "email_logs", cols) def downgrade() -> None: # Safe downgrade for development only. SQLite may not support all drop-column # operations depending on version, so production rollback should use backups. for name in [ "ix_email_logs_processing_started_at", "ix_email_logs_last_attempt_at", "ix_email_logs_next_retry_at", "ix_email_logs_queued_at", "ix_email_logs_is_retryable", "ix_email_logs_queue_priority", ]: try: op.drop_index(name, table_name="email_logs") except Exception: pass for col in [ "processing_started_at", "last_attempt_at", "next_retry_at", "queued_at", "is_retryable", "queue_priority", "max_attempts", "attempt_count", ]: try: op.drop_column("email_logs", col) except Exception: pass