Stage 2 of the HRDPS plan. Mirror of hrrr_profiles for HRDPS-derived
rows so HRDPS can be dropped/reprocessed without disturbing the 4500+
HRRR profiles already in prod. Same column set, same indexes,
PARTITION BY RANGE (valid_time) with quarterly partitions starting at
the deploy quarter.
Schema follows the @primary_key {:id, :binary_id, autogenerate: true}
convention and the same set of derived fields HrrrProfile carries
(surface_refractivity, min_refractivity_gradient, ducting_detected,
duct_characteristics, etc.) so the rest of the pipeline can consume
HRDPS-derived rows interchangeably with HRRR-derived rows.
Unique constraint on (lat, lon, valid_time) is enforced at the DB
level via the partitioned index; schema-level unique_constraint/2
matching against changeset errors doesn't apply because the
partition's index name varies per partition. Production upserts go
through `on_conflict: {:replace_all_except, ...}` per project
convention.
83 lines
2.9 KiB
Elixir
83 lines
2.9 KiB
Elixir
defmodule Microwaveprop.Weather.HrdpsProfileTest do
|
|
use Microwaveprop.DataCase, async: false
|
|
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather.HrdpsProfile
|
|
|
|
describe "changeset/2" do
|
|
test "valid with required fields" do
|
|
attrs = %{valid_time: ~U[2026-04-29 12:00:00Z], lat: 53.32, lon: -60.42}
|
|
cs = HrdpsProfile.changeset(%HrdpsProfile{}, attrs)
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "rejects missing required fields" do
|
|
cs = HrdpsProfile.changeset(%HrdpsProfile{}, %{})
|
|
refute cs.valid?
|
|
assert %{valid_time: ["can't be blank"]} = errors_on(cs)
|
|
assert %{lat: ["can't be blank"]} = errors_on(cs)
|
|
assert %{lon: ["can't be blank"]} = errors_on(cs)
|
|
end
|
|
|
|
test "casts the full set of derived fields" do
|
|
attrs = %{
|
|
valid_time: ~U[2026-04-29 12:00:00Z],
|
|
lat: 53.32,
|
|
lon: -60.42,
|
|
run_time: ~U[2026-04-29 12:00:00Z],
|
|
profile: [%{"pres" => 1000.0, "tmpc" => 12.0, "dwpc" => 7.0, "hght" => 100.0}],
|
|
hpbl_m: 800.0,
|
|
pwat_mm: nil,
|
|
surface_temp_c: 12.0,
|
|
surface_dewpoint_c: 7.0,
|
|
surface_pressure_mb: 1000.0,
|
|
surface_refractivity: 312.0,
|
|
min_refractivity_gradient: -50.0,
|
|
ducting_detected: false,
|
|
duct_characteristics: [],
|
|
is_grid_point: true
|
|
}
|
|
|
|
cs = HrdpsProfile.changeset(%HrdpsProfile{}, attrs)
|
|
assert cs.valid?
|
|
assert Ecto.Changeset.get_field(cs, :surface_refractivity) == 312.0
|
|
assert Ecto.Changeset.get_field(cs, :is_grid_point) == true
|
|
end
|
|
end
|
|
|
|
describe "persistence" do
|
|
test "inserts and reads back via the partitioned hrdps_profiles table" do
|
|
attrs = %{
|
|
valid_time: ~U[2026-04-29 12:00:00Z],
|
|
lat: 53.32,
|
|
lon: -60.42,
|
|
run_time: ~U[2026-04-29 12:00:00Z],
|
|
surface_temp_c: 7.8
|
|
}
|
|
|
|
{:ok, %HrdpsProfile{id: id}} =
|
|
%HrdpsProfile{} |> HrdpsProfile.changeset(attrs) |> Repo.insert()
|
|
|
|
reloaded = Repo.get!(HrdpsProfile, id)
|
|
assert reloaded.lat == 53.32
|
|
assert reloaded.surface_temp_c == 7.8
|
|
end
|
|
|
|
test "enforces unique constraint on (lat, lon, valid_time) at the DB level" do
|
|
# The partitioned table propagates the unique index per partition, so
|
|
# Postgres rejects duplicate (lat, lon, valid_time) inserts. Schema-
|
|
# level `unique_constraint/2` matching against changeset errors
|
|
# doesn't apply because the partition's index name varies per
|
|
# partition; callers that need upserts go through
|
|
# `on_conflict: {:replace_all_except, ...}` in the worker.
|
|
attrs = %{valid_time: ~U[2026-04-29 12:00:00Z], lat: 53.32, lon: -60.42}
|
|
|
|
assert {:ok, _} =
|
|
%HrdpsProfile{} |> HrdpsProfile.changeset(attrs) |> Repo.insert()
|
|
|
|
assert_raise Ecto.ConstraintError, fn ->
|
|
%HrdpsProfile{} |> HrdpsProfile.changeset(attrs) |> Repo.insert()
|
|
end
|
|
end
|
|
end
|
|
end
|