-- earth-now.co -- Author: Simon-Pierre Boucher -- Contact: contact@spboucher.ai -- File: infra/migrations/001_initial.sql -- Purpose: Initial TimescaleDB schema — observations (hypertable), counter_models, registry sync, share tokens, ingest audit CREATE EXTENSION IF NOT EXISTS timescaledb; -- --------------------------------------------------------------------------- -- observations — APPEND-ONLY. -- Normalized ingested data points. Rows are NEVER updated or deleted: a -- correction from a source is a new row (later ingested_at wins at read time). -- This is what makes every counter value traceable and re-fittable forever. -- --------------------------------------------------------------------------- CREATE TABLE observations ( id bigserial, metric_id text NOT NULL, source_id text NOT NULL, observed_time timestamptz NOT NULL, value double precision NOT NULL, ingested_at timestamptz NOT NULL DEFAULT now(), raw_ref text, -- Hypertables require the partitioning column in every unique constraint. PRIMARY KEY (id, observed_time) ); SELECT create_hypertable('observations', 'observed_time'); CREATE INDEX observations_metric_time_idx ON observations (metric_id, observed_time DESC); CREATE INDEX observations_source_time_idx ON observations (source_id, observed_time DESC); -- --------------------------------------------------------------------------- -- counter_models — versioned, NEVER UPDATEd. -- Every re-fit inserts a NEW row; `deployed` marks the row currently served. -- Deploying a model = insert new row with deployed = true (the previous -- deployed row is superseded by fitted_at ordering, not mutated). -- --------------------------------------------------------------------------- CREATE TABLE counter_models ( id bigserial PRIMARY KEY, metric_id text NOT NULL, model_version text NOT NULL, model jsonb NOT NULL, fitted_at timestamptz NOT NULL DEFAULT now(), deployed boolean NOT NULL DEFAULT false ); CREATE INDEX counter_models_metric_fitted_idx ON counter_models (metric_id, fitted_at DESC); CREATE INDEX counter_models_deployed_idx ON counter_models (metric_id) WHERE deployed; -- --------------------------------------------------------------------------- -- metrics_registry_sync — last synced state of packages/registry per metric. -- --------------------------------------------------------------------------- CREATE TABLE metrics_registry_sync ( metric_id text PRIMARY KEY, yaml_hash text NOT NULL, synced_at timestamptz NOT NULL DEFAULT now() ); -- --------------------------------------------------------------------------- -- share_tokens — revocable tokens for /m/:token pages (per-metric visibility). -- Revocation = set revoked_at (kept for audit, never hard-deleted). -- --------------------------------------------------------------------------- CREATE TABLE share_tokens ( token text PRIMARY KEY, metric_ids text[] NOT NULL, revoked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); -- --------------------------------------------------------------------------- -- ingest_runs — full auditability of every ingestion run (success or failure). -- --------------------------------------------------------------------------- CREATE TABLE ingest_runs ( id bigserial PRIMARY KEY, source_id text NOT NULL, started_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz, status text NOT NULL DEFAULT 'running', error text, raw_ref text, checksum text ); CREATE INDEX ingest_runs_source_started_idx ON ingest_runs (source_id, started_at DESC);