File size: 12,502 Bytes
e2d3383
 
 
 
f2b0895
e2d3383
 
 
 
 
 
 
 
 
 
 
 
 
f2b0895
 
e2d3383
 
 
 
 
 
 
 
 
 
 
 
f2b0895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2d3383
 
 
 
f2b0895
e2d3383
 
 
 
 
 
 
 
 
 
 
 
f2b0895
 
e2d3383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2b0895
 
e2d3383
 
 
f2b0895
 
 
 
 
 
e2d3383
f2b0895
e2d3383
 
 
 
f2b0895
 
e2d3383
 
f2b0895
 
e2d3383
 
f2b0895
 
 
 
 
 
 
 
 
e2d3383
 
f2b0895
 
e2d3383
 
 
 
 
 
 
 
f2b0895
 
 
 
 
 
 
 
e2d3383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2b0895
e2d3383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2b0895
e2d3383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2b0895
 
e2d3383
 
f2b0895
e2d3383
 
 
 
 
 
 
 
 
1b15bc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e2d3383
 
 
c7d99b0
 
 
 
 
 
 
 
 
 
 
 
 
e2d3383
 
 
 
 
f2b0895
 
e2d3383
 
 
 
 
1b15bc9
c7d99b0
e2d3383
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
"""
PostgreSQL schema for the Climate Risk Index Engine.

All table definitions as SQL strings with proper types, foreign keys,
indexes, and constraints. Tables are designed to be created in order
(referenced tables first).
"""

from __future__ import annotations


# ── Table creation order (respects foreign keys) ─────────────────────────

TABLES_ORDERED: list[str] = [
    "zones",
    "daily_readings",
    "healed_readings",
    "healing_log",
    "heat_indices",
    "predictions",
    "trigger_events",
    "basis_risk",
    "explanations",
    "notifications",
    "pipeline_runs",
]


# ── DDL statements ───────────────────────────────────────────────────────

CREATE_ZONES = """
CREATE TABLE IF NOT EXISTS zones (
    zone_id             TEXT PRIMARY KEY,
    name                TEXT NOT NULL,
    city                TEXT NOT NULL,
    country             TEXT NOT NULL,
    latitude            DOUBLE PRECISION NOT NULL,
    longitude           DOUBLE PRECISION NOT NULL,
    elevation_m         DOUBLE PRECISION,
    area_km2            DOUBLE PRECISION,
    population_est      INTEGER,
    settlement_type     TEXT NOT NULL CHECK (settlement_type IN ('formal', 'informal', 'mixed', 'commercial')),
    worker_population_est INTEGER,
    outdoor_exposure_pct DOUBLE PRECISION,
    heat_vulnerability  TEXT NOT NULL CHECK (heat_vulnerability IN ('high', 'moderate', 'low')),
    hot_months          INTEGER[] DEFAULT '{}',
    notes               TEXT DEFAULT '',
    created_at          TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_zones_city ON zones (city);
CREATE INDEX IF NOT EXISTS idx_zones_settlement ON zones (settlement_type);
CREATE INDEX IF NOT EXISTS idx_zones_vulnerability ON zones (heat_vulnerability);
"""

CREATE_DAILY_READINGS = """
CREATE TABLE IF NOT EXISTS daily_readings (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    date            DATE NOT NULL,
    temp_mean_c     DOUBLE PRECISION,
    temp_max_c      DOUBLE PRECISION,
    temp_min_c      DOUBLE PRECISION,
    humidity_pct    DOUBLE PRECISION,
    wind_speed_ms   DOUBLE PRECISION,
    solar_rad_wm2   DOUBLE PRECISION,
    precip_mm       DOUBLE PRECISION,
    source          TEXT DEFAULT 'unknown',
    data_quality    DOUBLE PRECISION DEFAULT 0.0,
    ingested_at     TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (zone_id, date)
);

CREATE INDEX IF NOT EXISTS idx_daily_readings_zone_date ON daily_readings (zone_id, date DESC);
CREATE INDEX IF NOT EXISTS idx_daily_readings_date ON daily_readings (date DESC);
"""

CREATE_HEALED_READINGS = """
CREATE TABLE IF NOT EXISTS healed_readings (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    date            DATE NOT NULL,
    raw_reading_id  BIGINT REFERENCES daily_readings(id),
    temp_mean_c     DOUBLE PRECISION,
    temp_max_c      DOUBLE PRECISION,
    temp_min_c      DOUBLE PRECISION,
    humidity_pct    DOUBLE PRECISION,
    wind_speed_ms   DOUBLE PRECISION,
    quality_score   DOUBLE PRECISION DEFAULT 0.0,
    heal_action     TEXT DEFAULT 'passthrough',
    fields_corrected TEXT[] DEFAULT '{}',
    healed_at       TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (zone_id, date)
);

CREATE INDEX IF NOT EXISTS idx_healed_readings_zone_date ON healed_readings (zone_id, date DESC);
CREATE INDEX IF NOT EXISTS idx_healed_readings_quality ON healed_readings (quality_score);
"""

