From 1c701a52544524f6dd37d81b0607929f2df1b886 Mon Sep 17 00:00:00 2001 From: Aleksey Sabilin Date: Sun, 16 Aug 2026 10:04:16 +0300 Subject: [PATCH] fix(share): register calendar_share migration and rebuild old PK schema. Refs EventHub/EventHubBack#73 --- src/handlers/handler_share_invites.erl | 8 ++- src/infra/migration_engine.erl | 5 +- .../20260815220000_calendar_share_invite.erl | 72 +++++++++++-------- test/unit/calendar_share_migration_tests.erl | 50 +++++++++++++ test/unit/migration_engine_tests.erl | 5 +- 5 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 test/unit/calendar_share_migration_tests.erl diff --git a/src/handlers/handler_share_invites.erl b/src/handlers/handler_share_invites.erl index c49c8d6..33de472 100644 --- a/src/handlers/handler_share_invites.erl +++ b/src/handlers/handler_share_invites.erl @@ -148,5 +148,9 @@ map_decide_error(Req, not_pending) -> handler_utils:send_error(Req, 409, <<"Invite is not pending">>); map_decide_error(Req, expired) -> handler_utils:send_error(Req, 410, <<"Invite expired">>); -map_decide_error(Req, _) -> - handler_utils:send_error(Req, 500, <<"Internal server error">>). +map_decide_error(Req, {no_exists, Table}) -> + handler_utils:report_and_send_error(Req, 503, + <<"Schema not ready">>, #{reason => no_exists, table => Table}); +map_decide_error(Req, Reason) -> + handler_utils:report_and_send_error(Req, 500, + <<"Internal server error">>, #{reason => Reason}). diff --git a/src/infra/migration_engine.erl b/src/infra/migration_engine.erl index f442ddb..2a098a7 100755 --- a/src/infra/migration_engine.erl +++ b/src/infra/migration_engine.erl @@ -28,7 +28,10 @@ '20260720210000_calendar_follow', '20260722150000_calendar_specialist_id', '20260722190000_specialist_invite', - '20260730120000_recurrence_exception_bag' + '20260730120000_recurrence_exception_bag', + '20260814193000_waitlist_entry', + '20260815200000_push_subscription', + '20260815220000_calendar_share_invite' ]). %% ------------------------------ diff --git a/src/migrations/20260815220000_calendar_share_invite.erl b/src/migrations/20260815220000_calendar_share_invite.erl index 7f0a00d..4db3567 100644 --- a/src/migrations/20260815220000_calendar_share_invite.erl +++ b/src/migrations/20260815220000_calendar_share_invite.erl @@ -1,4 +1,6 @@ %% @doc calendar_share: id PK + mirror_to_default; create calendar_share_invite. +%% Old schema PK was calendar_id — mnesia:transform_table cannot change the key, +%% so we rebuild the table (feature was unused until Back#73). -module('20260815220000_calendar_share_invite'). -export([up/0, down/0]). @@ -6,7 +8,7 @@ -include("records.hrl"). up() -> - transform_calendar_share(), + rebuild_calendar_share_if_needed(), ensure_index(calendar_share, calendar_id), ensure_index(calendar_share, user_id), ensure_table(calendar_share_invite, record_info(fields, calendar_share_invite)), @@ -21,7 +23,7 @@ down() -> _ = mnesia:delete_table(calendar_share_invite), ok. -transform_calendar_share() -> +rebuild_calendar_share_if_needed() -> case lists:member(calendar_share, mnesia:system_info(tables)) of false -> ensure_table(calendar_share, record_info(fields, calendar_share)); @@ -31,36 +33,50 @@ transform_calendar_share() -> true -> ok; false -> - Fun = fun(Rec) -> - case tuple_to_list(Rec) of - [calendar_share, CalId, UserId, Rights] -> - #calendar_share{ - id = infra_utils:generate_id(16), - calendar_id = CalId, - user_id = UserId, - rights = Rights, - mirror_to_default = false - }; - [calendar_share, Id, CalId, UserId, Rights] -> - #calendar_share{ - id = Id, - calendar_id = CalId, - user_id = UserId, - rights = Rights, - mirror_to_default = false - }; - _ -> - Rec - end - end, - case mnesia:transform_table(calendar_share, Fun, - record_info(fields, calendar_share)) of + Rows = dump_calendar_share_rows(), + case mnesia:delete_table(calendar_share) of {atomic, ok} -> ok; - {aborted, Reason} -> error({transform_calendar_share_failed, Reason}) - end + {aborted, Reason} -> error({delete_calendar_share_failed, Reason}) + end, + ensure_table(calendar_share, record_info(fields, calendar_share)), + case mnesia:wait_for_tables([calendar_share], 30000) of + ok -> ok; + {timeout, _} -> error({wait_calendar_share_timeout}) + end, + lists:foreach(fun(Rec) -> ok = mnesia:dirty_write(Rec) end, Rows), + ok end end. +dump_calendar_share_rows() -> + Raw = try mnesia:dirty_select(calendar_share, [{'_', [], ['$_']}]) + catch _:_ -> [] + end, + [transform_row(R) || R <- Raw]. + +transform_row({calendar_share, CalId, UserId, Rights}) + when is_binary(CalId), is_binary(UserId) -> + #calendar_share{ + id = infra_utils:generate_id(16), + calendar_id = CalId, + user_id = UserId, + rights = Rights, + mirror_to_default = false + }; +transform_row({calendar_share, Id, CalId, UserId, Rights}) + when is_binary(Id), is_binary(CalId), is_binary(UserId) -> + #calendar_share{ + id = Id, + calendar_id = CalId, + user_id = UserId, + rights = Rights, + mirror_to_default = false + }; +transform_row(#calendar_share{} = Rec) -> + Rec; +transform_row(Other) -> + error({unexpected_calendar_share_row, Other}). + ensure_table(Table, Attrs) -> case lists:member(Table, mnesia:system_info(tables)) of true -> diff --git a/test/unit/calendar_share_migration_tests.erl b/test/unit/calendar_share_migration_tests.erl new file mode 100644 index 0000000..25f9cbc --- /dev/null +++ b/test/unit/calendar_share_migration_tests.erl @@ -0,0 +1,50 @@ +-module(calendar_share_migration_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:stop(), + mnesia:delete_schema([node()]), + mnesia:create_schema([node()]), + mnesia:start(), + {atomic, ok} = mnesia:create_table(calendar_share, [ + {ram_copies, [node()]}, + {attributes, [calendar_id, user_id, rights]} + ]), + ok. + +cleanup(_) -> + catch mnesia:delete_table(calendar_share), + catch mnesia:delete_table(calendar_share_invite), + mnesia:stop(), + mnesia:delete_schema([node()]), + ok. + +calendar_share_migration_test_() -> + {foreach, fun setup/0, fun cleanup/1, [ + {"transforms old calendar_share to id+mirror", fun test_transform_old_share/0}, + {"idempotent when already new schema", fun test_idempotent_new_schema/0} + ]}. + +test_transform_old_share() -> + %% Pre-#73 row shape: PK = calendar_id (no id / mirror_to_default). + ok = mnesia:dirty_write({calendar_share, <<"cal1">>, <<"u1">>, write}), + ?assertEqual([calendar_id, user_id, rights], + mnesia:table_info(calendar_share, attributes)), + ok = '20260815220000_calendar_share_invite':up(), + ?assertEqual(record_info(fields, calendar_share), + mnesia:table_info(calendar_share, attributes)), + [Rec] = mnesia:dirty_match_object(#calendar_share{calendar_id = <<"cal1">>, _ = '_'}), + ?assertMatch(#calendar_share{user_id = <<"u1">>, rights = write, + mirror_to_default = false}, Rec), + ?assert(is_binary(Rec#calendar_share.id)), + ?assert(lists:member(calendar_share_invite, mnesia:system_info(tables))), + {ok, Share} = core_calendar_share:upsert(<<"cal2">>, <<"u2">>, admin, true), + ?assertEqual(admin, Share#calendar_share.rights), + ?assertEqual(true, Share#calendar_share.mirror_to_default). + +test_idempotent_new_schema() -> + ok = '20260815220000_calendar_share_invite':up(), + ok = '20260815220000_calendar_share_invite':up(), + ?assertEqual(record_info(fields, calendar_share), + mnesia:table_info(calendar_share, attributes)). diff --git a/test/unit/migration_engine_tests.erl b/test/unit/migration_engine_tests.erl index c820993..49ed456 100755 --- a/test/unit/migration_engine_tests.erl +++ b/test/unit/migration_engine_tests.erl @@ -14,7 +14,10 @@ "20260720210000_calendar_follow", "20260722150000_calendar_specialist_id", "20260722190000_specialist_invite", - "20260730120000_recurrence_exception_bag" + "20260730120000_recurrence_exception_bag", + "20260814193000_waitlist_entry", + "20260815200000_push_subscription", + "20260815220000_calendar_share_invite" ]). setup() ->