Current section
Files
Jump to
Current section
Files
lib/ecto_rollback_guard.ex
defmodule EctoRollbackGuard do
@moduledoc """
Detect destructive operations before rolling back Ecto migrations.
Analyzes migration source files via AST to identify operations that would
cause data loss when reverted — table drops, column removals, irreversible
migrations — and optionally enriches results with live row counts from
PostgreSQL.
## Usage
### In a Release module
defmodule MyApp.Release do
def rollback(repo, version) do
EctoRollbackGuard.log_preview(repo, version)
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
end
### Programmatic
EctoRollbackGuard.detect(source)
#=> [{:drop_table, :users}, {:drop_column, :entities, :mobile_number}]
EctoRollbackGuard.preview(MyApp.Repo, 20230101120000)
#=> {:ok, [%EctoRollbackGuard.Impact{...}]}
### Mix task
mix ecto_rollback_guard.preview --to 20230101120000
"""
alias EctoRollbackGuard.{Detector, Enricher, Impact, Preview, Reporter}
@doc """
Detect rollback operations from migration source text.
Returns a list of operation tuples, or `{:error, reason}` if the source
cannot be parsed.
"""
@spec detect(String.t()) :: [Impact.operation()] | {:error, term()}
defdelegate detect(source), to: Detector
@doc """
Returns `true` if any operation in the list is destructive.
"""
@spec destructive?([Impact.operation()]) :: boolean()
def destructive?(operations) do
Impact.from_operations(0, "", operations).destructive?
end
@doc """
Enrich operations with approximate row counts from PostgreSQL.
"""
@spec enrich([Impact.operation()], Ecto.Repo.t()) :: [Impact.operation()]
defdelegate enrich(operations, repo), to: Enricher
@doc """
Preview the impact of rolling back to `target_version`.
Returns `{:ok, [%Impact{}]}`.
## Options
- `:enrich` — query DB for row counts (default: `true`)
- `:migrations_path` — override migration directory
"""
@spec preview(Ecto.Repo.t(), non_neg_integer(), keyword()) ::
{:ok, [Impact.t()]} | {:error, term()}
def preview(repo, target_version, opts \\ []) do
Preview.preview(repo, target_version, opts)
end
@doc """
Log a formatted rollback impact preview to stdout.
Designed for use in Release modules before executing a rollback.
"""
@spec log_preview(Ecto.Repo.t(), non_neg_integer(), keyword()) :: :ok | {:error, term()}
def log_preview(repo, target_version, opts \\ []) do
result = preview(repo, target_version, opts)
do_log_preview(result)
end
defp do_log_preview({:ok, impacts}) do
IO.puts(Reporter.format_terminal(impacts))
:ok
end
defp do_log_preview({:error, reason}) do
IO.puts("Rollback preview failed: #{inspect(reason)}")
{:error, reason}
end
end