prop/test/microwaveprop/partition_manager_property_test.exs
Graham McIntire 7ee19c5b6a
perf+test: cache nearest_hrrr per cell, batch pg_inherits, +property tests
Two performance fixes the property tests pinned down:

* CalibrationSampler.build_for_hour/1 walked every cell through
  nearest_hrrr/3 twice — once in the producer and again in
  build_sample. Now computed once into a (band, lat, lon) → hrrr_or_nil
  map, halving the O(cells × profiles) scan (~10M comparisons saved
  per fire at typical sizes).
* PartitionManager.ensure_quarterly_partitions/2 issued one
  pg_inherits scan per (parent × lookahead). Refactored to one scan
  per parent with in-memory coverage check across all quarters.

Property tests for both modules:

* PartitionManager: tile-coverage invariant (no gaps, no overlaps),
  exact 3-month bounds, name format, multi-call idempotence,
  multi-parent independence — caught a real NaiveDateTime sort bug
  in the original test helpers. Default Enum.sort_by/2 falls back to
  term comparison on NaiveDateTime; now uses NaiveDateTime as the
  comparator module.
* CalibrationSampler: enqueued points = unique missing midpoints,
  empty enqueue when fully covered, sample row count invariant under
  HRRR availability.

Saved operational gotchas to CLAUDE.md (Oban leader election spans
all app=prop pods, kubectl exec deploy/prop ambiguity, half-year
partition coexistence, HRRR f000 publish lag).

3310 tests + 228 properties, 0 failures.
2026-05-05 17:40:13 -05:00

151 lines
5.1 KiB
Elixir

defmodule Microwaveprop.PartitionManagerPropertyTest do
@moduledoc """
Properties of the partition tiling produced by
`Microwaveprop.PartitionManager.ensure_quarterly_partitions/2`. The
goal is to pin down the math invariants (no gaps, no overlaps,
exact 3-month bounds, deterministic naming) so refactors of the
internal quarter helpers can't silently break them.
All tests use a synthetic parent created inside the sandbox
transaction so DDL rolls back cleanly when the test finishes.
"""
use Microwaveprop.DataCase, async: true
use ExUnitProperties
alias Microwaveprop.PartitionManager
alias Microwaveprop.Repo
defp synthetic_parent do
"test_partitioned_#{System.unique_integer([:positive])}"
end
defp create_partitioned_parent!(name) do
Repo.query!("""
CREATE TABLE public."#{name}" (
valid_time timestamp without time zone NOT NULL,
payload integer
) PARTITION BY RANGE (valid_time)
""")
end
defp partitions_of(parent) do
%Postgrex.Result{rows: rows} =
Repo.query!(
"""
SELECT child.relname, pg_get_expr(child.relpartbound, child.oid)
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = $1
""",
[parent]
)
rows
|> Enum.map(fn [name, bound] ->
[_, lo, hi] = Regex.run(~r/FROM \('([^']+)'\) TO \('([^']+)'\)/, bound)
{name, NaiveDateTime.from_iso8601!(lo), NaiveDateTime.from_iso8601!(hi)}
end)
# Default `Enum.sort_by/2` uses `Kernel.<=/2` which falls back to
# struct-field order on NaiveDateTime — NOT chronological. Pass
# NaiveDateTime as the comparator module so sort_by uses
# NaiveDateTime.compare/2.
|> Enum.sort_by(fn {_, lo, _} -> lo end, NaiveDateTime)
end
property "partitions tile the lookahead range with no gaps and no overlaps" do
check all(lookahead <- integer(0..8), max_runs: 25) do
parent = synthetic_parent()
create_partitioned_parent!(parent)
_ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
partitions = partitions_of(parent)
assert length(partitions) == lookahead + 1
partitions
|> Enum.chunk_every(2, 1, :discard)
|> Enum.each(fn [{name_a, _, hi_a} = a, {name_b, lo_b, _} = b] ->
# Adjacent partitions must touch exactly: end of A == start of B.
assert NaiveDateTime.compare(hi_a, lo_b) == :eq,
"gap/overlap: #{name_a} #{inspect(a)}#{name_b} #{inspect(b)}"
end)
end
end
property "every partition spans exactly one quarter (89-92 days)" do
check all(lookahead <- integer(0..6), max_runs: 15) do
parent = synthetic_parent()
create_partitioned_parent!(parent)
_ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
Enum.each(partitions_of(parent), fn {_, lo, hi} ->
days = NaiveDateTime.diff(hi, lo, :day)
assert days in 89..92, "quarter spanned #{days} days"
# Quarter starts on the 1st of Jan/Apr/Jul/Oct at midnight.
assert lo.day == 1
assert lo.hour == 0
assert lo.minute == 0
assert lo.second == 0
assert lo.month in [1, 4, 7, 10]
end)
end
end
property "partition names follow <parent>_YYYY_MM and are unique" do
check all(lookahead <- integer(0..6), max_runs: 15) do
parent = synthetic_parent()
create_partitioned_parent!(parent)
_ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
names = Enum.map(partitions_of(parent), fn {n, _, _} -> n end)
Enum.each(names, fn n ->
assert Regex.match?(~r/^#{parent}_\d{4}_(01|04|07|10)$/, n),
"bad partition name: #{n}"
end)
assert length(names) == length(Enum.uniq(names)), "duplicate partition names"
end
end
property "is idempotent — repeated calls are stable" do
check all(
lookahead <- integer(0..4),
extra_runs <- integer(1..3),
max_runs: 10
) do
parent = synthetic_parent()
create_partitioned_parent!(parent)
_ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
first = partitions_of(parent)
Enum.each(1..extra_runs, fn _ ->
results = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
# Every entry on a re-run must be :exists — never :created.
assert Enum.all?(results, fn {_, _, status} -> status == :exists end)
end)
assert partitions_of(parent) == first
end
end
property "respects multi-parent calls (independent tiles per parent)" do
check all(
lookahead <- integer(0..3),
n_parents <- integer(1..3),
max_runs: 8
) do
parents = Enum.map(1..n_parents, fn _ -> synthetic_parent() end)
Enum.each(parents, &create_partitioned_parent!/1)
_ = PartitionManager.ensure_quarterly_partitions(lookahead, parents)
Enum.each(parents, fn p ->
assert length(partitions_of(p)) == lookahead + 1
end)
end
end
end