"""climate record: TimescaleDB hypertables

Moves the raw daily climate record out of per-cell parquet files and into
Postgres, so the app queries the database instead of the filesystem (see
data/climate_store.py). Three objects:

* ``climate_history`` — a TimescaleDB hypertable of the full daily archive,
  durable/LOGGED (a 45-year, rate-limited refetch is expensive). Range-partitioned
  on ``date`` with a large 5-year chunk interval (the access pattern is
  all-rows-for-one-cell, so time chunking is incidental — a big interval keeps the
  chunk count tiny). Old chunks compress; the recent chunk stays writable for the
  hourly tail top-up.
* ``climate_recent`` — a plain table (NOT a hypertable) for the recent+forecast
  bundle. It holds *future* dates and is fully rewritten hourly, both of which
  fight time-partitioning.
* ``climate_sync`` — per-cell freshness (epoch seconds), replacing the parquet
  file mtimes that drove the topup cadence, the forecast TTL, and the
  ``recent_stamp`` token.

Postgres-only: guarded to no-op on the SQLite bind (tests / CI / offline Alembic),
where climate.py uses the parquet backend and these tables are never needed.

Revision ID: 0002_climate_hypertables
Revises: 0001_initial_schema
Create Date: 2026-07-20
"""
from alembic import op

revision = "0002_climate_hypertables"
down_revision = "0001_initial_schema"
branch_labels = None
depends_on = None


# The daily measurement columns shared by both tables (see data/climate_store.COLS).
_MEASURES = """
    tmax   DOUBLE PRECISION,
    tmin   DOUBLE PRECISION,
    precip DOUBLE PRECISION,
    wind   DOUBLE PRECISION,
    gust   DOUBLE PRECISION,
    humid  DOUBLE PRECISION,
    fmax   DOUBLE PRECISION,
    fmin   DOUBLE PRECISION,
    feels  DOUBLE PRECISION
"""


def upgrade() -> None:
    if op.get_bind().dialect.name != "postgresql":
        return  # SQLite fallback (tests/CI): climate uses the parquet backend.

    op.execute("CREATE EXTENSION IF NOT EXISTS timescaledb")

    # History: durable hypertable. PK (cell_id, date) includes the range-partition
    # column (date), which TimescaleDB requires of any unique index, and gives the
    # single-cell chronological scan the read path needs for free.
    op.execute(f"""
        CREATE TABLE climate_history (
            cell_id TEXT NOT NULL,
            date    DATE NOT NULL,
            {_MEASURES},
            PRIMARY KEY (cell_id, date)
        )
    """)
    op.execute(
        "SELECT create_hypertable('climate_history', "
        "by_range('date', INTERVAL '5 years'))")

    # Columnar compression, grouped per cell. Only chunks older than a year are
    # compressed — the hourly tail top-up only ever writes dates within the last
    # week, i.e. the current uncompressed chunk, so compression never collides with
    # a write.
    op.execute("""
        ALTER TABLE climate_history SET (
            timescaledb.compress,
            timescaledb.compress_segmentby = 'cell_id',
            timescaledb.compress_orderby   = 'date'
        )
    """)
    op.execute("SELECT add_compression_policy('climate_history', INTERVAL '365 days')")

    # Recent + forecast bundle: plain table (holds future dates, rewritten hourly).
    op.execute(f"""
        CREATE TABLE climate_recent (
            cell_id TEXT NOT NULL,
            date    DATE NOT NULL,
            {_MEASURES},
            PRIMARY KEY (cell_id, date)
        )
    """)

    # Per-cell freshness — epoch seconds (DOUBLE mirrors store.py's updated_at and
    # makes recent_stamp = int(recent_synced_at) trivial).
    op.execute("""
        CREATE TABLE climate_sync (
            cell_id           TEXT PRIMARY KEY,
            history_synced_at DOUBLE PRECISION,
            recent_synced_at  DOUBLE PRECISION,
            cols_version      SMALLINT NOT NULL DEFAULT 1
        )
    """)


def downgrade() -> None:
    if op.get_bind().dialect.name != "postgresql":
        return
    op.execute("DROP TABLE IF EXISTS climate_sync")
    op.execute("DROP TABLE IF EXISTS climate_recent")
    op.execute("DROP TABLE IF EXISTS climate_history")
    # Leave the timescaledb extension installed — other objects may rely on it and
    # dropping an extension is a heavier, rarely-wanted operation.
