Current section
Files
Jump to
Current section
Files
lib/mix/tenant.ex
defmodule Mix.Tenant do
@moduledoc """
Useful functions for any tenant mix task.
Here is the list of tasks we have for now:
- [`mix tenant.gen.migration`](./Mix.Tasks.Tenant.Gen.Migration.html) -
generates a tenant migration for the repo
- [`mix tenant.migrate`](./Mix.Tasks.Tenant.Migrate.html) -
runs the repository tenant migrations
- [`mix tenant.migrations`](./Mix.Tasks.Tenant.Migrations.html) -
displays the repository migration status
- [`mix tenant.rollback`](./Mix.Tasks.Tenant.Rollback.html) -
rolls back the repository tenant migrations
"""
alias Ecto.Migrator
alias Logger.App
alias Mix.Ecto
alias Mix.Project
@aliases [
r: :repo,
n: :step
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
start: :boolean,
quiet: :boolean,
prefix: :string,
pool_size: :integer,
log_sql: :boolean,
strict_version_order: :boolean,
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean
]
@doc """
Ensures the migrations path exists for the given `repo`.
Returns the path for the `repo` tenant migrations folder if succeeds
or `Mix.raise`'s if it fails.
"""
def ensure_tenant_migrations_path(repo) do
path = Path.join(source_repo_priv(repo), Tenant.config().migrations_path)
if not Project.umbrella?() and not File.dir?(path) do
raise_missing_migrations(Path.relative_to_cwd(path), repo)
end
path
end
@doc """
Generates a nicely formatted timestamp
"""
def timestamp() do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
defp raise_missing_migrations(path, repo) do
Mix.raise("""
Could not find migrations directory #{inspect(path)}
for repo #{inspect(repo)}.
This may be because you are in a new project and the
migration directory has not been created yet. Creating an
empty directory at the path above will fix this error.
If you expected existing migrations to be found, please
make sure your repository has been properly configured
and the configured path exists.
""")
end
@doc """
Runs the tenant migrations with the given `args` and using
`migrator` function.
"""
def run_tenant_migrations(args, direction, migrator \\ &Migrator.run/4) do
repos = Ecto.parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :step, 1)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_sql: false),
else: opts
Enum.each(repos, &run_tenant_migrations(&1, args, opts, direction, migrator))
end
defp run_tenant_migrations(repo, args, opts, direction, migrator) do
Ecto.ensure_repo(repo, args)
path = ensure_tenant_migrations_path(repo)
{:ok, pid, apps} = ensure_started(repo, opts)
pool = repo.config()[:pool]
Code.compiler_options(ignore_module_conflict: true)
migrated =
Enum.flat_map(Tenant.all(repo), fn tenant ->
opts = Keyword.put(opts, :prefix, tenant)
if function_exported?(pool, :unboxed_run, 2) do
pool.unboxed_run(repo, fn -> migrator.(repo, path, direction, opts) end)
else
migrator.(repo, path, direction, opts)
end
end)
Code.compiler_options(ignore_module_conflict: false)
pid && repo.stop()
restart_apps_if_migrated(apps, migrated)
end
@doc """
Returns the private repository path relative to the source.
"""
def source_repo_priv(repo) do
config = repo.config()
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
app = Keyword.fetch!(config, :otp_app)
Path.join(Project.deps_paths()[app] || File.cwd!(), priv)
end
@doc """
Ensures the given repository is started and running.
"""
@spec ensure_started(Ecto.Repo.t(), Keyword.t()) :: {:ok, pid | nil, [atom]}
def ensure_started(repo, opts) do
{:ok, started} = Application.ensure_all_started(:ecto_sql)
# If we starting EctoSQL just now, assume
# logger has not been properly booted yet.
if :ecto_sql in started && Process.whereis(Logger) do
backends = Application.get_env(:logger, :backends, [])
try do
App.stop()
Application.put_env(:logger, :backends, [:console])
:ok = App.start()
after
Application.put_env(:logger, :backends, backends)
end
end
{:ok, apps} = repo.__adapter__().ensure_all_started(repo.config(), :temporary)
pool_size = Keyword.get(opts, :pool_size, 2)
case repo.start_link(pool_size: pool_size) do
{:ok, pid} ->
{:ok, pid, apps}
{:error, {:already_started, _pid}} ->
{:ok, nil, apps}
{:error, error} ->
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
end
end
@doc """
Restarts the app if there was any migration command.
"""
@spec restart_apps_if_migrated([atom], list()) :: :ok
def restart_apps_if_migrated(_apps, []), do: :ok
def restart_apps_if_migrated(apps, [_ | _]) do
# Silence the logger to avoid application down messages.
Logger.remove_backend(:console)
for app <- Enum.reverse(apps) do
Application.stop(app)
end
for app <- apps do
Application.ensure_all_started(app)
end
:ok
after
Logger.add_backend(:console, flush: true)
end
defp pad(i), do: i |> to_string() |> String.pad_leading(2, "0")
end