defmodule Xmeta.South.Migrations do
@data_path "./priv/repo/migrations.exs"
@migrate_path "./priv/repo/migrations"
def get_data_path() do
conf = Application.get_env(:ecto_south, :path)
if conf, do: conf[:data_path] || @data_path, else: @data_path
end
def get_migrate_path() do
conf = Application.get_env(:ecto_south, :path)
if conf, do: conf[:migrate_path] || @migrate_path, else: @migrate_path
end
def mods(), do: find_xmd_modules()
def old_data() do
try do
Code.eval_file(get_data_path())
Xmeta.South.Migrations.Data.show()
catch
_, _ -> false
end
end
def can_check?() do
migration_count = length(File.ls!(@migrate_path))
migration_count = if migration_count > 0, do: migration_count-1, else: 0
repo_status = Xmeta.South.Ecto.migrations_status([]) |> List.first
if migration_count > repo_status.count_up do
false
else
true
end
end
def run() do
if can_check?() do
old = old_data()
new = meta_mod_all(mods())
if length(new) == 0 do
Mix.shell.info "Not search any ecto schema \n"
end
unless old do
diff(new, %{})
init_migrations_file()
else
if old == new do
Mix.shell.info "No changes detected \n"
else
Mix.shell.info "Has changes, checking....\n"
diff(new, old)
# init_migrations_file()
end
end
else
file_name = find_last_migrations_file()
Mix.shell.info("need to migrate your new migration file: #{file_name}")
end
end
def meta_mod(mod) do
mod_types = Enum.reduce(
mod.__schema__(:fields),
%{},
fn (f, acc) ->
Map.put(acc, f, mod.__schema__(:type, f))
end
)
types = Enum.reduce(
mod.__schema__(:associations),
mod_types,
fn (ref, acc) ->
ass = mod.__schema__(:association, ref)
if ass do
type = if ass.cardinality != :many do
String.to_atom "ref__#{ass.related.__schema__(:source)}"
else
String.to_atom "m2m__#{ass.related.__schema__(:source)}__#{ass.join_through}"
end
Map.put acc, ass.field, type
else
acc
end
end
)
%{
types: types,
primary_key: mod.__schema__(:primary_key)
}
end
def meta_mod_all(mods) do
Enum.reduce(
mods,
[],
fn (m, acc) ->
acc ++ [{String.to_atom(m.__schema__(:source)), meta_mod(m)}]
end
)
end
def diff(new, old) do
changes = for {k, v} <- new do
unless old[k] == v do
unless old[k], do: create_table(k, v), else: check_field(k, v, old[k])
end
end
changes_drop = for {k, _} <- old do
unless new[k], do: drop_table(k)
end
change_data = Enum.filter(changes ++ changes_drop, fn x -> x != nil end)
show_change(change_data)
content = Xmeta.South.Migrate.Template.get(change_data)
file_name = "/#{string_time_now()}_south.exs"
File.write(get_migrate_path() <> file_name, content)
end
def string_time_now() do
d = DateTime.utc_now
month = if d.month < 10, do: "0#{d.month}", else: d.month
day = if d.day < 10, do: "0#{d.day}", else: d.day
hour = if d.hour < 10, do: "0#{d.hour}", else: d.hour
minute = if d.minute < 10, do: "0#{d.minute}", else: d.minute
second = if d.second < 10, do: "0#{d.second}", else: d.second
"#{d.year}#{month}#{day}#{hour}#{minute}#{second}"
end
# change = %{action: atom, table: atom, schema: [%{action: atom, filed: atom, type: atom }]}
def create_table(table, data) do
change_field = for {k, v} <- Enum.to_list(data[:types]) do
%{action: :add, filed: k, type: v}
end
%{table: table, action: :create, schema: change_field}
end
def drop_table(table), do: %{table: table, action: :drop}
def check_field(table, new_data, old_data) do
new = new_data[:types]
old = old_data[:types]
change_add = Enum.reduce(
Enum.to_list(new),
[],
fn ({f, t}, acc) ->
unless old[f], do: acc ++ [%{action: :add, filed: f, type: t}], else: acc
end
)
change_delete = Enum.reduce(
Enum.to_list(old),
[],
fn ({f, _}, acc) ->
unless new[f], do: acc ++ [%{action: :remove, filed: f}], else: acc
end
)
%{table: table, action: :alter, schema: change_add ++ change_delete}
end
def show_change(data) do
for i <- data do
Mix.shell.info "table: #{i.table}, action: #{i.action}"
if i[:schema] do
for s <- i.schema do
if s[:type] do
Mix.shell.info " #{s.action}: #{s.filed}, :#{s.type}"
else
Mix.shell.info " #{s.action}: #{s.filed}"
end
end
end
IO.puts ""
end
end
def init_migrations_file() do
content = mods()
|> meta_mod_all()
|> Xmeta.South.Migrations.Template.get
File.write(get_data_path(), content)
end
def find_last_migrations_file() do
migration_num = Enum.reduce(File.ls!(@migrate_path), 0, fn(file_name,num) ->
if String.contains?(file_name, "_south") do
new_num = file_name |> String.split("_") |> List.first |> String.to_integer
if new_num > num, do: new_num, else: num
else
num
end
end)
"#{migration_num}_south.exs"
end
def find_xmd_modules() do
{_, keys} = Mix.Project.config()[:app] |> :application.get_all_key()
modules = keys[:modules]
Enum.reduce modules, [], fn(mod,acc) ->
module_name = to_string(mod)
if String.contains?(module_name, "Xmd") do
acc ++ [mod]
else
acc
end
end
end
end