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 _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