All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
293 lines
15 KiB
Go
293 lines
15 KiB
Go
// Package postgres documents weatherfeeder's PostgreSQL sink contract for
|
|
// downstream SQL consumers.
|
|
//
|
|
// This package wires weatherfeeder canonical events into normalized relational
|
|
// tables. Downstream consumers can reconstruct the same canonical JSON objects
|
|
// that were written by joining parent/child tables as described below.
|
|
//
|
|
// Canonical input schemas:
|
|
// - weather.observation.v1 -> model.WeatherObservation
|
|
// - weather.forecast.v1 -> model.WeatherForecastRun
|
|
// - weather.forecast_discussion.v1 -> model.WeatherForecastDiscussion
|
|
// - weather.weather_story.v1 -> model.WeatherStoryRun
|
|
// - weather.alert.v1 -> model.WeatherAlertRun
|
|
// - weather.outlook.v2 -> model.WeatherOutlookRun
|
|
//
|
|
// Parent/child relationships:
|
|
// - observations.event_id -> observation_present_weather.event_id
|
|
// - forecasts.event_id -> forecast_periods.run_event_id
|
|
// - forecast_discussions.event_id -> forecast_discussion_key_messages.run_event_id
|
|
// - weather_story_runs.event_id -> weather_stories.run_event_id
|
|
// - alert_runs.event_id -> alerts.run_event_id
|
|
// - alerts.(run_event_id, alert_index) -> alert_references.(run_event_id, alert_index)
|
|
// - outlook_runs.event_id -> outlooks.run_event_id
|
|
// - outlook_runs.event_id -> outlook_discussions.run_event_id
|
|
//
|
|
// Dedupe and retention behavior:
|
|
// - Parent primary keys (event_id): observations, forecasts, alert_runs, outlook_runs.
|
|
// - Child primary keys use positional indexes to preserve payload order.
|
|
// - Prune columns:
|
|
// - observations.observed_at
|
|
// - observation_present_weather.observed_at
|
|
// - forecasts.issued_at
|
|
// - forecast_periods.issued_at
|
|
// - forecast_discussions.issued_at
|
|
// - forecast_discussion_key_messages.issued_at
|
|
// - weather_story_runs.as_of
|
|
// - weather_stories.as_of
|
|
// - alert_runs.as_of
|
|
// - alerts.as_of
|
|
// - alert_references.as_of
|
|
// - outlook_runs.as_of
|
|
// - outlooks.as_of
|
|
// - outlook_discussions.as_of
|
|
//
|
|
// Envelope field mapping (shared parent columns)
|
|
//
|
|
// These columns exist on parent tables such as observations, forecasts,
|
|
// forecast_discussions, weather_story_runs, alert_runs, and outlook_runs:
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
//
|
|
// Table contract
|
|
//
|
|
// 1. observations (PK: event_id)
|
|
//
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
// - station_id TEXT NULL -> payload.stationId
|
|
// - station_name TEXT NULL -> payload.stationName
|
|
// - observed_at TIMESTAMPTZ -> payload.timestamp
|
|
// - condition_code INTEGER -> payload.conditionCode
|
|
// - is_day BOOLEAN NULL -> payload.isDay
|
|
// - text_description TEXT NULL -> payload.textDescription
|
|
// - temperature_c DOUBLE PRECISION NULL -> payload.temperatureC
|
|
// - dewpoint_c DOUBLE PRECISION NULL -> payload.dewpointC
|
|
// - wind_direction_degrees DOUBLE PRECISION NULL -> payload.windDirectionDegrees
|
|
// - wind_speed_kmh DOUBLE PRECISION NULL -> payload.windSpeedKmh
|
|
// - wind_gust_kmh DOUBLE PRECISION NULL -> payload.windGustKmh
|
|
// - barometric_pressure_pa DOUBLE PRECISION NULL -> payload.barometricPressurePa
|
|
// - visibility_meters DOUBLE PRECISION NULL -> payload.visibilityMeters
|
|
// - relative_humidity_percent DOUBLE PRECISION NULL -> payload.relativeHumidityPercent
|
|
// - apparent_temperature_c DOUBLE PRECISION NULL -> payload.apparentTemperatureC
|
|
//
|
|
// 2. observation_present_weather (PK: event_id, weather_index)
|
|
//
|
|
// - event_id TEXT -> observations.event_id / payload.presentWeather[i]
|
|
// - weather_index INTEGER -> i (array position in payload.presentWeather)
|
|
// - observed_at TIMESTAMPTZ -> payload.timestamp
|
|
// - raw_text TEXT NULL -> JSON-encoded payload.presentWeather[i].raw
|
|
//
|
|
// Note: raw_text stores compact JSON text. Consumers that need the original
|
|
// object should parse raw_text as JSON.
|
|
//
|
|
// 3. forecasts (PK: event_id)
|
|
//
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
// - location_id TEXT NULL -> payload.locationId
|
|
// - location_name TEXT NULL -> payload.locationName
|
|
// - issued_at TIMESTAMPTZ -> payload.issuedAt
|
|
// - updated_at TIMESTAMPTZ NULL -> payload.updatedAt
|
|
// - product TEXT -> payload.product
|
|
// - latitude DOUBLE PRECISION NULL -> payload.latitude
|
|
// - longitude DOUBLE PRECISION NULL -> payload.longitude
|
|
// - elevation_meters DOUBLE PRECISION NULL -> payload.elevationMeters
|
|
// - period_count INTEGER -> len(payload.periods)
|
|
//
|
|
// 4. forecast_periods (PK: run_event_id, period_index)
|
|
//
|
|
// - run_event_id TEXT -> forecasts.event_id / payload.periods[i]
|
|
// - period_index INTEGER -> i (array position in payload.periods)
|
|
// - issued_at TIMESTAMPTZ -> payload.issuedAt (copied from parent)
|
|
// - start_time TIMESTAMPTZ -> payload.periods[i].startTime
|
|
// - end_time TIMESTAMPTZ -> payload.periods[i].endTime
|
|
// - name TEXT NULL -> payload.periods[i].name
|
|
// - is_day BOOLEAN NULL -> payload.periods[i].isDay
|
|
// - condition_code INTEGER NULL -> payload.periods[i].conditionCode
|
|
// - text_description TEXT NULL -> payload.periods[i].textDescription
|
|
// - temperature_c DOUBLE PRECISION NULL -> payload.periods[i].temperatureC
|
|
// - temperature_c_min DOUBLE PRECISION NULL -> payload.periods[i].temperatureCMin
|
|
// - temperature_c_max DOUBLE PRECISION NULL -> payload.periods[i].temperatureCMax
|
|
// - dewpoint_c DOUBLE PRECISION NULL -> payload.periods[i].dewpointC
|
|
// - relative_humidity_percent DOUBLE PRECISION NULL -> payload.periods[i].relativeHumidityPercent
|
|
// - wind_direction_degrees DOUBLE PRECISION NULL -> payload.periods[i].windDirectionDegrees
|
|
// - wind_speed_kmh DOUBLE PRECISION NULL -> payload.periods[i].windSpeedKmh
|
|
// - wind_gust_kmh DOUBLE PRECISION NULL -> payload.periods[i].windGustKmh
|
|
// - barometric_pressure_pa DOUBLE PRECISION NULL -> payload.periods[i].barometricPressurePa
|
|
// - visibility_meters DOUBLE PRECISION NULL -> payload.periods[i].visibilityMeters
|
|
// - apparent_temperature_c DOUBLE PRECISION NULL -> payload.periods[i].apparentTemperatureC
|
|
// - cloud_cover_percent DOUBLE PRECISION NULL -> payload.periods[i].cloudCoverPercent
|
|
// - probability_of_precipitation_percent DOUBLE PRECISION NULL -> payload.periods[i].probabilityOfPrecipitationPercent
|
|
// - precipitation_amount_mm DOUBLE PRECISION NULL -> payload.periods[i].precipitationAmountMm
|
|
// - snowfall_depth_mm DOUBLE PRECISION NULL -> payload.periods[i].snowfallDepthMm
|
|
// - uv_index DOUBLE PRECISION NULL -> payload.periods[i].uvIndex
|
|
//
|
|
// 5. weather_story_runs (PK: event_id)
|
|
//
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
// - office_id TEXT NULL -> payload.officeId
|
|
// - as_of TIMESTAMPTZ -> payload.asOf
|
|
// - story_count INTEGER -> len(payload.stories)
|
|
//
|
|
// 6. weather_stories (PK: run_event_id, story_index)
|
|
//
|
|
// - run_event_id TEXT -> weather_story_runs.event_id / payload.stories[i]
|
|
// - story_index INTEGER -> i (array position in payload.stories)
|
|
// - as_of TIMESTAMPTZ -> payload.asOf (copied from parent)
|
|
// - office_id TEXT NULL -> payload.stories[i].officeId
|
|
// - start_time TIMESTAMPTZ -> payload.stories[i].startTime
|
|
// - end_time TIMESTAMPTZ -> payload.stories[i].endTime
|
|
// - updated_at TIMESTAMPTZ -> payload.stories[i].updatedAt
|
|
// - title TEXT NULL -> payload.stories[i].title
|
|
// - description TEXT NULL -> payload.stories[i].description
|
|
// - alt_text TEXT NULL -> payload.stories[i].altText
|
|
// - priority BOOLEAN -> payload.stories[i].priority
|
|
// - story_order INTEGER -> payload.stories[i].order
|
|
// - download_url TEXT NULL -> payload.stories[i].downloadUrl
|
|
//
|
|
// 7. alert_runs (PK: event_id)
|
|
//
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
// - location_id TEXT NULL -> payload.locationId
|
|
// - location_name TEXT NULL -> payload.locationName
|
|
// - as_of TIMESTAMPTZ -> payload.asOf
|
|
// - latitude DOUBLE PRECISION NULL -> payload.latitude
|
|
// - longitude DOUBLE PRECISION NULL -> payload.longitude
|
|
// - alert_count INTEGER -> len(payload.alerts)
|
|
//
|
|
// 8. alerts (PK: run_event_id, alert_index)
|
|
//
|
|
// - run_event_id TEXT -> alert_runs.event_id / payload.alerts[i]
|
|
// - alert_index INTEGER -> i (array position in payload.alerts)
|
|
// - as_of TIMESTAMPTZ -> payload.asOf (copied from parent)
|
|
// - alert_id TEXT -> payload.alerts[i].id
|
|
// - event TEXT NULL -> payload.alerts[i].event
|
|
// - headline TEXT NULL -> payload.alerts[i].headline
|
|
// - severity TEXT NULL -> payload.alerts[i].severity
|
|
// - urgency TEXT NULL -> payload.alerts[i].urgency
|
|
// - certainty TEXT NULL -> payload.alerts[i].certainty
|
|
// - status TEXT NULL -> payload.alerts[i].status
|
|
// - message_type TEXT NULL -> payload.alerts[i].messageType
|
|
// - category TEXT NULL -> payload.alerts[i].category
|
|
// - response TEXT NULL -> payload.alerts[i].response
|
|
// - description TEXT NULL -> payload.alerts[i].description
|
|
// - instruction TEXT NULL -> payload.alerts[i].instruction
|
|
// - sent TIMESTAMPTZ NULL -> payload.alerts[i].sent
|
|
// - effective TIMESTAMPTZ NULL -> payload.alerts[i].effective
|
|
// - onset TIMESTAMPTZ NULL -> payload.alerts[i].onset
|
|
// - ends TIMESTAMPTZ NULL -> payload.alerts[i].ends
|
|
// - expires TIMESTAMPTZ NULL -> payload.alerts[i].expires
|
|
// - area_description TEXT NULL -> payload.alerts[i].areaDescription
|
|
// - sender_name TEXT NULL -> payload.alerts[i].senderName
|
|
// - reference_count INTEGER -> len(payload.alerts[i].references)
|
|
//
|
|
// 9. alert_references (PK: run_event_id, alert_index, reference_index)
|
|
//
|
|
// - run_event_id TEXT -> alert_runs.event_id / payload.alerts[i].references[j]
|
|
// - alert_index INTEGER -> i (array position in payload.alerts)
|
|
// - reference_index INTEGER -> j (array position in payload.alerts[i].references)
|
|
// - as_of TIMESTAMPTZ -> payload.asOf (copied from parent)
|
|
// - id TEXT NULL -> payload.alerts[i].references[j].id
|
|
// - identifier TEXT NULL -> payload.alerts[i].references[j].identifier
|
|
// - sender TEXT NULL -> payload.alerts[i].references[j].sender
|
|
// - sent TIMESTAMPTZ NULL -> payload.alerts[i].references[j].sent
|
|
//
|
|
// 10. outlook_runs (PK: event_id)
|
|
//
|
|
// - event_id TEXT -> event.id
|
|
// - event_kind TEXT -> event.kind
|
|
// - event_source TEXT -> event.source
|
|
// - event_schema TEXT -> event.schema
|
|
// - event_emitted_at TIMESTAMPTZ -> event.emitted_at
|
|
// - event_effective_at TIMESTAMPTZ NULL -> event.effective_at
|
|
// - location_id TEXT NULL -> payload.locationId
|
|
// - location_name TEXT NULL -> payload.locationName
|
|
// - latitude DOUBLE PRECISION NULL -> payload.latitude
|
|
// - longitude DOUBLE PRECISION NULL -> payload.longitude
|
|
// - as_of TIMESTAMPTZ -> payload.asOf
|
|
// - issued_at TIMESTAMPTZ NULL -> payload.issuedAt
|
|
// - outlook_count INTEGER -> len(payload.outlooks)
|
|
// - discussion_count INTEGER -> len(payload.discussions)
|
|
//
|
|
// 11. outlooks (PK: run_event_id, outlook_index)
|
|
//
|
|
// - run_event_id TEXT -> outlook_runs.event_id / payload.outlooks[i]
|
|
// - outlook_index INTEGER -> i (array position in payload.outlooks)
|
|
// - as_of TIMESTAMPTZ -> payload.asOf (copied from parent)
|
|
// - outlook_id TEXT -> payload.outlooks[i].id
|
|
// - provider TEXT -> payload.outlooks[i].provider
|
|
// - product TEXT -> payload.outlooks[i].product
|
|
// - day INTEGER -> payload.outlooks[i].day
|
|
// - outlook_type TEXT -> payload.outlooks[i].outlookType
|
|
// - label TEXT -> payload.outlooks[i].label
|
|
// - label_text TEXT NULL -> payload.outlooks[i].labelText
|
|
// - severity_rank INTEGER NULL -> payload.outlooks[i].severityRank
|
|
// - valid_from TIMESTAMPTZ -> payload.outlooks[i].validFrom
|
|
// - valid_to TIMESTAMPTZ -> payload.outlooks[i].validTo
|
|
// - issued_at TIMESTAMPTZ -> payload.outlooks[i].issuedAt
|
|
// - expires_at TIMESTAMPTZ -> payload.outlooks[i].expiresAt
|
|
// - forecaster TEXT NULL -> payload.outlooks[i].forecaster
|
|
// - source_url TEXT NULL -> payload.outlooks[i].sourceUrl
|
|
// - image_url TEXT NULL -> payload.outlooks[i].imageUrl
|
|
// - contains_location BOOLEAN -> payload.outlooks[i].containsLocation
|
|
// - geometry_json TEXT -> compact JSON payload.outlooks[i].geometry
|
|
//
|
|
// 12. outlook_discussions (PK: run_event_id, discussion_index)
|
|
//
|
|
// - run_event_id TEXT -> outlook_runs.event_id / payload.discussions[i]
|
|
// - discussion_index INTEGER -> i (array position in payload.discussions)
|
|
// - as_of TIMESTAMPTZ -> payload.asOf (copied from parent)
|
|
// - day INTEGER -> payload.discussions[i].day
|
|
// - headline TEXT NULL -> payload.discussions[i].headline
|
|
// - summary TEXT NULL -> payload.discussions[i].summary
|
|
// - discussion TEXT NULL -> payload.discussions[i].discussion
|
|
// - updated_at TIMESTAMPTZ NULL -> payload.discussions[i].updatedAt
|
|
//
|
|
// Reconstructing canonical JSON payloads
|
|
//
|
|
// - WeatherObservation:
|
|
// read one row from observations, then join child rows by event_id ordered by
|
|
// weather_index to rebuild presentWeather arrays.
|
|
//
|
|
// - WeatherForecastRun:
|
|
// read one row from forecasts, then join forecast_periods by run_event_id
|
|
// ordered by period_index to rebuild periods.
|
|
//
|
|
// - WeatherStoryRun:
|
|
// read one row from weather_story_runs, then join weather_stories by
|
|
// run_event_id ordered by story_index to rebuild stories.
|
|
//
|
|
// - WeatherAlertRun:
|
|
// read one row from alert_runs, join alerts by run_event_id ordered by
|
|
// alert_index, then join alert_references by (run_event_id, alert_index)
|
|
// ordered by reference_index to rebuild references per alert.
|
|
//
|
|
// - WeatherOutlookRun:
|
|
// read one row from outlook_runs, join outlooks by run_event_id ordered by
|
|
// outlook_index to rebuild outlooks, then join outlook_discussions by
|
|
// run_event_id ordered by discussion_index to rebuild discussions.
|
|
package postgres
|