"""add feishu outbox reply hash Revision ID: add_feishu_outbox_reply_hash Revises: add_ai_pmo_simulation_field Create Date: 2026-03-10 """ from __future__ import annotations from typing import Any from alembic import op import sqlalchemy as sa revision = "add_feishu_outbox_reply_hash" down_revision = "add_ai_pmo_simulation_field" 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 return any(col.get("name") == column_name for col in inspector.get_columns(table_name)) 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 _create_index_if_not_exists( bind, *, index_name: str, table_name: str, columns: list[str], unique: bool = False, postgresql_where: Any = None, sqlite_where: Any = None, ) -> None: if _index_exists(bind, table_name, index_name): return kwargs: dict[str, Any] = {} if postgresql_where is not None: kwargs["postgresql_where"] = postgresql_where if sqlite_where is not None: kwargs["sqlite_where"] = sqlite_where op.create_index(index_name, table_name, columns, unique=unique, **kwargs) 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 not _table_exists(bind, "feishu_reply_outbox"): return if not _column_exists(bind, "feishu_reply_outbox", "source_message_id"): op.add_column("feishu_reply_outbox", sa.Column("source_message_id", sa.String(length=128), nullable=True)) if not _column_exists(bind, "feishu_reply_outbox", "reply_hash"): op.add_column("feishu_reply_outbox", sa.Column("reply_hash", sa.String(length=32), nullable=True)) _create_index_if_not_exists( bind, index_name="ix_feishu_reply_outbox_source_message_id", table_name="feishu_reply_outbox", columns=["source_message_id"], ) _create_index_if_not_exists( bind, index_name="ix_feishu_reply_outbox_reply_hash", table_name="feishu_reply_outbox", columns=["reply_hash"], ) sent_where = sa.text("status = 'sent' AND reply_hash IS NOT NULL") _create_index_if_not_exists( bind, index_name="uq_feishu_reply_outbox_sent_reply_hash", table_name="feishu_reply_outbox", columns=["tenant_id", "reply_hash"], unique=True, postgresql_where=sent_where, sqlite_where=sent_where, ) def downgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "feishu_reply_outbox"): return _drop_index_if_exists(bind, "feishu_reply_outbox", "uq_feishu_reply_outbox_sent_reply_hash") _drop_index_if_exists(bind, "feishu_reply_outbox", "ix_feishu_reply_outbox_reply_hash") _drop_index_if_exists(bind, "feishu_reply_outbox", "ix_feishu_reply_outbox_source_message_id") if _column_exists(bind, "feishu_reply_outbox", "reply_hash"): op.drop_column("feishu_reply_outbox", "reply_hash") if _column_exists(bind, "feishu_reply_outbox", "source_message_id"): op.drop_column("feishu_reply_outbox", "source_message_id")