Skip to content

Commit

Permalink
Merge 854b944 into 30d97e7
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher Keele committed Dec 14, 2013
2 parents 30d97e7 + 854b944 commit 38b5d9b
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 61 deletions.
83 changes: 77 additions & 6 deletions integration_test/pg/migrations_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule Ecto.Integration.MigrationsTest do
end
def down do
[]
[ "DELETE FROM migrations_test WHERE id IN ( SELECT id FROM migrations_test LIMIT 1 )" ]
end
end
"""
Expand Down Expand Up @@ -77,27 +77,98 @@ defmodule Ecto.Integration.MigrationsTest do
end
end

test "run all migrations" do
test "run up all migrations" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42, 43] = run_up(TestRepo, path)
assert [42, 43] = run(TestRepo, path)

create_migration(44, @good_migration)
assert [44] = run_up(TestRepo, path)
assert [44] = run(TestRepo, path)

assert [] = run_up(TestRepo, path)
assert [] = run(TestRepo, path)

assert Postgrex.Result[num_rows: 3] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")
end
end

test "run up to migration" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42] = run(TestRepo, path, to: 42)

assert Postgrex.Result[num_rows: 1] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")

assert [43] = run(TestRepo, path, to: 43)
end
end

test "run up 1 migration" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42] = run(TestRepo, path, step: 1)

assert Postgrex.Result[num_rows: 1] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")

assert [43] = run(TestRepo, path, to: 43)
end
end

test "run down 1 migration" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42, 43] = run(TestRepo, path)

assert [43] = run(TestRepo, path, direction: :down)

assert Postgrex.Result[num_rows: 1] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")

assert [43] = run(TestRepo, path, to: 43)
end
end

test "run down to migration" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42, 43] = run(TestRepo, path)

assert [43] = run(TestRepo, path, direction: :down, to: 43)

assert Postgrex.Result[num_rows: 1] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")

assert [43] = run(TestRepo, path, to: 43)
end
end

test "run down all migrations" do
in_tmp fn path ->
create_migration(42, @good_migration)
create_migration(43, @good_migration)
assert [42, 43] = run(TestRepo, path)

assert [43, 42] = run(TestRepo, path, direction: :down, all: true)

assert Postgrex.Result[num_rows: 0] =
Postgres.query(TestRepo, "SELECT * FROM migrations_test")

assert [42, 43] = run(TestRepo, path)
end
end

test "bad migration raises" do
in_tmp fn path ->
create_migration(42, @bad_migration)
assert_raise Postgrex.Error, fn ->
run_up(TestRepo, path)
run(TestRepo, path)
end
end
end
Expand Down
159 changes: 126 additions & 33 deletions lib/ecto/migrator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule Ecto.Migrator do
@doc """
Runs an up migration on the given repository.
"""
@spec up(Ecto.Repo.t, integer, Module.t) :: :ok | :already_up | no_return
def up(repo, version, module) do
commands = List.wrap(module.up)
repo.adapter.migrate_up(repo, version, commands)
Expand All @@ -31,69 +32,161 @@ defmodule Ecto.Migrator do
@doc """
Runs a down migration on the given repository.
"""
@spec down(Ecto.Repo.t, integer, Module.t) :: :ok | :missing_up | no_return
def down(repo, version, module) do
commands = List.wrap(module.down)
repo.adapter.migrate_down(repo, version, commands)
end

@doc false
def run_all(repo, directory) do
IO.write "Ecto.Migrator.run_all/2 is deprecated, please use Ecto.Migrator.run/3 instead."
run repo, directory
end

@doc """
Runs all migrations in the given directory.
Apply migrations in a directory to a repository.
Available options are:
- `direction:` `:up` or `:down`
defaults to :up
- `all:` runs all available if `true`
- `step:` runs a specific number of migrations
- `to:` runs all until supplied version is reached
If `:all`, `:step`, or `:to` are not provided, the direction
determines the default migration strategy:
- `up:` runs with `all: true`
- `down:` runs with `step: 1`
If more than one stragegy is provided, it will conservatively favor
the most explicit strategy: it will run `:to` a migration,
before it `:steps` through migrations, before it runs `:all`.
"""
@spec run_up(Ecto.Repo.t, binary) :: [integer] | no_return
def run_up(repo, directory) do
migrations = Path.join(directory, "*")
|> Path.wildcard
|> Enum.filter(&Regex.match?(%r"\d+_.+\.exs$", &1))
|> attach_versions
@spec run(Ecto.Repo.t, binary, Keyword.t) :: [integer]
def run(repo, directory, opts // []) do
{ direction, strategy } = parse_opts(opts)
do_run repo, directory, direction, strategy
end

ensure_no_duplication(migrations)
# To extend Migrator.run with different strategies,
# define a `do_run` clause that matches on it
# and insert the strategy type into the `strategies` function below.

migrations
|> filter_migrated(repo)
|> execute_migrations(repo)
defp do_run(repo, directory, direction, {:to, target_version}) do
within_target_version? = fn
{ version, _ }, target, :up ->
version <= target
{ version, _ }, target, :down ->
version >= target
end
pending_in_direction(repo, directory, direction)
|> Enum.take_while(&(within_target_version?.(&1, target_version, direction)))
|> migrate(direction, repo)
end

defp attach_versions(files) do
Enum.map(files, fn(file) ->
{ integer, _ } = Integer.parse(Path.basename(file))
{ integer, file }
end)
defp do_run(repo, directory, direction, {:step, count}) do
pending_in_direction(repo, directory, direction)
|> Enum.take(count)
|> migrate(direction, repo)
end

defp ensure_no_duplication([{ version, _ } | t]) do
if List.keyfind(t, version, 0) do
raise Ecto.MigrationError, message: "migrations can't be executed, version #{version} is duplicated"
else
ensure_no_duplication(t)
defp do_run(repo, directory, direction, {:all, true}) do
pending_in_direction(repo, directory, direction)
|> migrate(direction, repo)
end

# Keep in order of precedence.
defp strategies, do: [:to, :step, :all]

defp parse_opts(opts) do
{ direction, opts } = Keyword.pop(opts, :direction, :up)
{ direction, strategy_from(opts) || default_strategy_for(direction) }
end

defp strategy_from([]), do: nil
defp strategy_from([strategy]) do
if strategy |> elem(0) |> Kernel.in(strategies) do
strategy
end
end
defp strategy_from(opts) do
opts
|> Enum.filter(&(&1 |> elem(0) |> Kernel.in(strategies)))
|> Enum.sort(&(strategy_precedence(&1) > strategy_precedence(&2)))
|> Enum.first
end

defp ensure_no_duplication([]), do: :ok
defp strategy_precedence({ strategy, _ }) do
strategies
|> :lists.reverse
|> Enum.find_index(&(&1 == strategy))
end

defp default_strategy_for(:up), do: { :all, true }
defp default_strategy_for(:down), do: { :step, 1 }

defp filter_migrated(migrations, repo) do
defp pending_in_direction(repo, directory, :up) do
versions = repo.adapter.migrated_versions(repo)
Enum.filter(migrations, fn { version, _file } ->
not (version in versions)
migrations_for(directory) |>
Enum.filter(fn { version, _file } ->
not (version in versions)
end)
end

defp pending_in_direction(repo, directory, :down) do
versions = repo.adapter.migrated_versions(repo)
migrations_for(directory) |>
Enum.filter(fn { version, _file } ->
version in versions
end)
|> :lists.reverse
end

defp migrations_for(directory) do
Path.join(directory, "*")
|> Path.wildcard
|> Enum.filter(&Regex.match?(%r"\d+_.+\.exs$", &1))
|> attach_versions
end

defp attach_versions(files) do
Enum.map(files, fn(file) ->
{ integer, _ } = Integer.parse(Path.basename(file))
{ integer, file }
end)
end

defp execute_migrations(migrations, repo) do
defp migrate(migrations, direction, repo) do
ensure_no_duplication(migrations)
migrator = case direction do
:up -> &up/3
:down -> &down/3
end

Enum.map migrations, fn { version, file } ->
{ mod, _bin } =
Enum.find(Code.load_file(file), fn { mod, _bin } ->
function_exported?(mod, :__migration__, 0)
end) || raise_no_migration_in_file(file)

commands = List.wrap(mod.up)
case repo.adapter.migrate_up(repo, version, commands) do
:already_up ->
version
:ok ->
version
end
migrator.(repo, version, mod)
version
end
end

defp ensure_no_duplication([{ version, _ } | t]) do
if List.keyfind(t, version, 0) do
raise Ecto.MigrationError, message: "migrations can't be executed, version #{version} is duplicated"
else
ensure_no_duplication(t)
end
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
Expand Down
20 changes: 17 additions & 3 deletions lib/mix/tasks/ecto.migrate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,37 @@ defmodule Mix.Tasks.Ecto.Migrate do
use Mix.Task
import Mix.Tasks.Ecto

@shortdoc "Runs the given repo migrations"
@shortdoc "Runs migrations up on a repo"

@moduledoc """
Runs the pending migrations for the given repository.
Migrations are expected to be found inside the migrations
directory returned by the priv function defined in the
repository.
Runs all pending migrations by default. To migrate up
to a version number, supply `--to version_number`.
To migrate up a specific number of times, use `--step n`.
## Examples
mix ecto.migrate MyApp.Repo
mix ecto.migrate MyApp.Repo -n 3
mix ecto.migrate MyApp.Repo --step 3
mix ecto.migrate MyApp.Repo -v 20080906120000
mix ecto.migrate MyApp.Repo --to 20080906120000
"""
def run(args, migrator // &Ecto.Migrator.run_up/2) do
def run(args, migrator // &Ecto.Migrator.run/3) do
{ opts, args, _ } = OptionParser.parse args,
switches: [all: :boolean, step: :integer, version: :integer],
aliases: [n: :step, v: :to]
{ repo, _ } = parse_repo(args)
ensure_repo(repo)
ensure_started(repo)
migrator.(repo, migrations_path(repo))
migrator.(repo, migrations_path(repo), Keyword.merge(opts, direction: :up))
end
end
39 changes: 39 additions & 0 deletions lib/mix/tasks/ecto.rollback.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule Mix.Tasks.Ecto.Rollback do
use Mix.Task
import Mix.Tasks.Ecto

@shortdoc "Reverts migrations down on a repo"

@moduledoc """
Reverts applied migrations in the given repository.
Migrations are expected to be found inside the migrations
directory returned by the priv function defined in the
repository.
Runs the latest applied migration by default. To roll back to
to a version number, supply `--to version_number`.
To roll back a specific number of times, use `--step n`.
To undo all applied migrations, provide `--all`.
## Examples
mix ecto.rollback MyApp.Repo
mix ecto.rollback MyApp.Repo -n 3
mix ecto.rollback MyApp.Repo --step 3
mix ecto.rollback MyApp.Repo -v 20080906120000
mix ecto.rollback MyApp.Repo --to 20080906120000
"""
def run(args, migrator // &Ecto.Migrator.run/3) do
{ opts, args, _ } = OptionParser.parse args,
switches: [all: :boolean, step: :integer, version: :integer],
aliases: [n: :step, v: :to]
{ repo, _ } = parse_repo(args)
ensure_repo(repo)
ensure_started(repo)
migrator.(repo, migrations_path(repo), Keyword.merge(opts, direction: :down))
end
end
Loading

0 comments on commit 38b5d9b

Please sign in to comment.