Обновление схемы данных без потерь #17
This commit is contained in:
120
src/infra/migration_engine.erl
Normal file
120
src/infra/migration_engine.erl
Normal file
@@ -0,0 +1,120 @@
|
||||
-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)}).
|
||||
Reference in New Issue
Block a user