-module(exodus).
-export([run/1, list/1, revert/1, redo/1, config/2, config/3, timestamp/0, cleanup/1]).
-spec config(list() | map(), module()) -> map().
config(Migrations, Driver) ->
config(Migrations, Driver, null).
-spec config(list() | map(), module(), any()) -> map().
config(#{} = Migrations, Driver, Conn) ->
config(maps:to_list(Migrations), Driver, Conn);
config(Migrations, Driver, Conn) ->
#{migrations => Migrations,
driver => Driver,
conn => Conn}.
-spec run(map()) -> ok | {error, any()}.
run(#{driver := Driver,
conn := Conn,
migrations := Migrations}) ->
case Driver:up(Conn) of
{error, _Err} = E ->
E;
ok ->
case Driver:list(Conn) of
{error, _Err} = E ->
E;
{ok, AppliedMigrations} ->
NewMigrations = filter_applied_migrations(Migrations, AppliedMigrations),
run_migrations(Driver, Conn, sort_migrations(NewMigrations))
end
end;
run(_) ->
{error, invalid_config}.
-spec run_migrations(module(), any(), list()) -> ok | {error, any()}.
run_migrations(Driver, Conn, [{Migration, Timestamp} | T]) ->
case Driver:exec(Conn, Migration:up()) of
{error, _Err} = E ->
E;
ok ->
case Driver:add(Conn, atom_to_binary(Migration), Timestamp) of
{error, _Err} = E ->
E;
ok ->
run_migrations(Driver, Conn, T)
end
end;
run_migrations(_Driver, _Query, []) ->
ok.
-spec filter_applied_migrations(list(), list()) -> list().
filter_applied_migrations(Migrations, AppliedMigrations) ->
FormatAppliedMigrations =
lists:map(fun({_Id, Migration, Timestamp}) -> {Migration, Timestamp} end,
AppliedMigrations),
AppliedMigrationsMap = maps:from_list(FormatAppliedMigrations),
MigrationsMap = maps:from_list(Migrations),
maps:to_list(
maps:without(
maps:keys(AppliedMigrationsMap), MigrationsMap)).
-spec sort_migrations(list()) -> list().
sort_migrations(Migrations) ->
lists:sort(fun({_, A}, {_, B}) -> A =< B end, Migrations).
-spec list(map()) -> {ok, list()} | {error, any()}.
list(#{driver := Driver, conn := Conn}) ->
Driver:list(Conn).
-spec revert(map()) -> ok | {error, any()}.
revert(#{driver := Driver, conn := Conn}) ->
case Driver:last(Conn) of
{error, _Err} = E ->
E;
{ok, {Id, Migration, _Timestamp}} ->
case Driver:exec(Conn, Migration:down()) of
{error, _Err} = E ->
E;
ok ->
case Driver:delete(Conn, Id) of
{error, _Err} = E ->
E;
ok ->
ok
end
end
end.
-spec redo(map()) -> ok | {error, any()}.
redo(Config) ->
case revert(Config) of
{error, _Err} = E ->
E;
ok ->
run(Config)
end.
-spec cleanup(map()) -> ok | {error, any()}.
cleanup(#{driver := Driver,
conn := Conn}) ->
case Driver:list(Conn) of
{error, _Err} = E ->
E;
{ok, AppliedMigrations} ->
case cleanup_migrations(Driver, Conn, lists:reverse(AppliedMigrations)) of
{error, _Err} = E ->
E;
ok ->
Driver:down(Conn)
end
end.
-spec cleanup_migrations(module(), any(), list()) -> ok | {error, any()}.
cleanup_migrations(Driver, Conn, [{Id, _Name, _Timestamp} | T]) ->
case Driver:delete(Conn, Id) of
{error, _Err} = E ->
E;
ok ->
cleanup_migrations(Driver, Conn, T)
end;
cleanup_migrations(_Driver, _Conn, []) ->
ok.
-spec timestamp() -> integer().
timestamp() ->
{A, B, C} = erlang:timestamp(),
Bin = list_to_binary([integer_to_list(A), integer_to_list(B), integer_to_list(C)]),
binary_to_integer(Bin).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
filter_applied_migrations_test() ->
Migrations = [{create_todos, 12345}, {create_tags, 123456}],
Migrations2 = [{1, create_todos, 12345}],
?assertEqual([{create_tags, 123456}], filter_applied_migrations(Migrations, Migrations2)).
sort_migrations_test() ->
Migrations = [{create_todos, 12345}, {create_tags, 123456}, {create_stuff, 123}],
?assertEqual([{create_stuff, 123}, {create_todos, 12345}, {create_tags, 123456}],
sort_migrations(Migrations)).
timestamp_test() ->
T = timestamp(),
?assert(is_integer(T)).
-endif.