CREATE_HEALING_LOG = """
CREATE TABLE IF NOT EXISTS healing_log (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    date            DATE NOT NULL,
    healed_reading_id BIGINT REFERENCES healed_readings(id),
    agent_type      TEXT DEFAULT 'rule_based',
    reasoning       TEXT,
    corrections     JSONB DEFAULT '{}',
    tools_used      TEXT[] DEFAULT '{}',
    confidence      DOUBLE PRECISION,
    tokens_used     INTEGER DEFAULT 0,
    latency_ms      INTEGER DEFAULT 0,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_healing_log_zone ON healing_log (zone_id, date DESC);
"""

CREATE_HEAT_INDICES = """
CREATE TABLE IF NOT EXISTS heat_indices (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    date            DATE NOT NULL,
    grid_temp_c     DOUBLE PRECISION,
    uhi_delta_c     DOUBLE PRECISION,
    corrected_temp_c DOUBLE PRECISION,
    wbgt_c          DOUBLE PRECISION,
    heat_index_c    DOUBLE PRECISION,
    heat_risk_score DOUBLE PRECISION CHECK (heat_risk_score BETWEEN 0 AND 100),
    risk_level      TEXT CHECK (risk_level IN ('low', 'moderate', 'high', 'critical')),
    consecutive_hot_days INTEGER DEFAULT 0,
    computed_at     TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (zone_id, date)
);

CREATE INDEX IF NOT EXISTS idx_heat_indices_zone_date ON heat_indices (zone_id, date DESC);
CREATE INDEX IF NOT EXISTS idx_heat_indices_risk ON heat_indices (risk_level);
"""

CREATE_PREDICTIONS = """
CREATE TABLE IF NOT EXISTS predictions (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    date            DATE NOT NULL,
    trigger_probability_7d DOUBLE PRECISION CHECK (trigger_probability_7d BETWEEN 0 AND 1),
    prediction_confidence DOUBLE PRECISION CHECK (prediction_confidence BETWEEN 0 AND 1),
    model_tier      TEXT DEFAULT 'climatology',
    xgb_probability DOUBLE PRECISION,
    lstm_probability DOUBLE PRECISION,
    ensemble_method TEXT DEFAULT 'average',
    predicted_at    TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (zone_id, date)
);

CREATE INDEX IF NOT EXISTS idx_predictions_zone_date ON predictions (zone_id, date DESC);
CREATE INDEX IF NOT EXISTS idx_predictions_tier ON predictions (model_tier);
"""

CREATE_TRIGGER_EVENTS = """
CREATE TABLE IF NOT EXISTS trigger_events (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    trigger_level   TEXT NOT NULL CHECK (trigger_level IN ('critical', 'warning', 'watch')),
    triggered_at    TIMESTAMPTZ NOT NULL,
    max_temp_c      DOUBLE PRECISION,
    max_wbgt_c      DOUBLE PRECISION,
    consecutive_days INTEGER,
    heat_risk_score DOUBLE PRECISION,
    settlement_type TEXT,
    payout_per_worker_usd DOUBLE PRECISION,
    enrolled_workers INTEGER,
    total_payout_usd DOUBLE PRECISION,
    resolved_at     TIMESTAMPTZ,
    resolution_notes TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_trigger_events_zone ON trigger_events (zone_id, triggered_at DESC);
CREATE INDEX IF NOT EXISTS idx_trigger_events_level ON trigger_events (trigger_level);
CREATE INDEX IF NOT EXISTS idx_trigger_events_unresolved ON trigger_events (zone_id) WHERE resolved_at IS NULL;
"""

CREATE_BASIS_RISK = """
CREATE TABLE IF NOT EXISTS basis_risk (
    id              BIGSERIAL PRIMARY KEY,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    assessed_at     TIMESTAMPTZ DEFAULT NOW(),
    overall_score   DOUBLE PRECISION NOT NULL CHECK (overall_score BETWEEN 0 AND 1),
    false_positive_rate DOUBLE PRECISION,
    false_negative_rate DOUBLE PRECISION,
    correlation     DOUBLE PRECISION,
    mae             DOUBLE PRECISION,
    total_events    INTEGER,
    true_positives  INTEGER,
    true_negatives  INTEGER,
    false_positives INTEGER,
    false_negatives INTEGER,
    trigger_accuracy DOUBLE PRECISION,
    tier_accuracy   JSONB DEFAULT '{}',
    recommendations TEXT[] DEFAULT '{}',
    confidence_low  DOUBLE PRECISION,
    confidence_high DOUBLE PRECISION
);

CREATE INDEX IF NOT EXISTS idx_basis_risk_zone ON basis_risk (zone_id, assessed_at DESC);
"""

