Current section

Files

Jump to
ecto lib ecto migrator.ex
Raw

lib/ecto/migrator.ex

defmodule Ecto.Migrator do
@moduledoc """
This module provides the migration API.
## Example
defmodule MyApp.MigrationExample do
use Ecto.Migration
def up do
execute "CREATE TABLE users(id serial PRIMARY_KEY, username text)"
end
def down do
execute "DROP TABLE users"
end
end
Ecto.Migrator.up(Repo, 20080906120000, MyApp.MigrationExample)
"""
require Logger
alias Ecto.Migration.Runner
alias Ecto.Migration.SchemaMigration
@doc """
Gets all migrated versions.
This function ensures the migration table exists
if no table has been defined yet.
"""
@spec migrated_versions(Ecto.Repo.t) :: [integer]
def migrated_versions(repo) do
SchemaMigration.ensure_schema_migrations_table!(repo)
SchemaMigration.migrated_versions(repo)
end
@doc """
Runs an up migration on the given repository.
## Options
* `:log` - the level to use for logging. Defaults to `:info`.
Can be any of `Logger.level/0` values or `false`.
"""
@spec up(Ecto.Repo.t, integer, Module.t, Keyword.t) :: :ok | :already_up | no_return
def up(repo, version, module, opts \\ []) do
versions = migrated_versions(repo)
if version in versions do
:already_up
else
do_up(repo, version, module, opts)
:ok
end
end
defp do_up(repo, version, module, opts) do
run_maybe_in_transaction repo, module, fn ->
attempt(repo, module, :forward, :up, :up, opts)
|| attempt(repo, module, :forward, :change, :up, opts)
|| raise Ecto.MigrationError, message: "#{inspect module} does not implement a `up/0` or `change/0` function"
SchemaMigration.up(repo, version)
end
end
@doc """
Runs a down migration on the given repository.
## Options
* `:log` - the level to use for logging. Defaults to `:info`.
Can be any of `Logger.level/0` values or `false`.
"""
@spec down(Ecto.Repo.t, integer, Module.t) :: :ok | :already_down | no_return
def down(repo, version, module, opts \\ []) do
versions = migrated_versions(repo)
if version in versions do
do_down(repo, version, module, opts)
:ok
else
:already_down
end
end
defp do_down(repo, version, module, opts) do
run_maybe_in_transaction repo, module, fn ->
attempt(repo, module, :forward, :down, :down, opts)
|| attempt(repo, module, :backward, :change, :down, opts)
|| raise Ecto.MigrationError, message: "#{inspect module} does not implement a `down/0` or `change/0` function"
SchemaMigration.down(repo, version)
end
end
defp run_maybe_in_transaction(repo, module, fun) do
cond do
module.__migration__[:disable_ddl_transaction] ->
fun.()
repo.__adapter__.supports_ddl_transaction? ->
repo.transaction [log: false, timeout: :infinity], fun
true ->
fun.()
end
end
defp attempt(repo, module, direction, operation, reference, opts) do
if Code.ensure_loaded?(module) and
function_exported?(module, operation, 0) do
Runner.run(repo, module, direction, operation, reference, opts)
:ok
end
end
@doc """
Apply migrations in a directory to a repository with given strategy.
A strategy must be given as an option.
## Options
* `:all` - runs all available if `true`
* `:step` - runs the specific number of migrations
* `:to` - runs all until the supplied version is reached
* `:log` - the level to use for logging. Defaults to `:info`.
Can be any of `Logger.level/0` values or `false`.
"""
@spec run(Ecto.Repo.t, binary, atom, Keyword.t) :: [integer]
def run(repo, directory, direction, opts) do
versions = migrated_versions(repo)
cond do
opts[:all] ->
run_all(repo, versions, directory, direction, opts)
to = opts[:to] ->
run_to(repo, versions, directory, direction, to, opts)
step = opts[:step] ->
run_step(repo, versions, directory, direction, step, opts)
true ->
raise ArgumentError, message: "expected one of :all, :to, or :step strategies"
end
end
defp run_to(repo, versions, directory, direction, target, opts) do
within_target_version? = fn
{version, _, _}, target, :up ->
version <= target
{version, _, _}, target, :down ->
version >= target
end
pending_in_direction(versions, directory, direction)
|> Enum.take_while(&(within_target_version?.(&1, target, direction)))
|> migrate(direction, repo, opts)
end
defp run_step(repo, versions, directory, direction, count, opts) do
pending_in_direction(versions, directory, direction)
|> Enum.take(count)
|> migrate(direction, repo, opts)
end
defp run_all(repo, versions, directory, direction, opts) do
pending_in_direction(versions, directory, direction)
|> migrate(direction, repo, opts)
end
defp pending_in_direction(versions, directory, :up) do
migrations_for(directory)
|> Enum.filter(fn {version, _name, _file} -> not (version in versions) end)
end
defp pending_in_direction(versions, directory, :down) do
migrations_for(directory)
|> Enum.filter(fn {version, _name, _file} -> version in versions end)
|> Enum.reverse
end
defp migrations_for(directory) do
query = Path.join(directory, "*")
for entry <- Path.wildcard(query),
info = extract_migration_info(entry),
do: info
end
defp extract_migration_info(file) do
base = Path.basename(file)
ext = Path.extname(base)
case Integer.parse(Path.rootname(base)) do
{integer, "_" <> name} when ext == ".exs" ->
{integer, name, file}
_ ->
nil
end
end
defp migrate(migrations, direction, repo, opts) do
if Enum.empty? migrations do
level = Keyword.get(opts, :log, :info)
log(level, "Already #{direction}")
end
ensure_no_duplication(migrations)
Enum.map migrations, fn {version, _name, file} ->
{mod, _bin} =
Enum.find(Code.load_file(file), fn {mod, _bin} ->
function_exported?(mod, :__migration__, 0)
end) || raise_no_migration_in_file(file)
case direction do
:up -> do_up(repo, version, mod, opts)
:down -> do_down(repo, version, mod, opts)
end
version
end
end
defp ensure_no_duplication([{version, name, _} | t]) do
if List.keyfind(t, version, 0) do
raise Ecto.MigrationError,
message: "migrations can't be executed, migration version #{version} is duplicated"
end
if List.keyfind(t, name, 1) do
raise Ecto.MigrationError,
message: "migrations can't be executed, migration name #{name} is duplicated"
end
ensure_no_duplication(t)
end
defp ensure_no_duplication([]), do: :ok
defp raise_no_migration_in_file(file) do
raise Ecto.MigrationError, message: "file #{Path.relative_to_cwd(file)} does not contain any Ecto.Migration"
end
defp log(false, _msg), do: :ok
defp log(level, msg), do: Logger.log(level, msg)
end