"""finance_budgets — the finance domain's annual spend budget (USD). Revision ID: 304 Revises: 303 Create Date: 2026-06-18 PR2 of the finance closed-loop theme: a budget the materialised actuals (``finance_spend_facts``) are measured against, scoped by an optional ``(fiscal_year, cost_type, department, cost_center)`` tuple (any dimension NULL = "all"). Distinct from ``procurement_budgets`` (forward PO-commitment locking). Standalone table, no FK — same rebuildable-projection rationale as the sibling finance tables, and dodges TiDB CHAR/VARCHAR FK strictness. Idempotent / re-runnable (guards on table presence). """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "304" down_revision = "303" branch_labels = None depends_on = None TABLE = "finance_budgets" def _table_exists(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if _table_exists(bind, TABLE): return op.create_table( TABLE, sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("fiscal_year", sa.Integer(), nullable=False), sa.Column("cost_type", sa.String(length=32), nullable=True), sa.Column("department", sa.String(length=128), nullable=True), sa.Column("cost_center", sa.String(length=64), nullable=True), sa.Column("amount_usd", sa.Numeric(precision=16, scale=2), nullable=False), sa.Column("owner_person_id", sa.String(length=36), nullable=True), sa.Column("note", sa.Text(), nullable=True), sa.Column( "last_alerted_threshold", sa.Integer(), server_default="0", nullable=False, ), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False, ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "fiscal_year", "cost_type", "department", "cost_center", name="uq_finance_budget_scope", ), mysql_engine="InnoDB", mysql_charset="utf8mb4", mysql_collate="utf8mb4_unicode_ci", ) op.create_index("ix_finance_budgets_year", TABLE, ["fiscal_year"]) def downgrade() -> None: bind = op.get_bind() if _table_exists(bind, TABLE): op.drop_table(TABLE)