Packages
ecto_sql
3.14.0
3.14.0
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.1
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.2
3.10.1
3.10.0
3.9.2
3.9.1
3.9.0
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
SQL-based adapters for Ecto and database migrations
Current section
Files
Jump to
Current section
Files
lib/ecto/migration/runner.ex
defmodule Ecto.Migration.Runner do
@moduledoc false
use Agent, restart: :temporary
require Logger
alias Ecto.Migration.Table
alias Ecto.Migration.Index
alias Ecto.Migration.Constraint
alias Ecto.Migration.Command
@doc """
Runs the given migration.
"""
def run(repo, config, version, module, direction, operation, migrator_direction, opts) do
level = Keyword.get(opts, :log, :info)
sql = Keyword.get(opts, :log_migrations_sql, false)
log = %{level: level, sql: sql}
args = {self(), repo, config, module, direction, migrator_direction, log}
{:ok, runner} = DynamicSupervisor.start_child(Ecto.MigratorSupervisor, {__MODULE__, args})
metadata(runner, opts)
direction_msg = if operation == :change, do: " #{direction}", else: nil
log(level, "== Running #{version} #{inspect(module)}.#{operation}/0#{direction_msg}")
{time, _} = :timer.tc(fn -> perform_operation(repo, module, operation) end)
log(level, "== Migrated #{version} in #{inspect(div(time, 100_000) / 10)}s")
after
stop()
end
@doc """
Stores the runner metadata.
"""
def metadata(runner, opts) do
prefix = opts[:prefix]
Process.put(:ecto_migration, %{runner: runner, prefix: prefix && to_string(prefix)})
end
@doc """
Starts the runner for the specified repo.
"""
def start_link({parent, repo, config, module, direction, migrator_direction, log}) do
Agent.start_link(fn ->
Process.link(parent)
%{
direction: direction,
repo: repo,
migration: module,
migrator_direction: migrator_direction,
command: nil,
subcommands: [],
log: log,
commands: [],
config: config
}
end)
end
@doc """
Stops the runner.
"""
def stop() do
Agent.stop(runner())
end
@doc """
Accesses the given repository configuration.
"""
def repo_config(key, default) do
Agent.get(runner(), &Keyword.get(&1.config, key, default))
end
@doc """
Returns the migrator command (up or down).
* forward + up: up
* forward + down: down
* forward + change: up
* backward + change: down
"""
def migrator_direction do
Agent.get(runner(), & &1.migrator_direction)
end
@doc """
Gets the repo for this migration
"""
def repo do
Agent.get(runner(), & &1.repo)
end
@doc """
Gets the prefix for this migration
"""
def prefix do
case Process.get(:ecto_migration) do
%{prefix: prefix} -> prefix
_ -> raise "could not find migration runner process for #{inspect(self())}"
end
end
@doc """
Executes queue migration commands.
Reverses the order commands are executed when doing a rollback
on a change/0 function and resets commands queue.
"""
def flush do
%{commands: commands, direction: direction, repo: repo, log: log, migration: migration} =
Agent.get_and_update(runner(), fn state -> {state, %{state | commands: []}} end)
commands = if direction == :backward, do: commands, else: Enum.reverse(commands)
for command <- commands do
execute_in_direction(repo, migration, direction, log, command)
end
end
@doc """
Queues command tuples or strings for execution.
Ecto.MigrationError will be raised when the server
is in `:backward` direction and `command` is irreversible.
"""
def execute(command) do
reply =
Agent.get_and_update(runner(), fn
%{command: nil} = state ->
{:ok, %{state | subcommands: [], commands: [command | state.commands]}}
%{command: _} = state ->
{:error, %{state | command: nil}}
end)
case reply do
:ok ->
:ok
:error ->
raise Ecto.MigrationError, "cannot execute nested commands"
end
end
@doc """
Starts a command.
"""
def start_command(command) do
reply =
Agent.get_and_update(runner(), fn
%{command: nil} = state ->
{:ok, %{state | command: command}}
%{command: _} = state ->
{:error, %{state | command: command}}
end)
case reply do
:ok ->
:ok
:error ->
raise Ecto.MigrationError, "cannot execute nested commands"
end
end
@doc """
Queues and clears current command. Must call `start_command/1` first.
"""
def end_command do
Agent.update(runner(), fn state ->
{operation, object} = state.command
command = {operation, object, Enum.reverse(state.subcommands)}
%{state | command: nil, subcommands: [], commands: [command | state.commands]}
end)
end
@doc """
Adds a subcommand to the current command. Must call `start_command/1` first.
"""
def subcommand(subcommand) do
reply =
Agent.get_and_update(runner(), fn
%{command: nil} = state ->
{:error, state}
state ->
{:ok, update_in(state.subcommands, &[subcommand | &1])}
end)
case reply do
:ok ->
:ok
:error ->
raise Ecto.MigrationError, message: "cannot execute command outside of block"
end
end
## Execute
defp execute_in_direction(repo, migration, :forward, log, %Command{up: up}) do
log_and_execute_ddl(repo, migration, log, up)
end
defp execute_in_direction(repo, migration, :forward, log, command) do
log_and_execute_ddl(repo, migration, log, command)
end
defp execute_in_direction(repo, migration, :backward, log, %Command{down: down}) do
log_and_execute_ddl(repo, migration, log, down)
end
defp execute_in_direction(repo, migration, :backward, log, command) do
if reversed = reverse(command) do
log_and_execute_ddl(repo, migration, log, reversed)
else
raise Ecto.MigrationError,
message:
"cannot reverse migration command: #{command(command)}. " <>
"You will need to explicitly define up/0 and down/0 in your migration"
end
end
defp reverse({:create, %Index{} = index}),
do: {:drop, index, :restrict}
defp reverse({:create_if_not_exists, %Index{} = index}),
do: {:drop_if_exists, index, :restrict}
defp reverse({:drop, %Index{} = index, _}),
do: {:create, index}
defp reverse({:drop_if_exists, %Index{} = index, _}),
do: {:create_if_not_exists, index}
defp reverse({:rename, %Index{} = index, new_name}),
do: {:rename, %{index | name: new_name}, index.name}
defp reverse({:create, %Table{} = table, _columns}),
do: {:drop, table, :restrict}
defp reverse({:create_if_not_exists, %Table{} = table, _columns}),
do: {:drop_if_exists, table, :restrict}
defp reverse({:rename, %Table{} = table_current, %Table{} = table_new}),
do: {:rename, table_new, table_current}
defp reverse({:rename, %Table{} = table, current_column, new_column}),
do: {:rename, table, new_column, current_column}
defp reverse({:alter, %Table{} = table, changes}) do
if reversed = table_reverse(changes, []) do
{:alter, table, reversed}
end
end
# It is not a good idea to reverse constraints because
# we can't guarantee data integrity when applying them back.
defp reverse({:create_if_not_exists, %Constraint{} = constraint}),
do: {:drop_if_exists, constraint, :restrict}
defp reverse({:create, %Constraint{} = constraint}),
do: {:drop, constraint, :restrict}
defp reverse(_command), do: false
defp table_reverse([{:remove, name, type, opts} | t], acc) do
table_reverse(t, [{:add, name, type, opts} | acc])
end
defp table_reverse([{:modify, name, type, opts} | t], acc) do
case opts[:from] do
nil ->
false
{reverse_type, from_opts} when is_list(from_opts) ->
reverse_from = {type, Keyword.delete(opts, :from)}
reverse_opts = Keyword.put(from_opts, :from, reverse_from)
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
reverse_type ->
reverse_opts = Keyword.put(opts, :from, type)
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
end
end
defp table_reverse([{:add, name, type, _opts} | t], acc) do
table_reverse(t, [{:remove, name, type, []} | acc])
end
defp table_reverse([_ | _], _acc) do
false
end
defp table_reverse([], acc) do
acc
end
## Helpers
defp perform_operation(repo, module, operation) do
if function_exported?(repo, :in_transaction?, 0) and repo.in_transaction?() do
if function_exported?(module, :after_begin, 0) do
module.after_begin()
flush()
end
apply(module, operation, [])
flush()
if function_exported?(module, :before_commit, 0) do
module.before_commit()
flush()
end
else
apply(module, operation, [])
flush()
end
end
defp runner do
case Process.get(:ecto_migration) do
%{runner: runner} -> runner
_ -> raise "could not find migration runner process for #{inspect(self())}"
end
end
defp log_and_execute_ddl(repo, migration, log, {instruction, %Index{} = index}) do
maybe_warn_index_ddl_transaction(index, migration)
maybe_warn_index_migration_lock(index, repo, migration)
log_and_execute_ddl(repo, log, {instruction, index})
end
defp log_and_execute_ddl(repo, _migration, log, command) do
log_and_execute_ddl(repo, log, command)
end
defp log_and_execute_ddl(_repo, _log, func) when is_function(func, 0) do
func.()
:ok
end
defp log_and_execute_ddl(repo, %{level: level, sql: sql}, command) do
log(level, command(command))
meta = Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
{:ok, logs} = repo.__adapter__().execute_ddl(meta, command, timeout: :infinity, log: sql)
Enum.each(logs, fn {ddl_log_level, message, metadata} ->
ddl_log(ddl_log_level, level, message, metadata)
end)
:ok
end
defp ddl_log(_level, false, _msg, _metadata), do: :ok
defp ddl_log(level, _, msg, metadata), do: log(level, msg, metadata)
defp log(level, msg, metadata \\ [])
defp log(false, _msg, _metadata), do: :ok
defp log(true, msg, metadata), do: Logger.log(:info, msg, metadata)
defp log(level, msg, metadata), do: Logger.log(level, msg, metadata)
defp maybe_warn_index_ddl_transaction(%{concurrently: true} = index, migration) do
migration_config = migration.__migration__()
if not migration_config[:disable_ddl_transaction] do
IO.warn(
"""
Migration #{inspect(migration)} has set index `#{index.name}` on table \
`#{index.table}` to concurrently but did not disable ddl transaction. \
Please set:
use Ecto.Migration
@disable_ddl_transaction true
""",
[]
)
end
end
defp maybe_warn_index_ddl_transaction(_index, _migration), do: :ok
defp maybe_warn_index_migration_lock(%{concurrently: true} = index, repo, migration) do
migration_lock_disabled = migration.__migration__()[:disable_migration_lock]
lock_strategy = repo.config()[:migration_lock]
adapter = repo.__adapter__()
case {migration_lock_disabled, adapter, lock_strategy} do
{false, Ecto.Adapters.Postgres, :pg_advisory_lock} ->
:ok
{false, Ecto.Adapters.Postgres, _} ->
IO.warn(
"""
Migration #{inspect(migration)} has set index `#{index.name}` on table \
`#{index.table}` to concurrently but did not disable migration lock. \
Please set:
use Ecto.Migration
@disable_migration_lock true
Alternatively, consider using advisory locks during migrations in the \
repo configuration:
config #{inspect(repo)}, migration_lock: :pg_advisory_lock
""",
[]
)
{false, _adapter, _migration_lock} ->
IO.warn(
"""
Migration #{inspect(migration)} has set index `#{index.name}` on table \
`#{index.table}` to concurrently but did not disable migration lock. \
Please set:
use Ecto.Migration
@disable_migration_lock true
""",
[]
)
_ ->
:ok
end
end
defp maybe_warn_index_migration_lock(_index, _repo, _migration), do: :ok
defp command(ddl) when is_binary(ddl) or is_list(ddl),
do: "execute #{inspect(ddl)}"
defp command({:create, %Table{} = table, _}),
do: "create #{table_modifiers(table.modifiers)} #{quote_name(table.prefix, table.name)}"
defp command({:create_if_not_exists, %Table{} = table, _}),
do:
"create #{table_modifiers(table.modifiers)} if not exists #{quote_name(table.prefix, table.name)}"
defp command({:alter, %Table{} = table, _}),
do: "alter table #{quote_name(table.prefix, table.name)}"
defp command({:drop, %Table{} = table, mode}),
do: "drop table #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
defp command({:drop_if_exists, %Table{} = table, mode}),
do: "drop table if exists #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
defp command({:create, %Index{} = index}),
do: "create index #{quote_name(index.prefix, index.name)}"
defp command({:create_if_not_exists, %Index{} = index}),
do: "create index if not exists #{quote_name(index.prefix, index.name)}"
defp command({:drop, %Index{} = index, mode}),
do: "drop index #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
defp command({:drop_if_exists, %Index{} = index, mode}),
do: "drop index if exists #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
defp command({:rename, %Index{} = index_current, new_name}),
do: "rename index #{quote_name(index_current.prefix, index_current.name)} to #{new_name}"
defp command({:rename, %Table{} = current_table, %Table{} = new_table}),
do:
"rename table #{quote_name(current_table.prefix, current_table.name)} to #{quote_name(new_table.prefix, new_table.name)}"
defp command({:rename, %Table{} = table, current_column, new_column}),
do:
"rename column #{current_column} to #{new_column} on table #{quote_name(table.prefix, table.name)}"
defp command({:create, %Constraint{check: nil, exclude: nil}}),
do: raise(ArgumentError, "a constraint must have either a check or exclude option")
defp command({:create, %Constraint{check: check, exclude: exclude}})
when is_binary(check) and is_binary(exclude),
do: raise(ArgumentError, "a constraint must not have both check and exclude options")
defp command({:create, %Constraint{check: check} = constraint}) when is_binary(check),
do:
"create check constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
defp command({:create, %Constraint{exclude: exclude} = constraint}) when is_binary(exclude),
do:
"create exclude constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
defp command({:drop, %Constraint{} = constraint, _}),
do:
"drop constraint #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
defp command({:drop_if_exists, %Constraint{} = constraint, _}),
do:
"drop constraint if exists #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
defp drop_mode(:restrict), do: ""
defp drop_mode(:cascade), do: " cascade"
defp quote_name(nil, name), do: quote_name(name)
defp quote_name(prefix, name), do: quote_name(prefix) <> "." <> quote_name(name)
defp quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name))
defp quote_name(name), do: name
defp table_modifiers(value) when is_binary(value), do: "#{value} table"
defp table_modifiers(_), do: "table"
end