-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)}).