defmodule Ecto.AutoMigrator do
@callback create!(any) :: :ok
@callback create_repo(atom(), any) :: :ok | {:error, any}
@callback migrate!(any) :: :ok
@callback migrate_repo(atom(), any) :: :ok | {:error, any}
@callback recreate_repo(atom(), any) :: :ok | {:error, any}
@callback rollback(atom, any) :: {:ok, any, any} | {:error, any}
@callback load_app :: :ok | {:error, any}
@callback repos(any) :: list(atom())
@callback run_migrations? :: boolean
defmacro __using__(_opts) do
quote do
@behaviour Ecto.AutoMigrator
if Module.get_attribute(__MODULE__, :doc) == nil do
@doc """
Automatic migrations module which can be added to the supervision tree
"""
end
use GenServer
require Logger
@app Mix.Project.config()[:app]
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
@impl true
@spec init(any) :: :ignore
def init(args) do
Logger.info("Starting #{inspect(__MODULE__)}")
if run_migrations?() do
create!(args)
migrate!(args)
end
:ignore
end
@doc """
Equiv of mix ecto.create
"""
def create!(args) do
Logger.info("Ensuring repos are created")
load_app()
Enum.each(repos(args), fn repo ->
# Attempt to ensure that the DBs are created before progressing
create_repo(repo, args)
end)
:ok
end
@doc """
Do the creation/init of a single repo
"""
def create_repo(repo, args \\ nil)
def create_repo(repo, _args) do
Logger.info("About to create: #{inspect(repo)}")
try do
repo.__adapter__.storage_up(repo.config)
rescue
e ->
Logger.error("Error creating Repo: #{inspect(repo)} got #{inspect(e)}")
{:error, e}
else
_ -> :ok
end
end
@doc """
Delete and then recreate repo
"""
def recreate_repo(repo, args) do
try do
repo.__adapter__.storage_down(repo.config)
repo.__adapter__.storage_up(repo.config)
rescue
e ->
Logger.error("Error deleting and recreating Repo: #{inspect(repo)} got #{inspect(e)}")
{:error, e}
else
_ ->
# Purge all in use connections or we will still be using the old DB files
repo.stop(5)
end
end
@doc """
Equiv of mix ecto.migrate
"""
def migrate!(args) do
Logger.info("Ensuring repos are migrated")
load_app()
for repo <- repos(args) do
:ok = migrate_repo(repo, args)
end
:ok
end
@doc """
Do the migration of a single repo
"""
def migrate_repo(repo, args \\ nil)
def migrate_repo(repo, _args) do
Logger.info("Running migrations for: #{inspect(repo)}")
try do
Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
rescue
e ->
Logger.error("Error migrating Repo: #{inspect(repo)} got #{inspect(e)}")
{:error, e}
else
{:ok, _, _} -> :ok
{:error, term} -> {:error, term}
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
@doc """
Load all modules, etc needed for our migration to be able to complete successfully
"""
def load_app() do
Application.load(@app)
end
@doc """
The list of modules we will operate against
Defaults to the same as mix ecto.migrate, ie list of repos from:
Application.fetch_env!(:my_app, :ecto_repos)
"""
def repos(repos \\ nil)
def repos(%{repos: repos}), do: repos
def repos(_args) do
Application.fetch_env!(@app, :ecto_repos)
end
@doc """
Whether to run migrations or not
Defaults to Application.fetch_env(:my_app, :run_migrations), set this in you config, eg:
config :my_app, run_migrations: System.get_env("RUN_MIGRATIONS") || Mix.env == :test
"""
def run_migrations?() do
case Application.fetch_env(@app, :run_migrations) do
{:ok, val} -> !!val
:error -> false
end
end
defoverridable Ecto.AutoMigrator
end
end
end