src/core/core_ticket.erl View File
| 13 | list_all() -> |
| 14 | mnesia:dirty_match_object(#ticket{_ = '_'}). |
| 15 | |
| 16 | get_by_id(Id) -> |
| 17 | case mnesia:dirty_read({ticket, Id}) of |
| 18 | [Ticket] -> {ok, Ticket}; |
| 25 | Updated = apply_updates(Ticket, Updates), |
| 26 | mnesia:dirty_write(Updated), |
| 27 | {ok, Updated}; |
| 33 | {ok, _Ticket} -> |
| 34 | mnesia:dirty_delete({ticket, Id}), |
| 35 | {ok, deleted}; |
| 70 | }, |
| 71 | mnesia:dirty_write(Ticket), |
| 72 | {ok, Ticket}. |
| 74 | list_by_user(UserId) -> |
| 75 | mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}). |
| 76 | |
| 96 | % Загружаем все тикеты (или можно только закрытые, если их мало – решите по нагрузке) |
| 97 | Tickets = mnesia:dirty_match_object(#ticket{_ = '_'}), |
| 98 | % Фильтруем закрытые с учётом нормализации статуса |
src/core/core_user.erl View File
| 31 | F = fun() -> |
| 32 | mnesia:write(User), |
| 33 | {ok, User} |
| 35 | |
| 36 | case mnesia:transaction(F) of |
| 37 | {atomic, Result} -> Result; |
| 43 | get_by_id(Id) -> |
| 44 | case mnesia:dirty_read(user, Id) of |
| 45 | [] -> {error, not_found}; |
| 51 | Match = #user{email = Email, _ = '_'}, |
| 52 | case mnesia:dirty_match_object(Match) of |
| 53 | [] -> {error, not_found}; |
| 66 | F = fun() -> |
| 67 | case mnesia:read(user, Id) of |
| 68 | [] -> |
| 71 | UpdatedUser = apply_updates(User, Updates), |
| 72 | mnesia:write(UpdatedUser), |
| 73 | {ok, UpdatedUser} |
| 76 | |
| 77 | case mnesia:transaction(F) of |
| 78 | {atomic, Result} -> Result; |
| 85 | Updated = User#user{last_login = calendar:universal_time()}, |
| 86 | mnesia:dirty_write(Updated), |
| 87 | {ok, Updated}; |
| 94 | Updated = User#user{status = Status, reason = Reason, updated_at = calendar:universal_time()}, |
| 95 | mnesia:dirty_write(Updated), |
| 96 | {ok, Updated}; |
| 104 | list_users() -> |
| 105 | Users = mnesia:dirty_match_object(#user{_ = '_'}), |
| 106 | ActiveUsers = [U || U <- Users, U#user.status =/= deleted], |
| 134 | Updated = User#user{status = blocked, reason = Reason, updated_at = calendar:universal_time()}, |
| 135 | mnesia:dirty_write(Updated), |
| 136 | {ok, Updated}; |
| 143 | Updated = User#user{status = active, reason = Reason, updated_at = calendar:universal_time()}, |
| 144 | mnesia:dirty_write(Updated), |
| 145 | {ok, Updated}; |
| 149 | count_users() -> |
| 150 | mnesia:table_info(user, size). |
| 151 | |
| 153 | list_all() -> |
| 154 | mnesia:dirty_match_object(#user{_ = '_'}). |
| 155 | |
| 156 | count_users_by_date(From, To) -> |
| 157 | All = mnesia:dirty_match_object(#user{_ = '_'}), |
| 158 | Filtered = lists:filter(fun(U) -> |
| 202 | false -> |
| 203 | case mnesia:dirty_index_read(user, Email, email) of |
| 204 | [] -> |
| 215 | }, |
| 216 | ok = mnesia:dirty_write(User), |
| 217 | {ok, User}; |
| 224 | delete_bot(Id) -> |
| 225 | case mnesia:dirty_read({user, Id}) of |
| 226 | [#user{role = bot}] -> |
| 227 | mnesia:dirty_delete({user, Id}), |
| 228 | ok; |
src/eventhub_app.erl View File
src/handlers/handler_calendar_view.erl View File
| 79 | is_owner(UserId, CalendarId) -> |
| 80 | case mnesia:dirty_read({calendar, CalendarId}) of |
| 81 | [#calendar{owner_id = UserId}] -> true; |
| 125 | |
| 126 | %% @private Извлекает "горячие" события из Mnesia. |
| 127 | -spec fetch_hot_events(binary(), integer(), integer()) -> list(#event{}). |
| 130 | End = {{Year, Month, calendar:last_day_of_the_month(Year, Month)}, {23, 59, 59}}, |
| 131 | mnesia:dirty_select(event, [ |
| 132 | {#event{calendar_id = CalendarId, start_time = '$1', _ = '_'}, |
src/infra/bot_controller.erl View File
src/infra/cluster_discovery.erl View File
| 38 | %% ------------------------------------------------------------------ |
| 39 | %% @doc Добавляет удалённый узел в Mnesia и реплицирует данные |
| 40 | %% ------------------------------------------------------------------ |
| 41 | join_and_replicate(Node) -> |
| 42 | case lists:member(Node, mnesia:system_info(db_nodes)) of |
| 43 | true -> |
| 44 | io:format("Node ~p already in Mnesia cluster, skipping~n", [Node]); |
| 45 | false -> |
| 46 | io:format("Adding node ~p to Mnesia cluster...~n", [Node]), |
| 47 | infra_mnesia:add_cluster_nodes([Node]) |
| 48 | end. |
src/infra/infra_mnesia.erl View File
| 1 | %% =================================================================== |
| 2 | %% EventHub – infra_mnesia (финальная версия с автоочисткой кластера) |
| 3 | %% =================================================================== |
| 4 | -module(infra_mnesia). |
| 5 | -behaviour(gen_server). |
| 72 | handle_call(wait_for_tables, _From, State) -> |
| 73 | mnesia:wait_for_tables(?TABLES, ?TABLE_WAIT_TIMEOUT), |
| 74 | {reply, ok, State}. |
| 90 | maybe_recreate_schema() -> |
| 91 | MnesiaDir = mnesia:system_info(directory), |
| 92 | case filelib:is_dir(MnesiaDir) of |
| 93 | false -> |
| 94 | io:format("Mnesia directory (~s) not found. Creating fresh schema...~n", [MnesiaDir]), |
| 95 | mnesia:stop(), |
| 96 | mnesia:delete_schema([node()]), |
| 97 | mnesia:create_schema([node()]), |
| 98 | mnesia:start(), |
| 99 | ok; |
| 100 | true -> |
| 101 | io:format("Mnesia directory exists (~s). Reusing existing schema.~n", [MnesiaDir]), |
| 102 | case mnesia:system_info(is_running) of |
| 103 | yes -> ok; |
| 104 | _ -> mnesia:start() |
| 105 | end |
| 108 | join_cluster(Nodes) -> |
| 109 | case mnesia:system_info(is_running) of |
| 110 | yes -> mnesia:stop(); |
| 111 | no -> ok |
| 112 | end, |
| 113 | application:set_env(mnesia, extra_db_nodes, Nodes), |
| 114 | mnesia:start(), |
| 115 | ensure_schema_disc(), |
| 121 | application:set_env(eventhub, extra_db_nodes, Nodes ++ ExtraNodes), |
| 122 | {ok, _} = mnesia:change_config(extra_db_nodes, Nodes), |
| 123 | ensure_schema_disc(), |
| 127 | ensure_schema_disc() -> |
| 128 | case lists:member(node(), mnesia:table_info(schema, disc_copies)) of |
| 129 | false -> |
| 130 | io:format("Changing schema copy to disc...~n"), |
| 131 | case mnesia:change_table_copy_type(schema, node(), disc_copies) of |
| 132 | {atomic, ok} -> ok; |
| 139 | add_local_disc_copy(Tab) -> |
| 140 | case lists:member(node(), mnesia:table_info(Tab, disc_copies)) of |
| 141 | false -> |
| 142 | io:format("Adding local disc copy of table ~p...~n", [Tab]), |
| 143 | case mnesia:add_table_copy(Tab, node(), disc_copies) of |
| 144 | {atomic, ok} -> ok; |
| 155 | wait_for_table(Tab) -> |
| 156 | case lists:member(Tab, mnesia:system_info(tables)) of |
| 157 | true -> ok; |
| 173 | Node =/= node() andalso net_adm:ping(Node) =:= pong |
| 174 | end, mnesia:system_info(db_nodes)), |
| 175 | DeadNodes = mnesia:system_info(db_nodes) -- [node() | AliveNodes], |
| 176 | lists:foreach(fun(Node) -> |
| 177 | io:format("Removing dead node ~p from Mnesia schema...~n", [Node]), |
| 178 | lists:foreach(fun(Tab) -> |
| 179 | case lists:member(Node, mnesia:table_info(Tab, disc_copies)) of |
| 180 | true -> catch mnesia:del_table_copy(Tab, Node); |
| 181 | false -> ok |
| 183 | end, ?DISC_TABLES), |
| 184 | catch mnesia:del_table_copy(schema, Node) |
| 185 | end, DeadNodes). |
| 192 | Opts = table_opts(Table), |
| 193 | case mnesia:create_table(Table, Opts) of |
| 194 | {atomic, ok} -> ok; |
| 228 | create_indices() -> |
| 229 | mnesia:add_table_index(event, calendar_id), |
| 230 | mnesia:add_table_index(event, title), |
| 231 | mnesia:add_table_index(event, created_at), |
| 232 | mnesia:add_table_index(event, start_time), |
| 233 | mnesia:add_table_index(event, event_type), |
| 234 | mnesia:add_table_index(event, master_id), |
| 235 | mnesia:add_table_index(event, specialist_id), |
| 236 | mnesia:add_table_index(event, status), |
| 237 | mnesia:add_table_index(booking, event_id), |
| 238 | mnesia:add_table_index(booking, user_id), |
| 239 | mnesia:add_table_index(booking, status), |
| 240 | mnesia:add_table_index(calendar, owner_id), |
| 241 | mnesia:add_table_index(calendar, status), |
| 242 | mnesia:add_table_index(calendar, short_name), |
| 243 | mnesia:add_table_index(calendar, category), |
| 244 | mnesia:add_table_index(calendar_specialist, calendar_id), |
| 245 | mnesia:add_table_index(calendar_specialist, user_id), |
| 246 | mnesia:add_table_index(user, nickname), |
| 247 | mnesia:add_table_index(user, email), |
| 248 | mnesia:add_table_index(notification, user_id), |
| 249 | mnesia:add_table_index(notification, is_read), |
| 250 | ok. |
src/infra/infra_mnesia_fragmentation.erl View File
| 1 | %% =================================================================== |
| 2 | %% EventHub – утилита фрагментации больших таблиц Mnesia |
| 3 | %% =================================================================== |
| 4 | -module(infra_mnesia_fragmentation). |
| 5 | |
| 22 | %% Пример: |
| 23 | %% infra_mnesia_fragmentation:fragment_table(event, 4). |
| 24 | %% ------------------------------------------------------------------- |
| 25 | fragment_table(Table, FragCount) when FragCount > 1 -> |
| 26 | case mnesia:change_table_frag(Table, {activate, FragCount}) of |
| 27 | {atomic, ok} -> |
| 43 | defragment_table(Table) -> |
| 44 | case mnesia:change_table_frag(Table, deactivate) of |
| 45 | {atomic, ok} -> |
| 56 | add_fragment(Table, ExtraFrags) -> |
| 57 | case mnesia:add_table_fragment(Table, ExtraFrags) of |
| 58 | {atomic, ok} -> |
| 70 | try |
| 71 | IsFrag = mnesia:table_info(Table, frag_property), |
| 72 | FragCount = case IsFrag of |
| 76 | io:format("Table ~p fragmentation: ~p fragments~n", [Table, FragCount]), |
| 77 | FragList = mnesia:table_info(Table, frag_dist), |
| 78 | io:format("Fragment distribution: ~p~n", [FragList]), |
src/infra/infra_sup.erl View File
src/infra/migration_engine.erl View File
| 42 | handle_call(init_table, _From, State) -> |
| 43 | case lists:member(?TABLE, mnesia:system_info(tables)) of |
| 44 | true -> ok; |
| 45 | false -> |
| 46 | mnesia:create_table(?TABLE, [ |
| 47 | {disc_copies, [node()]}, |
| 51 | end, |
| 52 | infra_mnesia:wait_for_table(?TABLE), |
| 53 | {reply, ok, State}; |
| 104 | [list_to_atom(V) || #schema_migration{version = V} <- |
| 105 | mnesia:dirty_match_object(#schema_migration{_ = '_'})]. |
| 106 | |
| 114 | mark_applied(Version) -> |
| 115 | mnesia:dirty_write(#schema_migration{ |
| 116 | version = atom_to_list(Version), |
| 120 | unmark_applied(Version) -> |
| 121 | mnesia:dirty_delete({?TABLE, atom_to_list(Version)}). |
src/infra/stats_collector.erl View File
| 20 | handle_call(subscribe, _From, State) -> |
| 21 | mnesia:subscribe({table, event, simple}), |
| 22 | mnesia:subscribe({table, booking, simple}), |
| 23 | mnesia:subscribe({table, review, simple}), |
| 24 | {reply, ok, State}; |
| 35 | {noreply, State}; |
| 36 | handle_info({mnesia_table_event, {write, Record, _ActivityId}}, State) -> |
| 37 | Table = element(1, Record), |
| 69 | StOld = if Old =:= [] -> []; true -> element(#booking.status, Old) end, |
| 70 | CId = case mnesia:dirty_read({event, EvId}) of |
| 71 | [#event{calendar_id = C}] -> C; |
| 93 | EvId = element(#review.target_id, New), |
| 94 | case mnesia:dirty_read({event, EvId}) of |
| 95 | [#event{calendar_id = C}] -> C; |
| 124 | lists:foreach(fun({{Metric, EntityId}, Value}) -> |
| 125 | mnesia:dirty_write(#stats{ |
| 126 | id = list_to_binary(io_lib:format("~p_~p_~p", [Metric, EntityId, os:system_time(millisecond)])), |
src/logic/logic_event.erl View File
| 95 | }, |
| 96 | mnesia:dirty_write(Exception), |
| 97 | {ok, cancelled}; |
| 194 | % Для конкретного календаря загружаем все события (любой статус) |
| 195 | mnesia:dirty_index_match_object( |
| 196 | event, |
| 330 | Match = #recurrence_exception{master_id = MasterId, _ = '_'}, |
| 331 | mnesia:dirty_match_object(Match). |
| 332 | |
| 338 | merge_materialized(MasterId, Occurrences) -> |
| 339 | Materialized = mnesia:dirty_match_object( |
| 340 | #event{master_id = MasterId, is_instance = true, status = active, _ = '_'} |
src/logic/logic_review.erl View File
src/logic/logic_search.erl View File
src/migrations/README.md View File
test/unit/booking_integration_tests.erl View File
| 5 | setup() -> |
| 6 | mnesia:start(), |
| 7 | mnesia:create_table(user, [ |
| 8 | {attributes, record_info(fields, user)}, |
| 10 | ]), |
| 11 | mnesia:create_table(calendar, [ |
| 12 | {attributes, record_info(fields, calendar)}, |
| 14 | ]), |
| 15 | mnesia:create_table(event, [ |
| 16 | {attributes, record_info(fields, event)}, |
| 18 | ]), |
| 19 | mnesia:create_table(booking, [ |
| 20 | {attributes, record_info(fields, booking)}, |
| 25 | cleanup(_) -> |
| 26 | mnesia:delete_table(booking), |
| 27 | mnesia:delete_table(event), |
| 28 | mnesia:delete_table(calendar), |
| 29 | mnesia:delete_table(user), |
| 30 | mnesia:stop(), |
| 31 | ok. |
| 54 | }, |
| 55 | mnesia:dirty_write(User), |
| 56 | UserId. |
test/unit/core_banned_words_tests.erl View File
| 8 | setup() -> |
| 9 | % Гарантированно останавливаем Mnesia (если уже запущена) |
| 10 | catch mnesia:stop(), |
| 11 | % Запускаем Mnesia (первый раз вернёт {atomic, ok}, потом ok) |
| 12 | case mnesia:start() of |
| 13 | {atomic, ok} -> ok; |
| 16 | % Создаём таблицу (всегда возвращает {atomic, ok}) |
| 17 | {atomic, ok} = mnesia:create_table(banned_word, [ |
| 18 | {attributes, record_info(fields, banned_word)}, |
| 23 | cleanup(_) -> |
| 24 | mnesia:delete_table(banned_word), |
| 25 | mnesia:stop(). |