Skip to main content

lib/mix/tasks/recall.rollback.ex

defmodule Mix.Tasks.Recall.Rollback do
  @shortdoc "Rolls back the repository's applied Recall migrations"

  @moduledoc """
  Reverts applied migrations for the given Recall-backed repository.

  The Recall counterpart to `mix ecto.rollback` (see
  `Mix.Tasks.Recall.Migrate` for why a dedicated task exists). Like
  `ecto.rollback`, it rolls back a single migration by default; pass `--all`,
  `--step`, or `--to` to roll back more.

      mix recall.rollback             # rolls back the last migration
      mix recall.rollback --step 3
      mix recall.rollback -r MyApp.Repo --to 20240101000000

  ## Options

    * `-r`, `--repo` — the repo to roll back
    * `--all` — roll back all applied migrations
    * `--step` — roll back N migrations (default 1)
    * `--to` — roll back down to and including a target version
    * `--migrations-path` — a migrations directory (may be given more than once)
    * `--log-level` — the `Logger` level for migration logs (default `info`)
  """

  use Mix.Task

  import Mix.Ecto

  @aliases [r: :repo, n: :step, v: :to]

  @switches [
    all: :boolean,
    step: :integer,
    to: :integer,
    repo: [:string, :keep],
    migrations_path: [:string, :keep],
    log_level: :string
  ]

  @impl true
  def run(args) do
    repos = parse_repo(args)
    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)

    opts =
      opts
      |> default_step()
      |> normalize_log_level()
      |> normalize_paths()

    for repo <- repos do
      ensure_repo(repo, args)

      Ecto.Migrator.with_repo(repo, fn repo ->
        Recall.Migrator.run(repo, :down, opts)
      end)
    end
  end

  # Roll back exactly one migration unless a broader bound was given.
  defp default_step(opts) do
    if Keyword.has_key?(opts, :all) or Keyword.has_key?(opts, :step) or
         Keyword.has_key?(opts, :to) do
      opts
    else
      Keyword.put(opts, :step, 1)
    end
  end

  defp normalize_log_level(opts) do
    case Keyword.pop(opts, :log_level) do
      {nil, opts} -> opts
      {level, opts} -> Keyword.put(opts, :log, String.to_existing_atom(level))
    end
  end

  defp normalize_paths(opts) do
    case Keyword.get_values(opts, :migrations_path) do
      [] -> Keyword.delete(opts, :migrations_path)
      paths -> opts |> Keyword.delete(:migrations_path) |> Keyword.put(:migrations_paths, paths)
    end
  end
end