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.
151 lines
5.1 KiB
Elixir
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
|