Current section
Files
Jump to
Current section
Files
lib/data_migration/runner.ex
defmodule EctoSparkles.DataMigration.Runner do
@moduledoc """
Runs a `DataMigration`
"""
import Ecto.Query
# alias EctoSparkles.DataMigration
@spec run(module(), keyword()) :: :ok | no_return()
def run(migration_module, opts \\ []) do
config = apply_config(migration_module, opts)
config = %{config | repo: migration_repo(config.repo)}
if config.async do
Task.start(fn ->
throttle_change_in_batches(migration_module, config, config.first_id, opts)
end)
:ok
else
throttle_change_in_batches(migration_module, config, config.first_id, opts)
end
end
# Use the repo from Ecto's migration runner when available (so data migrations
# run against the correct repo, e.g. TestInstanceRepo), falling back to config.
defp migration_repo(fallback_repo) do
case ProcessTree.get(:ecto_migration) do
%{runner: _} ->
repo = Ecto.Migration.Runner.repo()
IO.puts("DataMigration: Using repo from Ecto.Migration.Runner: #{inspect(repo)}")
repo
_ ->
IO.puts("Warning: DataMigration is running outside of Ecto's migration runner; using repo from Ecto config. This may not be the correct repo in some contexts, e.g. when running tests with multiple repos.")
fallback_repo
end
end
defp throttle_change_in_batches(migration_module, config, last_id, opts, batch_i \\ 1)
defp throttle_change_in_batches(_migration_module, _, nil, _opts, _i), do: :ok
defp throttle_change_in_batches(migration_module, config, last_id, opts, batch_i) do
query =
apply_base_query(migration_module, opts)
|> where([i], i.id > ^last_id)
|> order_by([i], asc: i.id)
|> limit(^config.batch_size)
case config.repo.all(query, log: :info, timeout: :infinity) do
[] ->
IO.puts("DataMigration: Done")
# Occurs when no more elements match the query; the migration is done!
:ok
query_results ->
IO.puts("DataMigration: Start batch #{batch_i} - above ID #{last_id}")
apply_migrate(migration_module, query_results, opts)
Process.sleep(config.throttle_ms)
last_processed_id = List.last(query_results).id
throttle_change_in_batches(migration_module, config, last_processed_id, opts, batch_i + 1)
end
end
# `config/0` | `config/1`, `base_query/0` | `base_query/1`, `migrate/1` | `migrate/2` — call the
# opts-aware arity when the migration defines it, otherwise fall back to the plain one.
defp apply_config(module, opts) do
if function_exported?(module, :config, 1), do: module.config(opts), else: module.config()
end
defp apply_base_query(module, opts) do
if function_exported?(module, :base_query, 1),
do: module.base_query(opts),
else: module.base_query()
end
defp apply_migrate(module, results, opts) do
if function_exported?(module, :migrate, 2),
do: module.migrate(results, opts),
else: module.migrate(results)
end
end