"""add inbox structured fields Revision ID: add_inbox_structured_fields Revises: add_ai_orchestration_runs Create Date: 2026-03-08 """ from __future__ import annotations from alembic import op import sqlalchemy as sa revision = "add_inbox_structured_fields" down_revision = "add_ai_orchestration_runs" branch_labels = None depends_on = None def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def _column_exists(bind, table_name: str, column_name: str) -> bool: inspector = sa.inspect(bind) if not inspector.has_table(table_name): return False columns = inspector.get_columns(table_name) return any(col.get("name") == column_name for col in columns) def _index_exists(bind, table_name: str, index_name: str) -> bool: inspector = sa.inspect(bind) if not inspector.has_table(table_name): return False return any(idx.get("name") == index_name for idx in inspector.get_indexes(table_name)) def _add_column_if_missing(bind, table_name: str, column: sa.Column) -> None: if not _column_exists(bind, table_name, str(column.name)): op.add_column(table_name, column) def _drop_column_if_exists(bind, table_name: str, column_name: str) -> None: if _column_exists(bind, table_name, column_name): op.drop_column(table_name, column_name) def _create_index_if_missing(bind, table_name: str, index_name: str, columns: list[str]) -> None: if not _index_exists(bind, table_name, index_name): op.create_index(index_name, table_name, columns) def _drop_index_if_exists(bind, table_name: str, index_name: str) -> None: if _index_exists(bind, table_name, index_name): op.drop_index(index_name, table_name=table_name) def upgrade() -> None: bind = op.get_bind() if _table_exists(bind, "notifications"): _add_column_if_missing(bind, "notifications", sa.Column("kind", sa.String(length=30), nullable=True)) _add_column_if_missing(bind, "notifications", sa.Column("priority", sa.String(length=20), nullable=True)) _add_column_if_missing(bind, "notifications", sa.Column("status", sa.String(length=30), nullable=True)) _add_column_if_missing(bind, "notifications", sa.Column("blocking_reason", sa.Text(), nullable=True)) _add_column_if_missing(bind, "notifications", sa.Column("recommended_action", sa.String(length=80), nullable=True)) _add_column_if_missing(bind, "notifications", sa.Column("dedupe_key", sa.String(length=255), nullable=True)) _add_column_if_missing( bind, "notifications", sa.Column("occurrence_count", sa.Integer(), nullable=False, server_default="1"), ) _add_column_if_missing(bind, "notifications", sa.Column("payload", sa.JSON(), nullable=True)) _create_index_if_missing(bind, "notifications", "ix_notifications_kind", ["kind"]) _create_index_if_missing(bind, "notifications", "ix_notifications_priority", ["priority"]) _create_index_if_missing(bind, "notifications", "ix_notifications_status", ["status"]) _create_index_if_missing(bind, "notifications", "ix_notifications_dedupe_key", ["dedupe_key"]) if _table_exists(bind, "feishu_items"): _add_column_if_missing(bind, "feishu_items", sa.Column("decision_reason", sa.Text(), nullable=True)) _add_column_if_missing(bind, "feishu_items", sa.Column("blocking_reason", sa.Text(), nullable=True)) _add_column_if_missing( bind, "feishu_items", sa.Column("safe_to_auto_execute", sa.Boolean(), nullable=False, server_default=sa.false()), ) _add_column_if_missing( bind, "feishu_items", sa.Column("auto_execute_state", sa.String(length=30), nullable=True), ) _add_column_if_missing(bind, "feishu_items", sa.Column("dedupe_key", sa.String(length=255), nullable=True)) _add_column_if_missing(bind, "feishu_items", sa.Column("message_class", sa.String(length=30), nullable=True)) _add_column_if_missing( bind, "feishu_items", sa.Column("occurrence_count", sa.Integer(), nullable=False, server_default="1"), ) _add_column_if_missing(bind, "feishu_items", sa.Column("next_step_text", sa.Text(), nullable=True)) _add_column_if_missing(bind, "feishu_items", sa.Column("undoable_until", sa.TIMESTAMP(), nullable=True)) _add_column_if_missing(bind, "feishu_items", sa.Column("result_payload", sa.JSON(), nullable=True)) _create_index_if_missing(bind, "feishu_items", "ix_feishu_items_safe_to_auto_execute", ["safe_to_auto_execute"]) _create_index_if_missing(bind, "feishu_items", "ix_feishu_items_auto_execute_state", ["auto_execute_state"]) _create_index_if_missing(bind, "feishu_items", "ix_feishu_items_dedupe_key", ["dedupe_key"]) _create_index_if_missing(bind, "feishu_items", "ix_feishu_items_message_class", ["message_class"]) def downgrade() -> None: bind = op.get_bind() if _table_exists(bind, "notifications"): for index_name in ( "ix_notifications_dedupe_key", "ix_notifications_status", "ix_notifications_priority", "ix_notifications_kind", ): _drop_index_if_exists(bind, "notifications", index_name) for column_name in ( "payload", "occurrence_count", "dedupe_key", "recommended_action", "blocking_reason", "status", "priority", "kind", ): _drop_column_if_exists(bind, "notifications", column_name) if _table_exists(bind, "feishu_items"): for index_name in ( "ix_feishu_items_message_class", "ix_feishu_items_dedupe_key", "ix_feishu_items_auto_execute_state", "ix_feishu_items_safe_to_auto_execute", ): _drop_index_if_exists(bind, "feishu_items", index_name) for column_name in ( "result_payload", "undoable_until", "next_step_text", "occurrence_count", "message_class", "dedupe_key", "auto_execute_state", "safe_to_auto_execute", "blocking_reason", "decision_reason", ): _drop_column_if_exists(bind, "feishu_items", column_name)