defmodule Mix.Tasks.Continuum.Partitions.Create do @moduledoc """ Creates a monthly `continuum_events` partition. mix continuum.partitions.create mix continuum.partitions.create 2026-06 mix continuum.partitions.create 2026-06 --repo MyApp.Repo The task is idempotent. Without a month argument it creates the current UTC month partition. """ use Mix.Task @shortdoc "Creates a monthly continuum_events partition" @impl true def run(args) do {opts, rest, _} = OptionParser.parse(args, switches: [repo: :string]) Mix.Task.run("app.start") repo = parse_repo(opts) month = parse_month(List.first(rest)) create_partition(repo, month) Mix.shell().info("Created #{partition_name(month)}") end defp create_partition(repo, month) do next_month = month |> Date.add(32) |> Date.beginning_of_month() repo.query!(""" CREATE TABLE IF NOT EXISTS #{quote_ident(partition_name(month))} PARTITION OF continuum_events FOR VALUES FROM ('#{Date.to_iso8601(month)} 00:00:00+00') TO ('#{Date.to_iso8601(next_month)} 00:00:00+00') """) end defp parse_repo(opts) do case opts[:repo] do nil -> Application.get_env(:continuum, :repo) || Mix.raise("no repo configured. Pass --repo MyApp.Repo or set :continuum, :repo") repo -> Module.concat([repo]) end end defp parse_month(nil) do today = Date.utc_today() Date.new!(today.year, today.month, 1) end defp parse_month(<>) do with {year, ""} <- Integer.parse(year), {month, ""} <- Integer.parse(month), {:ok, date} <- Date.new(year, month, 1) do date else _ -> Mix.raise("month must be in YYYY-MM format") end end defp parse_month(_), do: Mix.raise("month must be in YYYY-MM format") defp partition_name(%Date{year: year, month: month}) do "continuum_events_y#{year}_m#{pad2(month)}" end defp quote_ident(name), do: ~s("#{String.replace(name, ~s("), ~s(""))}") defp pad2(month) when month < 10, do: "0#{month}" defp pad2(month), do: "#{month}" end