SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
14.6 KB · 203 lines sql
Raw Blame History
1-- 0003 — claim-first data layer, quality flags, campus containment, capacity ontology, run tracking, snapshots.2-- Idempotent (IF NOT EXISTS everywhere) so it can be re-applied on a database created before this file existed.34-- ─── claims ────────────────────────────────────────────────────────────────────────────────────────5CREATE TABLE IF NOT EXISTS "claims" (6  "id" text PRIMARY KEY,7  "subject_type" text NOT NULL,            -- facility | campus | project | operator | market | country8  "subject_id" text NOT NULL,9  "predicate" text NOT NULL,               -- it_capacity_mw | planned_power_mw | project_investment_usd | status | …10  "value" double precision,11  "value_text" text,12  "unit" text,                             -- MW | USD | …13  "scope" text NOT NULL DEFAULT 'unknown', -- building | facility | campus | metro | country | portfolio | company | unknown14  "scope_reason" text,15  "source_id" text NOT NULL,16  "connector_id" text NOT NULL,17  "document_id" text,18  "url" text NOT NULL,19  "published_at" text,                     -- partial date as published20  "retrieved_at" timestamp with time zone NOT NULL DEFAULT now(),21  "confidence" text NOT NULL DEFAULT 'moderate',22  "is_estimate" boolean NOT NULL DEFAULT false,23  "authority_tier" text NOT NULL DEFAULT 'D',24  "evidence_text" text,25  "evidence_start" integer,26  "evidence_end" integer,27  "parser_name" text,28  "parser_version" text,29  "run_id" text,30  "status" text NOT NULL DEFAULT 'current', -- current | superseded | rejected | review | unscoped31  "rejection_reason" text,32  "first_observed" timestamp with time zone NOT NULL DEFAULT now(),33  "last_observed" timestamp with time zone NOT NULL DEFAULT now(),34  "created_at" timestamp with time zone NOT NULL DEFAULT now()35);36CREATE UNIQUE INDEX IF NOT EXISTS "claims_uq" ON "claims" ("subject_type", "subject_id", "predicate", "source_id", "url", COALESCE("value", 0), COALESCE("value_text", ''));37CREATE INDEX IF NOT EXISTS "claims_subject_idx" ON "claims" ("subject_type", "subject_id", "predicate");38CREATE INDEX IF NOT EXISTS "claims_status_idx" ON "claims" ("status");39CREATE INDEX IF NOT EXISTS "claims_run_idx" ON "claims" ("run_id");40CREATE INDEX IF NOT EXISTS "claims_document_idx" ON "claims" ("document_id");4142-- ─── quality flags ─────────────────────────────────────────────────────────────────────────────────43CREATE TABLE IF NOT EXISTS "quality_flags" (44  "id" text PRIMARY KEY,45  "entity_type" text NOT NULL,46  "entity_id" text NOT NULL,47  "claim_id" text,48  "code" text NOT NULL,                    -- mw_single_site_gt_1000 | scope_company | inv_single_site_gt_50b | project_false_positive | duplicate | …49  "severity" text NOT NULL DEFAULT 'warn', -- info | warn | critical50  "field" text,51  "message" text NOT NULL,52  "details" jsonb,53  "priority" integer NOT NULL DEFAULT 0,   -- review priority (impact-weighted)54  "status" text NOT NULL DEFAULT 'open',   -- open | resolved | dismissed55  "resolution" text,56  "resolved_by" text,57  "resolved_at" timestamp with time zone,58  "run_id" text,59  "dedupe_key" text NOT NULL,60  "created_at" timestamp with time zone NOT NULL DEFAULT now(),61  "updated_at" timestamp with time zone NOT NULL DEFAULT now()62);63CREATE UNIQUE INDEX IF NOT EXISTS "quality_flags_dedupe_uq" ON "quality_flags" ("dedupe_key");64CREATE INDEX IF NOT EXISTS "quality_flags_entity_idx" ON "quality_flags" ("entity_type", "entity_id");65CREATE INDEX IF NOT EXISTS "quality_flags_open_idx" ON "quality_flags" ("status", "priority" DESC) WHERE "status" = 'open';66CREATE INDEX IF NOT EXISTS "quality_flags_code_idx" ON "quality_flags" ("code");6768-- ─── facilities: containment, capacity ontology, AI evidence ───────────────────────────────────────69ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "parent_facility_id" text REFERENCES "facilities"("id");70ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "record_scope" text NOT NULL DEFAULT 'facility'; -- building | facility | campus71ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "ai_evidence" text NOT NULL DEFAULT 'unknown';   -- confirmed | likely | associated | unknown72ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "utility_capacity_mw" double precision;73ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "grid_connection_mw" double precision;74ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "ultimate_campus_mw" double precision;75ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "capacity_scope" text;                            -- scope of the displayed MW figure76ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "capacity_semantics" text;                        -- predicate behind the displayed MW figure77ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "developer_id" text REFERENCES "operators"("id");78ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "landowner_id" text REFERENCES "operators"("id");79ALTER TABLE "facilities" ADD COLUMN IF NOT EXISTS "review_priority" integer NOT NULL DEFAULT 0;80CREATE INDEX IF NOT EXISTS "facilities_parent_idx" ON "facilities" ("parent_facility_id");81CREATE INDEX IF NOT EXISTS "facilities_campus_idx" ON "facilities" ("campus_id");82CREATE INDEX IF NOT EXISTS "facilities_merged_idx" ON "facilities" ("merged_into") WHERE "merged_into" IS NOT NULL;83CREATE INDEX IF NOT EXISTS "facilities_owner_idx" ON "facilities" ("owner_id");84CREATE INDEX IF NOT EXISTS "facilities_ai_idx" ON "facilities" ("ai_evidence") WHERE "ai_evidence" <> 'unknown';85CREATE INDEX IF NOT EXISTS "facilities_opened_idx" ON "facilities" ("opened_on") WHERE "opened_on" IS NOT NULL;8687-- ─── projects: classification, evidence, lifecycle, geocoding, scopes ─────────────────────────────88ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "project_class" text;                -- NEW_BUILD | EXPANSION | … | UNKNOWN89ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "evidence_level" text;               -- strong | weak | none90ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "merged_into" text;91ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "ai_evidence" text NOT NULL DEFAULT 'unknown';92ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "capacity_scope" text;93ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "capacity_semantics" text;94ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "investment_scope" text;95ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "investment_semantics" text;96ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "developer_id" text REFERENCES "operators"("id");97ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "tenant_id" text REFERENCES "operators"("id");98ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "campus_id" text REFERENCES "campuses"("id");99ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "construction_started_on" text;100ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "approved_on" text;101ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "permit_filed_on" text;102ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "opened_on" text;103ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "review_priority" integer NOT NULL DEFAULT 0;104ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "hidden" boolean NOT NULL DEFAULT false;          -- false positives kept for audit, never listed105CREATE INDEX IF NOT EXISTS "projects_class_idx" ON "projects" ("project_class");106CREATE INDEX IF NOT EXISTS "projects_merged_idx" ON "projects" ("merged_into") WHERE "merged_into" IS NOT NULL;107CREATE INDEX IF NOT EXISTS "projects_live_idx" ON "projects" ("status", "country_iso2") WHERE "merged_into" IS NULL AND NOT "hidden";108CREATE INDEX IF NOT EXISTS "projects_latlng_idx" ON "projects" ("lat", "lng") WHERE "lat" IS NOT NULL;109CREATE INDEX IF NOT EXISTS "projects_metro_idx" ON "projects" ("metro_id");110111-- ─── provenance / versions: run tracking + scope ──────────────────────────────────────────────────112ALTER TABLE "provenance" ADD COLUMN IF NOT EXISTS "run_id" text;113ALTER TABLE "provenance" ADD COLUMN IF NOT EXISTS "scope" text;114ALTER TABLE "provenance" ADD COLUMN IF NOT EXISTS "is_winner" boolean NOT NULL DEFAULT false; -- the observation backing the displayed column value115CREATE INDEX IF NOT EXISTS "provenance_current_idx" ON "provenance" ("entity_type", "entity_id", "field") WHERE "is_current";116CREATE INDEX IF NOT EXISTS "provenance_run_idx" ON "provenance" ("run_id");117ALTER TABLE "document_versions" ADD COLUMN IF NOT EXISTS "run_id" text;118ALTER TABLE "document_versions" ADD COLUMN IF NOT EXISTS "extractor_version" text;119CREATE INDEX IF NOT EXISTS "document_versions_run_idx" ON "document_versions" ("run_id");120ALTER TABLE "events" ADD COLUMN IF NOT EXISTS "run_id" text;121ALTER TABLE "events" ADD COLUMN IF NOT EXISTS "cluster_id" text;          -- same underlying announcement across outlets122ALTER TABLE "events" ADD COLUMN IF NOT EXISTS "evidence_count" integer NOT NULL DEFAULT 1;123ALTER TABLE "events" ADD COLUMN IF NOT EXISTS "is_ai" boolean NOT NULL DEFAULT false;124ALTER TABLE "events" ADD COLUMN IF NOT EXISTS "source_kind" text;125CREATE INDEX IF NOT EXISTS "events_significance_idx" ON "events" ("significance" DESC, "detected_at" DESC);126CREATE INDEX IF NOT EXISTS "events_project_idx" ON "events" ("project_id");127CREATE INDEX IF NOT EXISTS "events_cluster_idx" ON "events" ("cluster_id");128CREATE INDEX IF NOT EXISTS "events_run_idx" ON "events" ("run_id");129CREATE INDEX IF NOT EXISTS "events_metro_idx" ON "events" ("metro_id");130131-- ─── news items ────────────────────────────────────────────────────────────────────────────────────132ALTER TABLE "news_items" ADD COLUMN IF NOT EXISTS "project_class" text;133ALTER TABLE "news_items" ADD COLUMN IF NOT EXISTS "cluster_id" text;134ALTER TABLE "news_items" ADD COLUMN IF NOT EXISTS "metro_id" text;135CREATE INDEX IF NOT EXISTS "news_items_operator_gin" ON "news_items" USING gin ("operator_ids");136CREATE INDEX IF NOT EXISTS "news_items_cluster_idx" ON "news_items" ("cluster_id");137138-- ─── connectors: quarantine + health ───────────────────────────────────────────────────────────────139ALTER TABLE "connectors" ADD COLUMN IF NOT EXISTS "quarantine" boolean NOT NULL DEFAULT false;140ALTER TABLE "connectors" ADD COLUMN IF NOT EXISTS "consecutive_failures" integer NOT NULL DEFAULT 0;141ALTER TABLE "connectors" ADD COLUMN IF NOT EXISTS "blocked_since" timestamp with time zone;142ALTER TABLE "connectors" ADD COLUMN IF NOT EXISTS "last_discovered" integer;143ALTER TABLE "connectors" ADD COLUMN IF NOT EXISTS "priority_score" real;144ALTER TABLE "connector_runs" ADD COLUMN IF NOT EXISTS "quarantined" boolean NOT NULL DEFAULT false;145146-- ─── sources: licensing ────────────────────────────────────────────────────────────────────────────147ALTER TABLE "sources" ADD COLUMN IF NOT EXISTS "redistribution" text;      -- allowed | attribution | restricted | unknown148ALTER TABLE "sources" ADD COLUMN IF NOT EXISTS "attribution_required" boolean;149150-- ─── entity keys: scope the primary key by entity type ─────────────────────────────────────────────151DO $$152BEGIN153  IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'entity_keys_pkey' AND conrelid = 'entity_keys'::regclass154             AND array_length(conkey, 1) = 1) THEN155    ALTER TABLE "entity_keys" DROP CONSTRAINT "entity_keys_pkey";156    ALTER TABLE "entity_keys" ADD PRIMARY KEY ("key", "entity_type");157  END IF;158END $$;159160-- ─── daily snapshots for "as of" views and regression checks ───────────────────────────────────────161CREATE TABLE IF NOT EXISTS "entity_snapshots" (162  "day" date NOT NULL,163  "kind" text NOT NULL,        -- global_totals | ranking | facility_status | project_stage | operator_totals | country_totals164  "key" text NOT NULL,165  "payload" jsonb NOT NULL,166  "created_at" timestamp with time zone NOT NULL DEFAULT now(),167  CONSTRAINT "entity_snapshots_pk" PRIMARY KEY ("day", "kind", "key")168);169CREATE INDEX IF NOT EXISTS "entity_snapshots_kind_idx" ON "entity_snapshots" ("kind", "key", "day");170171-- ─── watchlists (private, cookie-scoped) ───────────────────────────────────────────────────────────172CREATE TABLE IF NOT EXISTS "watchlists" (173  "id" text PRIMARY KEY,174  "owner_token" text NOT NULL,175  "entity_type" text NOT NULL,176  "entity_id" text NOT NULL,177  "created_at" timestamp with time zone NOT NULL DEFAULT now()178);179CREATE UNIQUE INDEX IF NOT EXISTS "watchlists_uq" ON "watchlists" ("owner_token", "entity_type", "entity_id");180181-- ─── grid / power context (events already carry the type; this keeps market-level constraint notes) ─182CREATE TABLE IF NOT EXISTS "grid_constraints" (183  "id" text PRIMARY KEY,184  "metro_id" text REFERENCES "metros"("id"),185  "country_iso2" text REFERENCES "countries"("iso2"),186  "kind" text NOT NULL,         -- moratorium | grid_delay | capacity_restriction | load_cap | new_transmission | new_substation | regulation | large_load_queue187  "title" text NOT NULL,188  "summary" text,189  "effective_date" text,190  "source_id" text,191  "document_id" text,192  "url" text NOT NULL,193  "event_id" text,194  "confidence" text NOT NULL DEFAULT 'moderate',195  "created_at" timestamp with time zone NOT NULL DEFAULT now()196);197CREATE INDEX IF NOT EXISTS "grid_constraints_metro_idx" ON "grid_constraints" ("metro_id");198CREATE INDEX IF NOT EXISTS "grid_constraints_country_idx" ON "grid_constraints" ("country_iso2");199200-- ─── daily metrics time-series lookups ─────────────────────────────────────────────────────────────201CREATE INDEX IF NOT EXISTS "daily_metrics_series_idx" ON "daily_metrics" ("metric", "dim", "day");202CREATE INDEX IF NOT EXISTS "entity_matches_created_fac_idx" ON "entity_matches" ((candidate->>'createdFacilityId')) WHERE "status" = 'pending';203