CREATE_EXPLANATIONS = """
CREATE TABLE IF NOT EXISTS explanations (
    id              BIGSERIAL PRIMARY KEY,
    trigger_event_id BIGINT REFERENCES trigger_events(id),
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    trigger_level   TEXT NOT NULL,
    english_text    TEXT NOT NULL,
    swahili_text    TEXT NOT NULL,
    payout_amount   DOUBLE PRECISION,
    payout_currency TEXT DEFAULT 'USD',
    settlement_type TEXT,
    protective_actions TEXT[] DEFAULT '{}',
    provider        TEXT DEFAULT 'template',
    generated_at    TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_explanations_trigger ON explanations (trigger_event_id);
CREATE INDEX IF NOT EXISTS idx_explanations_zone ON explanations (zone_id, generated_at DESC);
"""

CREATE_NOTIFICATIONS = """
CREATE TABLE IF NOT EXISTS notifications (
    id              BIGSERIAL PRIMARY KEY,
    explanation_id  BIGINT REFERENCES explanations(id),
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    recipient       TEXT NOT NULL,
    channel         TEXT NOT NULL CHECK (channel IN ('console', 'sms', 'whatsapp')),
    status          TEXT NOT NULL CHECK (status IN ('sent', 'failed', 'dry_run', 'pending')),
    language        TEXT DEFAULT 'en',
    message_preview TEXT,
    message_sid     TEXT,
    cost_estimate   DOUBLE PRECISION DEFAULT 0.0,
    error           TEXT,
    sent_at         TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_notifications_zone ON notifications (zone_id, sent_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_status ON notifications (status);
"""

CREATE_PIPELINE_RUNS = """
CREATE TABLE IF NOT EXISTS pipeline_runs (
    id              BIGSERIAL PRIMARY KEY,
    run_id          TEXT UNIQUE NOT NULL,
    started_at      TIMESTAMPTZ NOT NULL,
    finished_at     TIMESTAMPTZ,
    status          TEXT DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed', 'partial')),
    zones_processed INTEGER DEFAULT 0,
    triggers_found  INTEGER DEFAULT 0,
    notifications_sent INTEGER DEFAULT 0,
    steps_completed TEXT[] DEFAULT '{}',
    step_status     JSONB DEFAULT '{}',
    total_cost_usd  DOUBLE PRECISION DEFAULT 0,
    error           TEXT,
    duration_s      DOUBLE PRECISION,
    config_snapshot JSONB DEFAULT '{}'
);

CREATE INDEX IF NOT EXISTS idx_pipeline_runs_status ON pipeline_runs (status);
CREATE INDEX IF NOT EXISTS idx_pipeline_runs_started ON pipeline_runs (started_at DESC);
"""

CREATE_WORKERS = """
CREATE TABLE IF NOT EXISTS workers (
    worker_id       TEXT PRIMARY KEY,
    name            TEXT NOT NULL,
    name_swahili    TEXT,
    nida_id         TEXT,
    phone           TEXT NOT NULL,
    zone_id         TEXT NOT NULL REFERENCES zones(zone_id),
    occupation      TEXT NOT NULL,
    age             INTEGER,
    years_outdoor   INTEGER,
    household_size  INTEGER,
    mobile_money    TEXT,
    tasaf_enrolled  BOOLEAN DEFAULT false,
    enrolled_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_workers_zone ON workers (zone_id);
"""


# ── Aggregate DDL ────────────────────────────────────────────────────────

CREATE_ZONE_THRESHOLDS = """
CREATE TABLE IF NOT EXISTS zone_thresholds (
    zone_id             TEXT PRIMARY KEY REFERENCES zones(zone_id),
    alert_threshold_c   DOUBLE PRECISION NOT NULL,
    payout_threshold_c  DOUBLE PRECISION NOT NULL,
    uhi_model           TEXT NOT NULL,
    threshold_mode      TEXT NOT NULL,
    computed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS zone_thresholds_computed_at_idx ON zone_thresholds(computed_at DESC);
"""


ALL_DDL: dict[str, str] = {
    "zones": CREATE_ZONES,
    "daily_readings": CREATE_DAILY_READINGS,
    "healed_readings": CREATE_HEALED_READINGS,
    "healing_log": CREATE_HEALING_LOG,
    "heat_indices": CREATE_HEAT_INDICES,
    "predictions": CREATE_PREDICTIONS,
    "trigger_events": CREATE_TRIGGER_EVENTS,
    "basis_risk": CREATE_BASIS_RISK,
    "explanations": CREATE_EXPLANATIONS,
    "notifications": CREATE_NOTIFICATIONS,
    "pipeline_runs": CREATE_PIPELINE_RUNS,
    "workers": CREATE_WORKERS,
    "zone_thresholds": CREATE_ZONE_THRESHOLDS,
}


def get_full_ddl() -> str:
    """Return the complete DDL script to create all tables in order."""
    parts = [f"-- Table: {name}\n{ddl}" for name, ddl in ALL_DDL.items()]
    return "\n\n".join(parts)


def get_table_names() -> list[str]:
    """Return table names in creation order."""
    return list(TABLES_ORDERED)