# 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` and `outlook_discussions`.

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`;
- outlook discussions by `discussion_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.
