lib/mix/tasks/ecto.gen.view_migration.ex

defmodule Mix.Tasks.Ecto.Gen.ViewMigration do
  use Mix.Task, async: true

  import Macro, only: [camelize: 1, underscore: 1]
  import Mix.Generator
  import Mix.Ecto
  import Mix.EctoSQL

  @moduledoc """
  Generates a view migration.

  Uses the same configuration as `Mix.Ecto.Gen.Migration`.

  ## Examples

      $ mix ecto.gen.view_migration posts_stats
      $ mix ecto.gen.view_migration post_stats -r Custom.Repo

  This will generate a migration file and a SQL file (in `priv/repo/sql`).
  The SQL file should be edited to contain the code for your view.

  ## Command line options
    * `-r`, `--repo` - the repo to generate migration for
    * `--migrations-path` - the path to run the migrations from, defaults to `priv/repo/migrations`
    * `--sql-path` - the path to store the sql files in, defaults to `priv/repo/sql`
  """

  @switches [migrations_path: :string, sql_path: :string]
  @aliases [r: :repo]

  @impl true
  def run(args) do
    repos = parse_repo(args)

    Enum.map(repos, fn repo ->
      case OptionParser.parse!(args, switches: @switches, aliases: @aliases) do
        {opts, [name]} ->
          ensure_repo(repo, args)

          ts = opts[:timestamp] || timestamp()

          sql_path = opts[:sql_path] || Path.join(source_repo_priv(repo), "sql")
          sql_basename = "#{underscore(name)}_#{ts}.sql"
          sql_file = Path.join(sql_path, sql_basename)
          unless File.dir?(sql_path), do: create_directory(sql_path)

          prev_sql =
            Path.join(sql_path, "#{underscore(name)}_*.sql")
            |> Path.wildcard()
            |> Enum.sort(:desc)
            |> List.first()

          prev_sql_basename = if prev_sql, do: Path.basename(prev_sql)

          create_file(sql_file, sql_template(name: name))

          migration_path =
            opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")

          migration_basename = "#{ts}_load_sql_#{underscore(name)}.exs"
          migration_file = Path.join(migration_path, migration_basename)
          unless File.dir?(migration_path), do: create_directory(migration_path)

          assigns = [
            name: name,
            sql_basename: sql_basename,
            prev_sql_basename: prev_sql_basename,
            mod: Module.concat([repo, Migrations, camelize(name)])
          ]

          create_file(migration_file, migration_template(assigns))

          if open?(sql_file) and Mix.shell().yes?("Do you want to run this migration?") do
            Mix.Task.run("ecto.migrate", [
              "-r",
              inspect(repo),
              "--migrations-path",
              migration_path
            ])
          end

          {sql_file, migration_file}

        {_, _} ->
          Mix.raise(
            "expected ecto.gen.view_migration to receive the view file name, " <>
              "got: #{inspect(Enum.join(args, " "))}"
          )
      end
    end)
  end

  defp migration_module do
    case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
      migration_module when is_atom(migration_module) -> migration_module
      other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
    end
  end

  defp app_name do
    Mix.Project.config()[:app]
  end

  defp timestamp do
    {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
    "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
  end

  defp pad(i) when i < 10, do: <<?0, ?0 + i>>
  defp pad(i), do: to_string(i)

  embed_template :sql, """
  create or replace view <%= @name %> as
  -- insert your view here
  """

  embed_template :migration, """
  defmodule <%= inspect @mod %> do
    use <%= inspect migration_module() %>
    def up do
      sql = Path.join(:code.priv_dir(<%= inspect app_name %>), "repo/sql/<%= @sql_basename %>") |> File.read!()
      execute(sql)
    end

    def down do
      sql =<%= if @prev_sql_basename do %> Path.join(:code.priv_dir(<%= inspect app_name() %>), "repo/sql/<%= @prev_sql_basename %>") |> File.read!()<% else %> "drop view if exists <%= @name %>"<% end %>
      execute(sql)
    end
  end
  """
end