venthrive-api / sql /intel_schema.sql
GitHub Actions
Sync from GitHub · 2026-10-21 11:53 UTC
b7c46f4
Raw History Blame Contribute Delete
6.76 kB
-- Intel store: Phase 0 + Phase 1 of the predictive business-intelligence system
-- (docs/blueprint/intelligence-system.md, ADR-0017..0021).
--
-- Portable SQL: runs unchanged on SQLite (Phase 1, local — ADR-0020) and on
-- Postgres/Supabase when the store moves. The price of that portability:
-- timestamps are ISO-8601 UTC text, booleans are 0/1 integers, JSON is text, ids
-- are generated in Python (no serial/uuid defaults). Revisit all three when the
-- store moves to Postgres for good.
--
-- Applied by `py -m intel.cli init` (idempotent — every statement is IF NOT EXISTS).
-- ─── Gazetteer ──────────────────────────────────────────────────────────────
-- Everything in the store is keyed by location, so this comes first. Ids are
-- dotted paths: 'ng', 'ng.lagos', 'ng.lagos.ikeja'. State/LGA names are exactly
-- the ones users pick in onboarding (data/nigeria-locations.ts → nigeria_locations.json).
create table if not exists locations (
id text primary key,
level text not null check (level in ('country', 'state', 'lga', 'town')),
name text not null,
parent_id text references locations(id),
lat real,
lon real,
source text not null
);
-- Normalised alternative names → location. Seeded from the gazetteer itself,
-- boundary-file spellings, a short curated city list, and place-data localities
-- (intel/locations.py). The seed of the Layer 5 search/geocoding service.
create table if not exists location_aliases (
alias text not null,
location_id text not null references locations(id),
origin text not null, -- 'gazetteer' | 'boundary' | 'curated' | 'places'
primary key (alias, location_id)
);
-- ─── Layer 1: stale awareness ───────────────────────────────────────────────
-- One row per external source. "Stale" means late against the source's OWN
-- cadence, and two dates are tracked because a fetch can succeed today and still
-- return last quarter's numbers.
create table if not exists sources (
id text primary key,
family text not null,
provider text not null,
licence text not null,
expected_cadence_hours integer not null,
latency_hours integer not null default 0, -- how far behind "now" the newest data legitimately is
last_attempt_at text,
last_success_at text,
latest_data_period text, -- newest period the source has given us
last_error text
);
-- Every layer reports "done" and the receiving layer acknowledges it. Layer 1
-- flags reports nobody received.
create table if not exists handoffs (
id text primary key,
run_id text not null,
from_layer text not null,
to_layer text not null,
report text not null, -- JSON
produced_at text not null,
received_at text,
received_by text
);
create index if not exists handoffs_pending on handoffs (to_layer, received_at);
-- ─── Layer 2: data stream ───────────────────────────────────────────────────
-- A stream is one metric from one source, stored at its NATIVE resolution
-- (never copied down to finer locations — that would fake precision the models
-- would then learn from).
create table if not exists streams (
id text primary key, -- 'weather.precipitation_sum'
family text not null,
metric text not null,
unit text not null,
kind text not null check (kind in ('driver', 'indicator')),
native_level text not null check (native_level in ('country', 'state', 'lga', 'town', 'point')),
cadence text not null check (cadence in ('daily', 'weekly', 'monthly', 'quarterly', 'yearly', 'event')),
source_id text not null references sources(id)
);
-- Numeric time series. Observed history and forecasts share one table:
-- history has is_forecast=0 and issued_at=''; each forecast run keeps its own
-- rows (issued_at = run date) so forecast skill can be measured later.
create table if not exists observations (
stream_id text not null references streams(id),
location_id text not null references locations(id),
period_start text not null,
is_forecast integer not null default 0 check (is_forecast in (0, 1)),
issued_at text not null default '',
value real not null,
ingested_at text not null,
primary key (stream_id, location_id, period_start, is_forecast, issued_at)
);
create index if not exists observations_loc on observations (location_id, stream_id, period_start);
-- Things that happen rather than values that move: a policy enacted, a holiday,
-- an incident, a project gazetted. Kept permanently — this is the history the
-- correlation step and models learn from (unlike news_items, which expire from
-- the user-facing feed).
create table if not exists events (
id text primary key,
family text not null, -- policy | weather | infrastructure | funding | security | calendar | other
type text not null, -- e.g. 'public_holiday', 'religious_holiday', 'season', 'news'
location_id text references locations(id),
starts_on text not null,
ends_on text,
title text not null,
detail text,
is_estimate integer not null default 0 check (is_estimate in (0, 1)),
source_id text not null references sources(id),
source_ref text not null, -- natural key at the source, so re-ingest is idempotent
ingested_at text not null,
unique (source_id, source_ref)
);
create index if not exists events_loc on events (location_id, starts_on);
-- Points of interest. subsector is one of backend/news/subsectors.json, or null
-- for a context place (worship, civic, landmark) — those still matter as
-- footfall generators even though they aren't competitors.
create table if not exists places (
id text primary key, -- Overture GERS id
name text,
category text,
taxonomy text,
subsector text,
confidence real,
operating_status text,
lat real not null,
lon real not null,
location_id text references locations(id), -- the LGA it falls in
locality text,
source_id text not null references sources(id),
release text not null
);
create index if not exists places_loc on places (location_id, subsector);