Обновление схемы данных без потерь #17

This commit is contained in:
2026-05-04 23:15:59 +03:00
parent af0a36185b
commit 12a9f5a1a6
7 changed files with 176 additions and 2 deletions

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