Skip to main content

lib/mix/tasks/continuum.partitions.create.ex

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(<<year::binary-size(4), "-", month::binary-size(2)>>) 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