Files
weatherapi/docs/internal/postgres-repository.md

165 lines
5.7 KiB
Markdown

# Postgres Repository
This document describes the outbound read adapter under
`internal/adapters/outbound/postgres`. It documents repository behavior and
storage assumptions without duplicating full schema documentation.
## Purpose
The Postgres repository implements `internal/app.Repository` against
weatherfeeder-owned tables. It reconstructs latest weather resources from SQL
rows and returns canonical weatherfeeder model values or application read
models.
## Inputs and Outputs
Inputs:
- a primary `*sql.DB` selected by runtime composition;
- query contexts from application service calls;
- weatherfeeder-populated Postgres rows.
Outputs:
- latest observation, forecast, discussion, weather story, alert, convective
outlook, and current conditions read models;
- nil data with nil error when the latest resource does not exist;
- contextual errors for query, scan, iteration, and JSON decode failures.
## Boundaries
The repository may:
- own SQL text and query ordering;
- scan rows into adapter-local row structs;
- map SQL nulls to pointers or omitted zero values;
- normalize timestamps to UTC;
- reconstruct child slices in database order.
The repository must not:
- bind HTTP query parameters;
- perform unit conversion or response rounding;
- convert timestamps to requested presentation timezones;
- render templates;
- create or migrate weatherfeeder tables.
## Config Fields Used
The repository does not read config directly. Runtime composition supplies the
primary `*sql.DB` selected from the first configured database entry.
## External Adapters Used
- `database/sql` for query execution and nullable scan types.
- `github.com/lib/pq` through runtime driver registration.
- `weatherfeeder/model` for canonical return values.
- `internal/app` for the repository interface and current-conditions model.
## State
The repository stores only a database handle. It owns no durable data, no
migrations, no polling state, and no retry queue.
## Repository Contract
`Repository` stores a `*sql.DB` and satisfies `app.Repository`.
Every public method first checks that the repository and database are
configured. A nil repository or nil database returns `postgres repository is not
configured`.
Missing parent rows map to `nil, nil`. This is how HTTP endpoints can return
successful responses with `data: null`.
## Route-to-Query Mapping
- `LatestObservation`: latest row from `observations`, then present-weather
rows from `observation_present_weather`.
- `CurrentConditions`: aggregates recent rows from `observations` using the
application-provided observation window.
- `LatestAlertRun`: latest row from `alert_runs`, then child `alerts` and
`alert_references`.
- `LatestHourlyForecast`: latest `forecasts` row where `product = 'hourly'`,
then child `forecast_periods`.
- `LatestNarrativeForecast`: latest `forecasts` row where
`product = 'narrative'`, then child `forecast_periods`.
- `LatestForecastDiscussion`: latest row from `forecast_discussions`, then
child `forecast_discussion_key_messages`.
- `LatestWeatherStoryRun`: latest row from `weather_story_runs`, then child
`weather_stories`.
- `LatestWeatherStory`: latest individual row from `weather_stories`.
- `LatestConvectiveOutlookRun`: latest row from `outlook_runs`, then child
`outlooks`.
Latest parent rows are selected by descending weather timestamp and
`event_emitted_at` where that tie-breaker is available in the query.
## Child Loading and Ordering
Child queries preserve stored order:
- observation present weather by `weather_index`;
- alerts by `alert_index`;
- alert references by `alert_index`, then `reference_index`;
- forecast periods by `period_index`;
- discussion key messages by `message_index`;
- weather stories by `story_index`;
- outlooks by `outlook_index`.
Alert references are attached after both alert and reference rows are loaded.
References are grouped by alert index and attached to their corresponding alert.
## Null and Timestamp Policy
Row structs use `sql.Null*` types for nullable columns. Mapper helpers convert:
- invalid strings to empty strings;
- invalid booleans, floats, times, and WMO codes to nil pointers;
- valid times to UTC.
Required parent timestamps are normalized to UTC directly in mappers. Optional
times go through `timePtr`, which also normalizes to UTC.
Current conditions return nil when the aggregate sample count is zero.
## JSON Decode Behavior
Observation present-weather rows store raw JSON text. Empty or null text maps
to an empty present-weather value. Invalid JSON returns a contextual decode
error with the weather index.
Outlook rows store `geometry_json` as compact GeoJSON text. The repository
validates and copies the JSON bytes into `json.RawMessage` without parsing or
reserializing the geometry.
## Failure Behavior
Repository methods wrap failures with operation context, for example:
- `query latest observation`
- `query forecast periods`
- `scan forecast period row`
- `iterate weather story rows`
- `decode observation present weather row`
This context should be preserved when adding new reads so operator logs and
tests identify the failing operation.
## Tests to Inspect Before Changing
- `internal/adapters/outbound/postgres/repository_test.go`
- `internal/app/service_test.go` for repository port expectations
- endpoint tests when no-data behavior or returned model shape affects HTTP
responses
## Invariants
- Keep SQL and row structs in the Postgres adapter.
- Keep weatherfeeder schema ownership outside `weatherapi`.
- Preserve `nil, nil` no-data behavior for missing latest parent rows.
- Preserve UTC normalization at the repository boundary.
- Preserve child ordering from stored index columns.
- Preserve contextual error wrapping for query, scan, iteration, and decode
failures.