120 lines
3.2 KiB
Erlang
120 lines
3.2 KiB
Erlang
-module(migration_engine).
|
|
-behaviour(gen_server).
|
|
|
|
-include("records.hrl").
|
|
|
|
%% API
|
|
-export([start_link/0, init_migrations_table/0, apply_pending/0,
|
|
rollback/1, status/0]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-define(TABLE, schema_migrations).
|
|
|
|
%% ------------------------------
|
|
%% API
|
|
%% ------------------------------
|
|
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
init_migrations_table() ->
|
|
gen_server:call(?MODULE, init_table).
|
|
|
|
apply_pending() ->
|
|
gen_server:call(?MODULE, apply_pending).
|
|
|
|
rollback(Version) ->
|
|
gen_server:call(?MODULE, {rollback, Version}).
|
|
|
|
status() ->
|
|
gen_server:call(?MODULE, status).
|
|
|
|
%% ------------------------------
|
|
%% gen_server callbacks
|
|
%% ------------------------------
|
|
|
|
init([]) ->
|
|
{ok, #{}}.
|
|
|
|
handle_call(init_table, _From, State) ->
|
|
case lists:member(?TABLE, mnesia:system_info(tables)) of
|
|
true -> ok;
|
|
false ->
|
|
mnesia:create_table(?TABLE, [
|
|
{disc_copies, [node()]},
|
|
{attributes, record_info(fields, schema_migration)},
|
|
{type, set}
|
|
])
|
|
end,
|
|
{reply, ok, State};
|
|
|
|
handle_call(apply_pending, _From, State) ->
|
|
Result = do_apply_pending(),
|
|
{reply, Result, State};
|
|
|
|
handle_call({rollback, Version}, _From, State) ->
|
|
Result = do_rollback(Version),
|
|
{reply, Result, State};
|
|
|
|
handle_call(status, _From, State) ->
|
|
Applied = applied_versions(),
|
|
Pending = pending_versions() -- Applied,
|
|
{reply, #{applied => lists:map(fun atom_to_list/1, Applied),
|
|
pending => lists:map(fun atom_to_list/1, Pending)}, State}.
|
|
|
|
handle_cast(_Msg, State) -> {noreply, State}.
|
|
handle_info(_Msg, State) -> {noreply, State}.
|
|
terminate(_Reason, _State) -> ok.
|
|
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
|
|
|
%% ------------------------------
|
|
%% Внутренняя логика
|
|
%% ------------------------------
|
|
|
|
do_apply_pending() ->
|
|
Pending = pending_versions() -- applied_versions(),
|
|
lists:foreach(fun(Module) -> code:ensure_loaded(Module) end, Pending),
|
|
lists:foldl(fun(Version, Acc) ->
|
|
try Version:up() of
|
|
_ -> mark_applied(Version), Acc
|
|
catch _:Reason ->
|
|
[{error, atom_to_list(Version), Reason} | Acc]
|
|
end
|
|
end, [], Pending).
|
|
|
|
do_rollback(TargetStr) ->
|
|
Target = list_to_atom(TargetStr),
|
|
Applied = applied_versions(),
|
|
ToRollback = lists:sort(fun(A,B) -> A > B end,
|
|
[V || V <- Applied, V > Target]),
|
|
lists:foreach(fun(Version) ->
|
|
code:ensure_loaded(Version),
|
|
try Version:down() of
|
|
_ -> unmark_applied(Version)
|
|
catch _:Reason ->
|
|
throw({rollback_failed, atom_to_list(Version), Reason})
|
|
end
|
|
end, ToRollback).
|
|
|
|
applied_versions() ->
|
|
[list_to_atom(V) || #schema_migration{version = V} <-
|
|
mnesia:dirty_match_object(#schema_migration{_ = '_'})].
|
|
|
|
pending_versions() ->
|
|
AllMods = code:all_available(),
|
|
[list_to_atom(Module) || Module <- extract_module_names(AllMods), lists:prefix("20", Module)].
|
|
|
|
extract_module_names(ModInfoList) ->
|
|
[Name || {Name, _, _} <- ModInfoList].
|
|
|
|
mark_applied(Version) ->
|
|
mnesia:dirty_write(#schema_migration{
|
|
version = atom_to_list(Version),
|
|
applied_at = calendar:local_time()
|
|
}).
|
|
|
|
unmark_applied(Version) ->
|
|
mnesia:dirty_delete({?TABLE, atom_to_list(Version)}). |