"""Add ``alert_rules`` JSON column to ``cmdb_cloud_scopes``. Revision ID: 055 Revises: 054 Create Date: 2026-04-23 Per-scope cost alerts — each scope carries a list of rules the billing sweep evaluates against that day's + month-to-date cost. Shape: ``` [ {"type": "daily", "threshold_usd": 100, "cc_person_ids": ["..."], "active": true}, {"type": "monthly", "threshold_usd": 2000, "cc_person_ids": ["..."], "active": true} ] ``` The scope owner + additional ``cc_person_ids`` get Feishu + email when the rule fires. Idempotency is per-(rule-index, day) for daily and per-(rule-index, month) for monthly so repeated sweeps don't re-alert. Simpler than a dedicated ``scope_alerts`` table because rules are almost always <= 4 per scope and never queried cross-scope; JSON column keeps the write + read path trivial. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "055" down_revision = "054" branch_labels = None depends_on = None def _column_exists(bind, table: str, col: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() " "AND TABLE_NAME = :t AND COLUMN_NAME = :c" ), {"t": table, "c": col}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if not _column_exists(bind, "cmdb_cloud_scopes", "alert_rules"): op.add_column( "cmdb_cloud_scopes", sa.Column("alert_rules", sa.JSON, nullable=True), ) def downgrade() -> None: bind = op.get_bind() if _column_exists(bind, "cmdb_cloud_scopes", "alert_rules"): op.drop_column("cmdb_cloud_scopes", "alert_rules")