"""add col_snapshot table for cached cost-of-living data Revision ID: 0005 Revises: 0004 Create Date: 2026-05-21 12:00:00.000000 Phase 2 of the cost-of-living subsystem (`fire_planner.col`). Caches Numbeo / Expatistan headline data with a 1-year TTL so the simulator can scale `spending_gbp` to local prices without re-scraping per-call. Refresh is async (Phase-3 CronJob); user-facing lookups never block on the network in the steady state. Unique on (city_slug, source_name) — multiple sources per city are allowed; service.py reconciles them when computing the headline. """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "0005" down_revision: str | None = "0004" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None SCHEMA = "fire_planner" def upgrade() -> None: op.create_table( "col_snapshot", sa.Column("id", sa.Integer(), nullable=False, autoincrement=True), sa.Column("city_slug", sa.String(length=64), nullable=False), sa.Column("city_display", sa.String(length=128), nullable=False), sa.Column("country", sa.String(length=64), nullable=False), sa.Column("source_name", sa.String(length=32), nullable=False), sa.Column("source_url", sa.String(), nullable=True), sa.Column("snapshot_date", sa.Date(), nullable=False), sa.Column("fetched_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False), sa.Column("total_no_rent_gbp", sa.Numeric(12, 2), nullable=False), sa.Column("total_with_rent_gbp", sa.Numeric(12, 2), nullable=False), sa.Column("rent_1bed_center_gbp", sa.Numeric(12, 2), nullable=False), sa.Column("rent_1bed_outside_gbp", sa.Numeric(12, 2), nullable=True), sa.Column("raw_currency", sa.String(length=3), nullable=False, server_default=sa.text("'GBP'")), sa.Column("gbp_per_unit", sa.Numeric(12, 8), nullable=False, server_default=sa.text("1")), sa.Column("by_category_json", sa.JSON(), nullable=True), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("city_slug", "source_name", name="uq_col_snapshot_city_source"), schema=SCHEMA, ) op.create_index( "ix_col_snapshot_city_slug", "col_snapshot", ["city_slug"], schema=SCHEMA, ) op.create_index( "ix_col_snapshot_expires_at", "col_snapshot", ["expires_at"], schema=SCHEMA, ) def downgrade() -> None: op.drop_index("ix_col_snapshot_expires_at", table_name="col_snapshot", schema=SCHEMA) op.drop_index("ix_col_snapshot_city_slug", table_name="col_snapshot", schema=SCHEMA) op.drop_table("col_snapshot", schema=SCHEMA)