/
migrator.ex
159 lines (131 loc) · 4.43 KB
/
migrator.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
defmodule Ecto.Migrator do
@moduledoc """
This module provides the migration API.
## Example
defmodule MyApp.MigrationExample do
use Ecto.Migration
def up do
"CREATE TABLE users(id serial PRIMARY_KEY, username text)"
end
def down do
"DROP TABLE users"
end
end
Ecto.Migrator.up(Repo, 20080906120000, MyApp.MigrationExample)
"""
@type strategy :: [all: true, to: non_neg_integer, step: non_neg_integer]
@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)
end
@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 """
Apply migrations in a directory to a repository with given strategy.
A strategy must be pass as an option. The available strategy types are:
* `:all` runs all available if `true`
* `:step` runs the specific number of migrations
* `:to` runs all until the supplied version is reached
"""
@spec run(Ecto.Repo.t, binary, atom, strategy) :: [integer]
def run(repo, directory, direction, opts) do
cond do
opts[:all] ->
run_all(repo, directory, direction)
to = opts[:to] ->
run_to(repo, directory, direction, to)
step = opts[:step] ->
run_step(repo, directory, direction, step)
true ->
raise ArgumentError, message: "expected one of :all, :to, or :step strategies"
end
end
defp run_to(repo, directory, direction, target) 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, direction)))
|> migrate(direction, repo)
end
defp run_step(repo, directory, direction, count) do
pending_in_direction(repo, directory, direction)
|> Enum.take(count)
|> migrate(direction, repo)
end
defp run_all(repo, directory, direction) do
pending_in_direction(repo, directory, direction)
|> migrate(direction, repo)
end
defp pending_in_direction(repo, directory, :up) do
versions = repo.adapter.migrated_versions(repo)
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 migrate(migrations, direction, repo) do
ensure_no_duplication(migrations)
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)
# TODO: Use logger in the future
file = Path.relative_to_cwd(file)
case direction do
:up ->
IO.puts IO.ANSI.format([:green, "* running ", :yellow, "UP ", :reset, file])
up(repo, version, mod)
:down ->
IO.puts IO.ANSI.format([:green, "* running ", :yellow, "DOWN ", :reset, file])
down(repo, version, mod)
end
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
end