# 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`. This is the latest stored alert snapshot. The repository maps both `ends` and `expires`; active-time filtering is performed by the application service. - `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.