Database design and structure¶
The CLI can persist each aggregated forecast response to a SQLite database. The database is designed as an append-oriented record of forecast runs: request metadata is stored once, provider outcomes are stored below the run, and each provider/model forecast owns its normalized minutely, hourly, daily, and alert rows.
This page describes the schema created by
omni_weather_forecast_apis.sqlite_store and the quota table created by
omni_weather_forecast_apis.quota.SqliteQuotaTracker.
Scope and design goals¶
The SQLite layer has four main goals:
- Preserve the provenance of every normalized value from CLI run to provider and model.
- Keep provider failures beside successful results so partial failures remain queryable.
- Store normalized time-series data in relational columns that work directly with SQLite, pandas, DuckDB, and similar tools.
- Provide a convenient hourly feature view for ensemble forecasting and verification workloads.
The database is not the HTTP response cache. The conditional-request cache is
process-local and in memory. The CLI's persistent location-timezone cache is a
separate companion file: forecasts.sqlite uses
forecasts.timezones.sqlite. Cache keys retain six decimal places, entries
record their resolver source/version and resolution time, and entries older
than 30 days are refreshed. The database also does not store observations or
computed consensus forecasts; those are intended to live in downstream systems.
Creation and write lifecycle¶
Set --sqlite PATH on the CLI, or set sqlite = "PATH" in the TOML
configuration:
uv run omni-weather \
--config ./config.toml \
--lat 40.7128 \
--lon -74.0060 \
--sqlite ./forecasts.sqlite
For each CLI invocation, persistence happens in this order:
SqliteQuotaTrackercreatesprovider_quota_usagewhen quota tracking is first used. Each actual provider request attempt, including a retry, reserves quota before the HTTP request is sent.- The completed
ForecastResponseinitializes or upgrades the forecast schema. - One
forecast_runsrow is inserted. - Every provider outcome is inserted into
provider_results. Successful outcomes also create model rows and normalized forecast data. - The forecast transaction is committed and its
run_idis returned. - Provider lifecycle events are written to
provider_logsin a separate transaction associated with thatrun_id.
Because forecast data and logs use separate transactions, a successfully saved run can exist without its logs if the later log write fails. A failed forecast write is rolled back when its connection closes.
The public library client does not automatically persist forecast responses.
Library users can call save_forecast_response() directly, use a response
hook, or provide their own storage adapter.
Relationship model¶
forecast_runs (one CLI invocation)
├── provider_results (one outcome per returned provider result)
│ └── source_forecasts (one normalized forecast per provider/model)
│ ├── minutely_points
│ ├── hourly_points
│ ├── daily_points
│ └── alerts
└── provider_logs (zero or more lifecycle events)
provider_quota_usage (independent provider/day counters)
The principal relationship path is:
forecast_runs.id
-> provider_results.run_id
-> source_forecasts.provider_result_id
-> *_points.source_forecast_id / alerts.source_forecast_id
provider_quota_usage is intentionally independent. A quota counter can be
incremented before a forecast run is saved, and it must survive deletion of
historical forecast data.
Schema summary¶
| Object | Cardinality and purpose |
|---|---|
schema_metadata |
Singleton structural schema-version row |
forecast_runs |
One row per persisted aggregate response |
provider_results |
One success or error outcome per result in a run |
source_forecasts |
One model-specific normalized forecast inside a successful provider result |
minutely_points |
Zero or more normalized precipitation points per source forecast |
hourly_points |
Zero or more normalized weather points per source forecast |
daily_points |
Zero or more normalized daily summaries per source forecast |
alerts |
Zero or more normalized weather alerts per source forecast |
provider_logs |
Zero or more provider lifecycle events, optionally associated with a run |
provider_quota_usage |
One durable counter per provider and UTC day |
stacking_features |
Read-only joined view of successful hourly forecasts |
db_repairs |
Audit log written by scripts/repair_db.py; absent unless a repair ran |
Alongside the database, the CLI writes a raw payload archive: a raw/
directory of gzipped JSONL files (one per invocation) recording every network
response, linked from forecast_runs.raw_archive_path. See
Data corrections for the format and rationale.
SQLite uses dynamic typing, but each declaration below establishes the
expected type affinity. Unless a column is marked NOT NULL, missing provider
data is stored as SQL NULL rather than a sentinel value.
Table reference¶
forecast_runs¶
This is the root table for a persisted aggregate response.
| Column | Type and constraints | Meaning |
|---|---|---|
id |
INTEGER PRIMARY KEY AUTOINCREMENT |
Stable run identifier returned by save_forecast_response() |
latitude |
REAL NOT NULL |
Requested latitude in decimal degrees |
longitude |
REAL NOT NULL |
Requested longitude in decimal degrees |
granularity |
TEXT NOT NULL |
JSON array of requested granularity slugs, such as ["hourly", "daily"] |
language |
TEXT NOT NULL |
Resolved language preference |
completed_at |
TEXT NOT NULL |
UTC ISO 8601 completion timestamp |
total_latency_ms |
REAL NOT NULL |
End-to-end aggregate latency in milliseconds |
total_results |
INTEGER NOT NULL |
Total number of provider results |
succeeded |
INTEGER NOT NULL |
Number of successful provider results |
failed |
INTEGER NOT NULL |
Number of failed provider results |
raw_archive_path |
TEXT |
Path to the gzipped JSONL raw payload archive for this run; NULL when archiving was disabled or no network traffic occurred |
app_version |
TEXT |
Package version that wrote the run; may be NULL for older rows, but normalization_revision is the authoritative semantic marker |
request_timezone |
TEXT |
IANA timezone on the resolved request; NULL when none was supplied or resolved |
normalization_revision |
INTEGER NOT NULL |
Semantic normalization revision; migrated legacy rows are revision 1, and current rows are revision 2 |
The summary counts are stored rather than recomputed so the database preserves the exact response summary emitted by the client.
provider_results¶
This table records both sides of the ProviderSuccess | ProviderError union.
Success-only and error-only columns are nullable.
| Column | Type and constraints | Meaning |
|---|---|---|
id |
INTEGER PRIMARY KEY AUTOINCREMENT |
Provider outcome identifier |
run_id |
INTEGER NOT NULL, foreign key |
Parent forecast_runs.id; deleted with its run |
provider |
TEXT NOT NULL |
Stable provider slug, such as open_meteo |
status |
TEXT NOT NULL |
success or error |
fetched_at |
TEXT |
Successful fetch timestamp as UTC ISO 8601 text |
fetched_at_unix |
INTEGER |
Successful fetch timestamp as Unix seconds |
run_cycle |
TEXT |
Synthetic six-hour UTC cycle bucket for a successful fetch |
latency_ms |
REAL NOT NULL |
Provider latency, including elapsed time until failure for errors |
error_code |
TEXT |
Normalized error slug for failed results |
error_message |
TEXT |
Human-readable failure detail |
http_status |
INTEGER |
HTTP status when the failure has one |
raw_json |
TEXT |
Optional provider success or error payload serialized as JSON |
run_cycle is calculated by flooring fetched_at to the previous 00:00,
06:00, 12:00, or 18:00 UTC boundary. It is a convenient grouping key,
not a provider-declared model initialization time. Providers do not all expose
their authoritative model cycle.
Successful raw payloads are normally NULL. They are populated when the
request enables include_raw or the CLI uses --include-raw. Error payloads
are stored when the normalized error contains raw detail. Values are encoded
with sorted JSON keys and fall back to string conversion for otherwise
non-serializable objects.
source_forecasts¶
A provider response may contain more than one model. This table gives each model its own parent row before inserting time-series data.
| Column | Type and constraints | Meaning |
|---|---|---|
id |
INTEGER PRIMARY KEY AUTOINCREMENT |
Source forecast identifier |
provider_result_id |
INTEGER NOT NULL, foreign key |
Parent provider_results.id; deleted with its provider result |
provider |
TEXT NOT NULL |
Source attribution slug |
model |
TEXT NOT NULL |
Provider model or source name |
timezone |
TEXT |
IANA timezone used for this source's civil-time normalization |
The provider slug is repeated here deliberately: analytics can identify a
model directly from source_forecasts without treating the model name as
globally unique.
minutely_points¶
| Column | Type and constraints | Meaning and normalized unit |
|---|---|---|
source_forecast_id |
INTEGER NOT NULL, foreign key |
Parent source_forecasts.id; deleted with its source |
timestamp |
TEXT NOT NULL |
UTC ISO 8601 valid time |
timestamp_unix |
INTEGER NOT NULL |
Valid time as Unix seconds |
precipitation_intensity |
REAL |
Precipitation rate in mm/h |
precipitation_probability |
REAL |
Probability from 0 to 1 |
hourly_points¶
Each row represents one provider/model forecast at one valid time. The schema keeps both normalized values and selected provider-native condition fields.
| Column | Type and constraints | Meaning and normalized unit |
|---|---|---|
source_forecast_id |
INTEGER NOT NULL, foreign key |
Parent source_forecasts.id; deleted with its source |
timestamp |
TEXT NOT NULL |
UTC ISO 8601 valid time |
timestamp_unix |
INTEGER NOT NULL |
Valid time as Unix seconds |
horizon_hours |
REAL |
Hours from provider fetch time to valid time |
temperature |
REAL |
Air temperature at 2 m, °C |
apparent_temperature |
REAL |
Feels-like temperature, °C |
dew_point |
REAL |
Dew point, °C |
humidity |
REAL |
Relative humidity, percent from 0 to 100 |
wind_speed |
REAL |
Wind speed at 10 m, m/s |
wind_gust |
REAL |
Wind gust speed, m/s |
wind_direction |
REAL |
Meteorological direction in degrees |
pressure_sea |
REAL |
Sea-level pressure, hPa |
pressure_surface |
REAL |
Surface pressure, hPa |
precipitation |
REAL |
Liquid-equivalent precipitation, mm |
precipitation_probability |
REAL |
Probability from 0 to 1 |
rain |
REAL |
Rain amount, mm |
snow |
REAL |
Liquid-equivalent snowfall, mm — only for providers that report liquid equivalent (e.g. OpenWeather, Open-Meteo via snowfall_water_equivalent) |
snowfall_depth |
REAL |
New snowfall depth, mm — only for providers that report depth (e.g. Open-Meteo snowfall, Pirate Weather snowAccumulation) |
snow_depth |
REAL |
Snow depth on the ground, mm |
cloud_cover |
REAL |
Total cloud cover, percent |
cloud_cover_low |
REAL |
Low cloud cover, percent |
cloud_cover_mid |
REAL |
Mid cloud cover, percent |
cloud_cover_high |
REAL |
High cloud cover, percent |
visibility |
REAL |
Visibility, km |
uv_index |
REAL |
UV index |
solar_radiation_ghi |
REAL |
Global horizontal irradiance, W/m² |
solar_radiation_dni |
REAL |
Direct normal irradiance, W/m² |
solar_radiation_dhi |
REAL |
Diffuse horizontal irradiance, W/m² |
condition |
TEXT |
Normalized WeatherCondition slug |
condition_original |
TEXT |
Provider-native condition text |
condition_code_original |
TEXT |
Provider-native condition or icon code, stringified before storage |
is_day |
INTEGER |
Boolean daylight flag stored as 1 or 0 |
horizon_hours is derived as:
(timestamp_unix - fetched_at_unix) / 3600.0
It can be fractional, zero, or negative. It measures lead time from the local fetch timestamp, not necessarily from the upstream model's initialization time.
daily_points¶
| Column | Type and constraints | Meaning and normalized unit |
|---|---|---|
source_forecast_id |
INTEGER NOT NULL, foreign key |
Parent source_forecasts.id; deleted with its source |
forecast_date |
TEXT NOT NULL |
Forecast date in ISO YYYY-MM-DD form |
temperature_max, temperature_min |
REAL |
Daily extrema, °C |
apparent_temperature_max, apparent_temperature_min |
REAL |
Apparent-temperature extrema, °C |
wind_speed_max, wind_gust_max |
REAL |
Daily maximum speeds, m/s |
wind_direction_dominant |
REAL |
Dominant meteorological direction in degrees |
precipitation_sum |
REAL |
Total liquid-equivalent precipitation, mm |
precipitation_probability_max |
REAL |
Maximum probability from 0 to 1 |
rain_sum, snowfall_sum |
REAL |
Daily rain and liquid-equivalent snowfall totals, mm |
snowfall_depth_sum |
REAL |
Daily new snowfall depth total, mm |
cloud_cover_mean |
REAL |
Mean cloud cover, percent |
uv_index_max |
REAL |
Maximum UV index |
visibility_min |
REAL |
Minimum visibility, km |
humidity_mean |
REAL |
Mean relative humidity, percent |
pressure_sea_mean |
REAL |
Mean sea-level pressure, hPa |
condition |
TEXT |
Representative normalized condition slug |
summary |
TEXT |
Provider summary text |
sunrise, sunset |
TEXT |
Optional UTC ISO 8601 timestamps |
moonrise, moonset |
TEXT |
Optional UTC ISO 8601 timestamps |
moon_phase |
REAL |
Moon phase from 0 to 1 |
daylight_duration |
REAL |
Daylight duration in seconds |
solar_radiation_sum |
REAL |
Shortwave radiation total, MJ/m² |
alerts¶
| Column | Type and constraints | Meaning |
|---|---|---|
source_forecast_id |
INTEGER NOT NULL, foreign key |
Parent source_forecasts.id; deleted with its source |
sender_name |
TEXT NOT NULL |
Alert issuer |
event |
TEXT NOT NULL |
Event or alert title |
start |
TEXT NOT NULL |
UTC ISO 8601 start timestamp |
end |
TEXT |
Optional UTC ISO 8601 end timestamp |
description |
TEXT NOT NULL |
Alert body |
severity |
TEXT |
Normalized severity slug |
url |
TEXT |
Optional source URL |
provider_logs¶
The client emits structured lifecycle events with phases start, retry,
success, and error.
| Column | Type and constraints | Meaning |
|---|---|---|
id |
INTEGER PRIMARY KEY AUTOINCREMENT |
Log event identifier |
run_id |
INTEGER, foreign key |
Optional parent forecast_runs.id; set to NULL if the run is deleted |
provider |
TEXT NOT NULL |
Provider slug |
phase |
TEXT NOT NULL |
Lifecycle phase |
message |
TEXT NOT NULL |
Human-readable event message |
latency_ms |
REAL NOT NULL DEFAULT 0 |
Elapsed time associated with the event |
error_code |
TEXT |
Normalized error slug when relevant |
http_status |
INTEGER |
HTTP status when relevant |
extra_json |
TEXT |
Optional structured metadata such as attempt, delay, or limit |
logged_at |
TEXT NOT NULL |
Event timestamp as ISO 8601 text |
run_id is nullable so callers can persist events independently. The CLI
normally assigns it after the forecast transaction returns the new run ID.
provider_quota_usage¶
This table is initialized by SqliteQuotaTracker, not by the forecast schema
initializer. It therefore appears only when the SQLite quota tracker is used.
| Column | Type and constraints | Meaning |
|---|---|---|
provider |
TEXT NOT NULL, composite primary key |
Provider slug |
day |
TEXT NOT NULL, composite primary key |
UTC day in ISO YYYY-MM-DD form |
request_count |
INTEGER NOT NULL DEFAULT 0 |
Reserved provider request attempts for that day |
Quota consumption uses BEGIN IMMEDIATE around the read-check-increment
sequence. That obtains a write reservation before reading, so two local
writers cannot both consume the last available request. Connections use a
10-second SQLite busy timeout.
Quota rows are not linked to forecast_runs: retries count separately, a
request may fail after consuming quota, and quota enforcement must remain
durable even when old forecast history is deleted.
Keys, uniqueness, and deletion behavior¶
forecast_runs, provider_results, source_forecasts, and provider_logs
have generated integer primary keys. provider_quota_usage has the composite
primary key (provider, day).
The point and alert tables do not define generated primary keys or uniqueness constraints. Their identity is the parent source plus their row contents. The writer appends each normalized list exactly once for a new source forecast; the schema does not deduplicate repeated timestamps supplied by a provider.
The forecast writer enables PRAGMA foreign_keys = ON. With foreign-key
enforcement enabled, deleting a run has these effects:
| Deleted row | Effect |
|---|---|
forecast_runs |
Cascades through provider results, source forecasts, all point rows, and alerts |
provider_results |
Cascades through source forecasts, points, and alerts |
source_forecasts |
Cascades through its point rows and alerts |
forecast_runs with logs |
Preserves log rows but sets provider_logs.run_id to NULL |
| Any forecast history | Does not change provider_quota_usage |
SQLite foreign-key enforcement is connection-local. External maintenance
scripts must execute PRAGMA foreign_keys = ON before relying on these
cascades.
Time representation¶
The normalized Pydantic schema converts point and response datetimes to UTC before persistence. SQLite stores time in three forms chosen for the expected query:
- ISO 8601
TEXTpreserves readable, timezone-bearing timestamps. - Unix-second
INTEGERcolumns support range scans and arithmetic for minutely/hourly valid times and provider fetch times. - Daily forecast dates and quota days use ISO
YYYY-MM-DDtext.
The duplicated ISO and Unix representations are intentional. Use Unix columns for comparisons and window queries; use ISO columns for display and export.
Indexes¶
The schema initializer creates these indexes with IF NOT EXISTS:
| Index | Columns | Intended access path |
|---|---|---|
idx_provider_results_run_provider |
provider_results(run_id, provider) |
Outcomes for a provider within one run |
idx_provider_results_run_cycle |
provider_results(run_cycle) |
Results grouped by synthetic cycle |
idx_source_forecasts_provider_result |
source_forecasts(provider_result_id) |
Models belonging to a result |
idx_hourly_points_source |
hourly_points(source_forecast_id) |
Hourly series for one source |
idx_hourly_points_horizon |
hourly_points(horizon_hours) |
Lead-time slices |
idx_hourly_points_timestamp |
hourly_points(timestamp_unix) |
Valid-time range scans |
idx_hourly_points_horizon_timestamp |
hourly_points(horizon_hours, timestamp_unix) |
Lead-time and valid-time filters |
idx_minutely_points_source_timestamp |
minutely_points(source_forecast_id, timestamp_unix) |
Ordered minutely series for one source |
idx_daily_points_source_date |
daily_points(source_forecast_id, forecast_date) |
Ordered daily series for one source |
Primary keys create their own SQLite indexes. There are currently no secondary indexes on alerts, provider logs, or quota counts.
stacking_features view¶
stacking_features is a read-only convenience view over successful hourly
data. It joins:
hourly_points
-> source_forecasts
-> provider_results
-> forecast_runs
The view exposes provenance and features in a single row:
run_idvalid_time,valid_time_unix,horizon_hoursrun_cycle,fetched_at,fetched_at_unixprovider,model,latitude,longitude- all normalized hourly numeric fields
- normalized
conditionandis_day - source
timezone(appended last to preserve the positional order of the pre-timezone view columns)
It filters to provider_results.status = 'success'. Error rows and
provider-native condition_original / condition_code_original fields are
not included.
Successive runs can contain forecasts for the same provider, model, location,
and valid time. Always include run_id when selecting one invocation, and use
an exact valid_time_unix when aligning providers. Filtering only on a rounded
horizon_hours can mix adjacent forecast times.
Example: select aligned features from the latest run at the first valid time at least 24 hours ahead:
WITH latest_run AS (
SELECT MAX(id) AS run_id
FROM forecast_runs
), target_time AS (
SELECT MIN(valid_time_unix) AS valid_time_unix
FROM stacking_features
WHERE run_id = (SELECT run_id FROM latest_run)
AND horizon_hours >= 24
)
SELECT
valid_time,
provider,
model,
horizon_hours,
temperature,
wind_speed,
precipitation_probability
FROM stacking_features
WHERE run_id = (SELECT run_id FROM latest_run)
AND valid_time_unix = (SELECT valid_time_unix FROM target_time)
ORDER BY provider, model;
The initializer drops and recreates this view whenever it initializes the schema. This keeps existing database files synchronized with the current view definition.
Query examples¶
Summarize recent runs¶
SELECT
id,
completed_at,
latitude,
longitude,
succeeded,
failed,
total_latency_ms
FROM forecast_runs
ORDER BY id DESC
LIMIT 20;
Inspect provider failures¶
SELECT
fr.id AS run_id,
fr.completed_at,
pr.provider,
pr.error_code,
pr.http_status,
pr.error_message
FROM provider_results AS pr
JOIN forecast_runs AS fr ON fr.id = pr.run_id
WHERE pr.status = 'error'
ORDER BY fr.id DESC, pr.provider;
Read one model's hourly series¶
SELECT
hp.timestamp,
hp.horizon_hours,
hp.temperature,
hp.wind_speed,
hp.condition
FROM hourly_points AS hp
JOIN source_forecasts AS sf ON sf.id = hp.source_forecast_id
JOIN provider_results AS pr ON pr.id = sf.provider_result_id
WHERE pr.run_id = :run_id
AND sf.provider = :provider
AND sf.model = :model
ORDER BY hp.timestamp_unix;
Inspect retries and error events¶
SELECT
provider,
phase,
message,
error_code,
http_status,
extra_json,
logged_at
FROM provider_logs
WHERE run_id = :run_id
AND phase IN ('retry', 'error')
ORDER BY id;
Check daily quota usage¶
SELECT provider, day, request_count
FROM provider_quota_usage
WHERE day >= date('now', '-7 days')
ORDER BY day DESC, provider;
Schema initialization and evolution¶
The singleton schema_metadata table records the current structural schema
version. Databases with a newer version are rejected instead of being modified
by an older package. The project does not use an external migration framework;
initialization is idempotent and runs before forecast or log writes:
- Base tables are created with
CREATE TABLE IF NOT EXISTS. PRAGMA table_infodetects older schemas.- Missing additive columns are introduced with
ALTER TABLE ... ADD COLUMN. - Secondary indexes are created with
CREATE INDEX IF NOT EXISTS. stacking_featuresis recreated only when its column contract is stale.schema_metadata.schema_versionis advanced after all upgrade steps succeed.
The built-in upgrade path currently recognizes these additions:
| Table | Additive columns |
|---|---|
forecast_runs |
raw_archive_path, app_version, request_timezone, normalization_revision |
provider_results |
fetched_at_unix, run_cycle |
source_forecasts |
timezone |
hourly_points |
horizon_hours, snowfall_depth |
daily_points |
snowfall_depth_sum |
provider_logs |
extra_json |
Future destructive or semantic changes require an explicit migration plan;
CREATE TABLE IF NOT EXISTS does not reconcile changed constraints or column
types in an already-existing table. Back up long-lived databases before
upgrading across releases that change the persistence schema.
Operational guidance¶
- Treat the database as append-oriented history. One CLI invocation creates a new run even when its request matches an earlier run.
- Keep
run_id, provider, model, location, and valid time in downstream keys. A valid timestamp is not unique across runs or sources. - Use
--include-rawonly when provider-native payloads are needed. Raw JSON can substantially increase database size and may contain data you do not want to retain. - Enable foreign keys on every external SQLite connection that performs deletes or relationship-sensitive writes.
- Copy or back up the database before applying retention deletes or upgrading across schema-changing releases.
- For sustained multi-process ingestion or warehouse-scale history, use a response hook to stream the normalized response to a database designed for that workload. The bundled SQLite store favors a local CLI and analysis workflow.