lib/auto_migrator.ex

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