-- 020: WP3 analytics snapshots — monthly per-workspace rollups. -- -- One row per (workspace, month), refreshed in place by the snapshot writer -- (apps/api/src/analytics/snapshot.service.ts) with set-based SQL and an -- idempotent upsert on (workspace_id, month). Months are materialized as -- range partitions of a `month date` key, mirroring 014_live_chat.sql. -- -- Creating future partitions: RLS on the partitioned parent does NOT apply to -- direct leaf access, so every leaf carries its own ENABLE/FORCE RLS and -- tenant_isolation policy. `analytics_ensure_month(date)` creates a month's -- partition idempotently with those controls; this migration precreates the -- previous, current and next month. Administrators run -- SELECT analytics_ensure_month(date '2025-09-01'); -- ahead of each new month (cron it monthly, one statement, no downtime). The -- DEFAULT partition below catches any month whose partition was not created, -- so a missed call never drops data — it only loses pruning until the -- partition is created and the rows moved into it. -- -- Partitioned-leaf register entry (proposal §4.4, exception (a)): PK carries -- the partition key, the table is never the target of an FK, and rows are -- referenced only by (workspace_id, month) / id soft links. -- -- Query catalog (owning WP: WP3 — packages/db does not exist in this repo, so -- the catalog entries live here and in snapshot.service.ts): -- analytics.snapshot.refresh INSERT .. ON CONFLICT (workspace_id, month) — -- served by the PK (workspace_id, month, id); write path, p95 <= 250 ms. -- analytics.snapshot.list keyset page ORDER BY month DESC — same PK, -- backward index scan, p95 <= 20 ms. -- analytics.snapshot.summary two PK point lookups (current + previous -- month), p95 <= 20 ms. CREATE TABLE analytics_snapshots ( id uuid NOT NULL DEFAULT gen_random_uuid(), workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT, -- Always a month start; the CHECK keeps the value aligned with its partition. month date NOT NULL CHECK (month = date_trunc('month', month)::date), publications_total bigint NOT NULL DEFAULT 0 CHECK (publications_total >= 0), -- {provider: count} for publications published in the month. publications_by_provider jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(publications_by_provider) = 'object'), posts_archived_total bigint NOT NULL DEFAULT 0 CHECK (posts_archived_total >= 0), -- Point-in-time current state at snapshot time (not a monthly delta): -- connections not disconnected when the snapshot was taken. active_connections int NOT NULL DEFAULT 0 CHECK (active_connections >= 0), drafts_created_total bigint NOT NULL DEFAULT 0 CHECK (drafts_created_total >= 0), publishing_attempts_total bigint NOT NULL DEFAULT 0 CHECK (publishing_attempts_total >= 0), -- Attempts whose terminal status is 'failed', counted by their creation month. publish_failures_total bigint NOT NULL DEFAULT 0 CHECK (publish_failures_total >= 0), snapshot_started_at timestamptz NOT NULL DEFAULT now(), snapshot_completed_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (workspace_id, month, id), UNIQUE (workspace_id, month) ) PARTITION BY RANGE (month); -- Idempotent: safe to re-run for the same month. CREATE FUNCTION analytics_ensure_month(p_month date) RETURNS void LANGUAGE plpgsql SET search_path = public AS $$ DECLARE v_start date := date_trunc('month', p_month)::date; v_table text := 'analytics_snapshots_' || to_char(v_start, 'YYYYMM'); BEGIN EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF analytics_snapshots FOR VALUES FROM (%L) TO (%L)', v_table, v_start, (v_start + interval '1 month')::date); EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', v_table); EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', v_table); IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = v_table AND policyname = 'tenant_isolation') THEN EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (workspace_id = NULLIF(current_setting(''app.workspace_id'', true), '''')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting(''app.workspace_id'', true), '''')::uuid)', v_table); END IF; END $$; REVOKE ALL ON FUNCTION analytics_ensure_month(date) FROM PUBLIC; SELECT analytics_ensure_month((current_date - interval '1 month')::date); SELECT analytics_ensure_month(current_date); SELECT analytics_ensure_month((current_date + interval '1 month')::date); -- Safety net for months nobody pre-created (see header). CREATE TABLE analytics_snapshots_default PARTITION OF analytics_snapshots DEFAULT; ALTER TABLE analytics_snapshots_default ENABLE ROW LEVEL SECURITY; ALTER TABLE analytics_snapshots_default FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON analytics_snapshots_default USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); CREATE TRIGGER trg_analytics_snapshot_ws_immutable BEFORE UPDATE ON analytics_snapshots FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); ALTER TABLE analytics_snapshots ENABLE ROW LEVEL SECURITY; ALTER TABLE analytics_snapshots FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON analytics_snapshots USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); -- Grants on the parent cover all current and future partitions for queries -- routed through it (014 style); direct leaf access also inherits from the -- owner's default privileges (003). GRANT SELECT, INSERT, UPDATE, DELETE ON analytics_snapshots TO sp_app;