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.
## Options
* `:log` - the level to use for logging. Defaults to `:info`.
Can be any of `Logger.level/0` values or `false`.
* `:prefix` - the prefix to run the migrations on
"""
@spec migrated_versions(Ecto.Repo.t, Keyword.t) :: [integer]
def migrated_versions(repo, opts \\ []) do
verbose_schema_migration repo, "retrieve migrated versions", fn ->
SchemaMigration.ensure_schema_migrations_table!(repo, opts[:prefix])
SchemaMigration.migrated_versions(repo, opts[:prefix])
end
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`.
* `:prefix` - the prefix to run the migrations on
"""
@spec up(Ecto.Repo.t, integer, module, Keyword.t) :: :ok | :already_up | no_return
def up(repo, version, module, opts \\ []) do
versions = migrated_versions(repo, opts)
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, "#{inspect module} does not implement a `up/0` or `change/0` function"
verbose_schema_migration repo, "update schema migrations", fn ->
SchemaMigration.up(repo, version, opts[:prefix])
end
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`.
* `:prefix` - the prefix to run the migrations on
"""
@spec down(Ecto.Repo.t, integer, module) :: :ok | :already_down | no_return
def down(repo, version, module, opts \\ []) do
versions = migrated_versions(repo, opts)
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, "#{inspect module} does not implement a `down/0` or `change/0` function"
verbose_schema_migration repo, "update schema migrations", fn ->
SchemaMigration.down(repo, version, opts[:prefix])
end
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(fun, [log: false, timeout: :infinity])
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 to a repository with a given strategy.
The second argument identifies where the migrations are sourced from. A file
path may be passed, in which case the migrations will be loaded from this
during the migration process. The other option is to pass a list of tuples
that identify the version number and migration modules to be run, for example:
Ecto.Migrator.run(Repo, [{0, MyApp.Migration1}, {1, MyApp.Migration2}, ...], :up, opts)
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`.
* `:prefix` - the prefix to run the migrations on
"""
@spec run(Ecto.Repo.t, binary | [{integer, module}], atom, Keyword.t) :: [integer]
def run(repo, migration_source, direction, opts) do
versions = migrated_versions(repo, opts)
cond do
opts[:all] ->
run_all(repo, versions, migration_source, direction, opts)
to = opts[:to] ->
run_to(repo, versions, migration_source, direction, to, opts)
step = opts[:step] ->
run_step(repo, versions, migration_source, direction, step, opts)
true ->
raise ArgumentError, "expected one of :all, :to, or :step strategies"
end
end
@doc """
Returns an array of tuples as the migration status of the given repo,
without actually running any migrations.
"""
def migrations(repo, directory) do
versions = migrated_versions(repo)
Enum.map(pending_in_direction(versions, directory, :down) |> Enum.reverse, fn {a, b, _}
-> {:up, a, b}
end)
++
Enum.map(pending_in_direction(versions, directory, :up), fn {a, b, _} ->
{:down, a, b}
end)
end
defp run_to(repo, versions, migration_source, direction, target, opts) do
within_target_version? = fn
{version, _, _}, target, :up ->
version <= target
{version, _, _}, target, :down ->
version >= target
end
pending_in_direction(versions, migration_source, direction)
|> Enum.take_while(&(within_target_version?.(&1, target, direction)))
|> migrate(direction, repo, opts)
end
defp run_step(repo, versions, migration_source, direction, count, opts) do
pending_in_direction(versions, migration_source, direction)
|> Enum.take(count)
|> migrate(direction, repo, opts)
end
defp run_all(repo, versions, migration_source, direction, opts) do
pending_in_direction(versions, migration_source, direction)
|> migrate(direction, repo, opts)
end
defp pending_in_direction(versions, migration_source, :up) do
migrations_for(migration_source)
|> Enum.filter(fn {version, _name, _file} -> not (version in versions) end)
end
defp pending_in_direction(versions, migration_source, :down) do
migrations_for(migration_source)
|> Enum.filter(fn {version, _name, _file} -> version in versions end)
|> Enum.reverse
end
# This function will match directories passed into `Migrator.run`.
defp migrations_for(migration_source) when is_binary(migration_source) do
query = Path.join(migration_source, "*")
for entry <- Path.wildcard(query),
info = extract_migration_info(entry),
do: info
end
# This function will match specific version/modules passed into `Migrator.run`.
defp migrations_for(migration_source) when is_list(migration_source) do
Enum.map migration_source, fn({version, module}) -> {version, module, :existing_module} end
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([], direction, _repo, opts) do
level = Keyword.get(opts, :log, :info)
log(level, "Already #{direction}")
[]
end
defp migrate(migrations, direction, repo, opts) do
ensure_no_duplication(migrations)
Enum.map migrations, fn {version, name_or_mod, file} ->
mod = extract_module(file, name_or_mod)
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,
"migrations can't be executed, migration version #{version} is duplicated"
end
if List.keyfind(t, name, 1) do
raise Ecto.MigrationError,
"migrations can't be executed, migration name #{name} is duplicated"
end
ensure_no_duplication(t)
end
defp ensure_no_duplication([]), do: :ok
defp is_migration_module?({mod, _bin}), do: function_exported?(mod, :__migration__, 0)
defp is_migration_module?(mod), do: function_exported?(mod, :__migration__, 0)
defp extract_module(:existing_module, mod) do
if is_migration_module?(mod), do: mod, else: raise_no_migration_in_module(mod)
end
defp extract_module(file, _name) do
modules = Code.load_file(file)
case Enum.find(modules, &is_migration_module?/1) do
{mod, _bin} -> mod
_otherwise -> raise_no_migration_in_file(file)
end
end
defp verbose_schema_migration(repo, reason, fun) do
try do
fun.()
rescue
error ->
Logger.error """
Could not #{reason}. This error usually happens due to the following:
* The database does not exist
* The "schema_migrations" table, which Ecto uses for managing
migrations, was defined by another library
To fix the first issue, run "mix ecto.create".
To address the second, you can run "mix ecto.drop" followed by
"mix ecto.create". Alternatively you may configure Ecto to use
another table for managing migrations:
config #{inspect repo.config[:otp_app]}, #{inspect repo},
migration_source: "some_other_table_for_schema_migrations"
The full error report is shown below.
"""
reraise error, System.stacktrace
end
end
defp raise_no_migration_in_file(file) do
raise Ecto.MigrationError,
"file #{Path.relative_to_cwd(file)} is not an Ecto.Migration"
end
defp raise_no_migration_in_module(mod) do
raise Ecto.MigrationError,
"module #{inspect mod} is not an Ecto.Migration"
end
defp log(false, _msg), do: :ok
defp log(level, msg), do: Logger.log(level, msg)
end