Add internal architecture documentation
This commit is contained in:
157
docs/internal/postgres-repository.md
Normal file
157
docs/internal/postgres-repository.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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, 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`.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user