src/khepri_tx.erl

%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright © 2021-2022 VMware, Inc. or its affiliates.  All rights reserved.
%%

%% @doc Khepri API for transactional queries and updates.
%%
%% Transactions are anonymous functions which take no arguments, much like
%% what Mnesia supports. However, unlike with Mnesia, transaction functions in
%% Khepri are restricted:
%%
%% <ul>
%% <li>Calls to BIFs and other functions is limited to a set of whitelisted
%% APIs. See {@link is_remote_call_valid/3} for the complete list.</li>
%% <li>Sending or receiving messages is denied.</li>
%% </ul>
%%
%% The reason is that the transaction function must always have the exact same
%% outcome given its inputs. Indeed, the transaction function is executed on
%% every Ra cluster members participating in the consensus. The function must
%% therefore modify the Khepri state (the database) identically on all Ra
%% members. This is also true for Ra members joining the cluster later or
%% catching up after a network partition.
%%
%% To achieve that:
%% <ol>
%% <li>The code of the transaction function is extracted from the its initial
%% Erlang module. This way, the transaction function does not depend on the
%% initial module availability and is not affected by a module reload. See
%% {@link khepri_fun})</li>
%% <li>The code is verified to make sure it does not perform any denied
%% operations.</li>
%% <li>The extracted transaction function is stored as a Khepri state machine
%% command in the Ra journal to be replicated on all Ra members.</li>
%% </ol>
%%
%% Functions in this module have simplified return values to cover most
%% frequent use cases. If you need more details about the queried or modified
%% tree nodes, like the ability to distinguish a non-existent tree node from a
%% tree node with no payload, you can use the {@link khepri_tx_adv} module.

-module(khepri_tx).

-include_lib("stdlib/include/assert.hrl").

-include("include/khepri.hrl").
-include("src/khepri_error.hrl").
-include("src/khepri_machine.hrl").
-include("src/khepri_ret.hrl").
-include("src/khepri_tx.hrl").

%% IMPORTANT: When adding a new khepri_tx function to be used inside a
%% transaction function:
%%   1. The function must be added to the whitelist in
%%      `khepri_tx_adv:is_remote_call_valid()' in this file.
%%   2. If the function modifies the tree, it must be handled in
%%      `khepri_tx_adv:is_standalone_fun_still_needed()' as well.
-export([get/1, get/2,
         get_or/2, get_or/3,
         get_many/1, get_many/2,
         get_many_or/2, get_many_or/3,
         exists/1, exists/2,
         has_data/1, has_data/2,
         is_sproc/1, is_sproc/2,
         count/1, count/2,
         fold/3, fold/4,
         foreach/2, foreach/3,
         map/2, map/3,
         filter/2, filter/3,

         put/2, put/3,
         put_many/2, put_many/3,
         create/2, create/3,
         update/2, update/3,
         compare_and_swap/3, compare_and_swap/4,

         delete/1, delete/2,
         delete_many/1, delete_many/2,
         clear_payload/1, clear_payload/2,
         clear_many_payloads/1, clear_many_payloads/2,

         abort/1,
         is_transaction/0]).

-compile({no_auto_import, [get/1, put/2, erase/1]}).

%% FIXME: Dialyzer complains about several functions with "optional" arguments
%% (but not all). I believe the specs are correct, but can't figure out how to
%% please Dialyzer. So for now, let's disable this specific check for the
%% problematic functions.
-dialyzer({no_underspecs, [exists/1,
                           has_data/1, has_data/2]}).

-type tx_fun_result() :: any() | no_return().
%% Return value of a transaction function.

-type tx_fun() :: fun().
%% Transaction function signature.

-type tx_abort() :: khepri:error(any()).
%% Return value after a transaction function aborted.

-export_type([tx_fun/0,
              tx_fun_result/0,
              tx_abort/0]).

%% -------------------------------------------------------------------
%% get().
%% -------------------------------------------------------------------

-spec get(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:payload_ret().
%% @doc Returns the payload of the tree node pointed to by the given path
%% pattern.
%%
%% This is the same as {@link khepri:get/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:get/2.

get(PathPattern) ->
    get(PathPattern, #{}).

-spec get(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Ret :: khepri:payload_ret().
%% @doc Returns the payload of the tree node pointed to by the given path
%% pattern.
%%
%% This is the same as {@link khepri:get/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:get/3.

get(PathPattern, Options) ->
    case khepri_tx_adv:get(PathPattern, Options) of
        {ok, #{data := Data}}           -> {ok, Data};
        {ok, #{sproc := StandaloneFun}} -> {ok, StandaloneFun};
        {ok, _}                         -> {ok, undefined};
        Error                           -> Error
    end.

%% -------------------------------------------------------------------
%% get_or().
%% -------------------------------------------------------------------

-spec get_or(PathPattern, Default) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Default :: khepri:data(),
      Ret :: khepri:payload_ret().
%% @doc Returns the payload of the tree node pointed to by the given path
%% pattern, or a default value.
%%
%% This is the same as {@link khepri:get_or/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_or/3.

get_or(PathPattern, Default) ->
    get_or(PathPattern, Default, #{}).

-spec get_or(PathPattern, Default, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Default :: khepri:data(),
      Options :: khepri:tree_options(),
      Ret :: khepri:payload_ret().
%% @doc Returns the payload of the tree node pointed to by the given path
%% pattern, or a default value.
%%
%% This is the same as {@link khepri:get_or/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_or/4.

get_or(PathPattern, Default, Options) ->
    case khepri_tx_adv:get(PathPattern, Options) of
        {ok, #{data := Data}}                     -> {ok, Data};
        {ok, #{sproc := StandaloneFun}}           -> {ok, StandaloneFun};
        {ok, _}                                   -> {ok, Default};
        {error, ?khepri_error(node_not_found, _)} -> {ok, Default};
        Error                                     -> Error
    end.

%% -------------------------------------------------------------------
%% get_many().
%% -------------------------------------------------------------------

-spec get_many(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns payloads of all the tree nodes matching the given path
%% pattern.
%%
%% This is the same as {@link khepri:get_many/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_many/2.

get_many(PathPattern) ->
    get_many(PathPattern, #{}).

-spec get_many(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns payloads of all the tree nodes matching the given path
%% pattern.
%%
%% This is the same as {@link khepri:get_many/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_many/3.

get_many(PathPattern, Options) ->
    get_many_or(PathPattern, undefined, Options).

%% -------------------------------------------------------------------
%% get_many_or().
%% -------------------------------------------------------------------

-spec get_many_or(PathPattern, Default) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Default :: khepri:data(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns payloads of all the tree nodes matching the given path
%% pattern, or a default payload.
%%
%% This is the same as {@link khepri:get_many_or/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_many_or/3.

get_many_or(PathPattern, Default) ->
    get_many_or(PathPattern, Default, #{}).

-spec get_many_or(PathPattern, Default, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Default :: khepri:data(),
      Options :: khepri:tree_options(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns payloads of all the tree nodes matching the given path
%% pattern, or a default payload.
%%
%% This is the same as {@link khepri:get_many_or/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:get_many_or/4.

get_many_or(PathPattern, Default, Options) ->
    Fun = fun(Path, NodeProps, Acc) ->
                  Payload = khepri_utils:node_props_to_payload(
                              NodeProps, Default),
                  Acc#{Path => Payload}
          end,
    Acc = #{},
    khepri_tx_adv:do_get_many(PathPattern, Fun, Acc, Options).

%% -------------------------------------------------------------------
%% exists().
%% -------------------------------------------------------------------

-spec exists(PathPattern) -> Exists when
      PathPattern :: khepri_path:pattern(),
      Exists :: boolean().
%% @doc Indicates if the tree node pointed to by the given path exists or not.
%%
%% This is the same as {@link khepri:exists/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:exists/2.

exists(PathPattern) ->
    exists(PathPattern, #{}).

-spec exists(PathPattern, Options) -> Exists when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Exists :: boolean().
%% @doc Indicates if the tree node pointed to by the given path exists or not.
%%
%% This is the same as {@link khepri:exists/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:exists/3.

exists(PathPattern, Options) ->
    Options1 = Options#{expect_specific_node => true,
                        props_to_return => []},
    case khepri_tx_adv:get_many(PathPattern, Options1) of
        {ok, _}                                   -> true;
        {error, ?khepri_error(node_not_found, _)} -> false;
        Error                                     -> Error
    end.

%% -------------------------------------------------------------------
%% has_data().
%% -------------------------------------------------------------------

-spec has_data(PathPattern) -> HasData | Error when
      PathPattern :: khepri_path:pattern(),
      HasData :: boolean(),
      Error :: khepri:error().
%% @doc Indicates if the tree node pointed to by the given path has data or
%% not.
%%
%% This is the same as {@link khepri:has_data/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:has_data/2.

has_data(PathPattern) ->
    has_data(PathPattern, #{}).

-spec has_data(PathPattern, Options) -> HasData | Error when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      HasData :: boolean(),
      Error :: khepri:error().
%% @doc Indicates if the tree node pointed to by the given path has data or
%% not.
%%
%% This is the same as {@link khepri:has_data/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:has_data/3.

has_data(PathPattern, Options) ->
    Options1 = Options#{expect_specific_node => true,
                        props_to_return => [has_payload]},
    case khepri_tx_adv:get_many(PathPattern, Options1) of
        {ok, NodePropsMap} ->
            [NodeProps] = maps:values(NodePropsMap),
            maps:get(has_data, NodeProps, false);
        {error, ?khepri_error(node_not_found, _)} ->
            false;
        Error ->
            Error
    end.

%% -------------------------------------------------------------------
%% is_sproc().
%% -------------------------------------------------------------------

-spec is_sproc(PathPattern) -> IsSproc | Error when
      PathPattern :: khepri_path:pattern(),
      IsSproc :: boolean(),
      Error :: khepri:error().
%% @doc Indicates if the tree node pointed to by the given path holds a stored
%% procedure or not.
%%
%% This is the same as {@link khepri:is_sproc/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:is_sproc/2.

is_sproc(PathPattern) ->
    is_sproc(PathPattern, #{}).

-spec is_sproc(PathPattern, Options) -> IsSproc | Error when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      IsSproc :: boolean(),
      Error :: khepri:error().
%% @doc Indicates if the tree node pointed to by the given path holds a stored
%% procedure or not.
%%
%% This is the same as {@link khepri:is_sproc/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:is_sproc/3.

is_sproc(PathPattern, Options) ->
    Options1 = Options#{expect_specific_node => true,
                        props_to_return => [has_payload]},
    case khepri_tx_adv:get_many(PathPattern, Options1) of
        {ok, NodePropsMap} ->
            [NodeProps] = maps:values(NodePropsMap),
            maps:get(is_sproc, NodeProps, false);
        {error, ?khepri_error(node_not_found, _)} ->
            false;
        Error ->
            Error
    end.

%% -------------------------------------------------------------------
%% count().
%% -------------------------------------------------------------------

-spec count(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:ok(Count) | khepri:error(),
      Count :: non_neg_integer().
%% @doc Counts all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:count/2} but inside the context of a
%% transaction function.
%%
%% @see khepri:count/2.

count(PathPattern) ->
    count(PathPattern, #{}).

-spec count(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Ret :: khepri:ok(Count) | khepri:error(),
      Count :: non_neg_integer().
%% @doc Counts all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:count/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:count/3.

count(PathPattern, Options) ->
    PathPattern1 = khepri_tx_adv:path_from_string(PathPattern),
    {#khepri_machine{root = Root},
     _SideEffects} = khepri_tx_adv:get_tx_state(),
    Fun = fun khepri_machine:count_node_cb/3,
    {_QueryOptions, TreeOptions} = khepri_machine:split_query_options(Options),
    TreeOptions1 = TreeOptions#{expect_specific_node => false},
    Ret = khepri_machine:find_matching_nodes(
            Root, PathPattern1, Fun, 0, TreeOptions1),
    case Ret of
        {error, ?khepri_exception(_, _) = Exception} ->
            ?khepri_misuse(Exception);
        _ ->
            Ret
    end.

%% -------------------------------------------------------------------
%% fold().
%% -------------------------------------------------------------------

-spec fold(PathPattern, Fun, Acc) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:fold_fun(),
      Acc :: khepri:fold_acc(),
      Ret :: khepri:ok(NewAcc) | khepri:error(),
      NewAcc :: Acc.
%% @doc Calls `Fun' on successive tree nodes matching the given path pattern,
%% starting with `Acc'.
%%
%% This is the same as {@link khepri:fold/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:fold/4.

fold(PathPattern, Fun, Acc) ->
    fold(PathPattern, Fun, Acc, #{}).

-spec fold(PathPattern, Fun, Acc, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:fold_fun(),
      Acc :: khepri:fold_acc(),
      Options :: khepri:tree_options(),
      Ret :: khepri:ok(NewAcc) | khepri:error(),
      NewAcc :: Acc.
%% @doc Calls `Fun' on successive tree nodes matching the given path pattern,
%% starting with `Acc'.
%%
%% This is the same as {@link khepri:fold/5} but inside the context of a
%% transaction function.
%%
%% @see khepri:fold/5.

fold(PathPattern, Fun, Acc, Options) ->
    PathPattern1 = khepri_tx_adv:path_from_string(PathPattern),
    {#khepri_machine{root = Root},
     _SideEffects} = khepri_tx_adv:get_tx_state(),
    {_QueryOptions, TreeOptions} = khepri_machine:split_query_options(Options),
    TreeOptions1 = TreeOptions#{expect_specific_node => false},
    Ret = khepri_machine:find_matching_nodes(
            Root, PathPattern1, Fun, Acc, TreeOptions1),
    case Ret of
        {error, ?khepri_exception(_, _) = Exception} ->
            ?khepri_misuse(Exception);
        _ ->
            Ret
    end.

%% -------------------------------------------------------------------
%% foreach().
%% -------------------------------------------------------------------

-spec foreach(PathPattern, Fun) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:foreach_fun(),
      Ret :: ok | khepri:error().
%% @doc Calls `Fun' for each tree node matching the given path pattern.
%%
%% This is the same as {@link khepri:foreach/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:foreach/3.

foreach(PathPattern, Fun) ->
    foreach(PathPattern, Fun, #{}).

-spec foreach(PathPattern, Fun, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:foreach_fun(),
      Options :: khepri:tree_options(),
      Ret :: ok | khepri:error().
%% @doc Calls `Fun' for each tree node matching the given path pattern.
%%
%% This is the same as {@link khepri:foreach/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:foreach/4.

foreach(PathPattern, Fun, Options) when is_function(Fun, 2) ->
    FoldFun = fun(Path, NodeProps, Acc) ->
                      _ = Fun(Path, NodeProps),
                      Acc
              end,
    case fold(PathPattern, FoldFun, ok, Options) of
        {ok, ok}                 -> ok;
        {error, _Reason} = Error -> Error
    end.

%% -------------------------------------------------------------------
%% map().
%% -------------------------------------------------------------------

-spec map(PathPattern, Fun) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:map_fun(),
      Ret :: khepri:ok(Map) | khepri:error(),
      Map :: #{khepri_path:native_path() => khepri:map_fun_ret()}.
%% @doc Produces a new map by calling `Fun' for each tree node matching the
%% given path pattern.
%%
%% This is the same as {@link khepri:map/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:map/3.

map(PathPattern, Fun) ->
    map(PathPattern, Fun, #{}).

-spec map(PathPattern, Fun, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Fun :: khepri:map_fun(),
      Options :: khepri:tree_options(),
      Ret :: khepri:ok(Map) | khepri:error(),
      Map :: #{khepri_path:native_path() => khepri:map_fun_ret()}.
%% @doc Produces a new map by calling `Fun' for each tree node matching the
%% given path pattern.
%%
%% This is the same as {@link khepri:map/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:map/4.

map(PathPattern, Fun, Options) when is_function(Fun, 2) ->
    FoldFun = fun(Path, NodeProps, Acc) ->
                      Ret = Fun(Path, NodeProps),
                      Acc#{Path => Ret}
              end,
    fold(PathPattern, FoldFun, #{}, Options).

%% -------------------------------------------------------------------
%% filter().
%% -------------------------------------------------------------------

-spec filter(PathPattern, Pred) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Pred :: khepri:filter_fun(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns a map for which predicate `Pred' holds true in tree nodes
%% matching the given path pattern.
%%
%% This is the same as {@link khepri:filter/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:filter/3.

filter(PathPattern, Pred) ->
    filter(PathPattern, Pred, #{}).

-spec filter(PathPattern, Pred, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Pred :: khepri:filter_fun(),
      Options :: khepri:tree_options(),
      Ret :: khepri:many_payloads_ret().
%% @doc Returns a map for which predicate `Pred' holds true in tree nodes
%% matching the given path pattern.
%%
%% This is the same as {@link khepri:filter/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:filter/4.

filter(PathPattern, Pred, Options) when is_function(Pred, 2) ->
    FoldFun = fun(Path, NodeProps, Acc) ->
                      case Pred(Path, NodeProps) of
                          true ->
                              Payload = khepri_utils:node_props_to_payload(
                                          NodeProps, undefined),
                              Acc#{Path => Payload};
                          false ->
                              Acc
                      end
              end,
    fold(PathPattern, FoldFun, #{}, Options).

%% -------------------------------------------------------------------
%% put().
%% -------------------------------------------------------------------

-spec put(PathPattern, Data) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Ret :: khepri:minimal_ret().
%% @doc Runs the stored procedure pointed to by the given path and returns the
%% result.
%%
%% This is the same as {@link khepri:put/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:put/3.

put(PathPattern, Data) ->
    put(PathPattern, Data, #{}).

-spec put(PathPattern, Data, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Runs the stored procedure pointed to by the given path and returns the
%% result.
%%
%% This is the same as {@link khepri:put/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:put/4.

put(PathPattern, Data, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:put(PathPattern, Data, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% put_many().
%% -------------------------------------------------------------------

-spec put_many(PathPattern, Data) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Ret :: khepri:minimal_ret().
%% @doc Sets the payload of all the tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:put_many/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:put_many/3.

put_many(PathPattern, Data) ->
    put_many(PathPattern, Data, #{}).

-spec put_many(PathPattern, Data, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Sets the payload of all the tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:put_many/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:put_many/4.

put_many(PathPattern, Data, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:put_many(PathPattern, Data, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% create().
%% -------------------------------------------------------------------

-spec create(PathPattern, Data) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Ret :: khepri:minimal_ret().
%% @doc Creates a tree node with the given payload.
%%
%% This is the same as {@link khepri:create/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:create/3.

create(PathPattern, Data) ->
    create(PathPattern, Data, #{}).

-spec create(PathPattern, Data, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Creates a tree node with the given payload.
%%
%% This is the same as {@link khepri:create/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:create/4.

create(PathPattern, Data, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:create(PathPattern, Data, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% update().
%% -------------------------------------------------------------------

-spec update(PathPattern, Data) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Ret :: khepri:minimal_ret().
%% @doc Updates an existing tree node with the given payload.
%%
%% This is the same as {@link khepri:update/3} but inside the context of a
%% transaction function.
%%
%% @see khepri:update/3.

update(PathPattern, Data) ->
    update(PathPattern, Data, #{}).

-spec update(PathPattern, Data, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Updates an existing tree node with the given payload.
%%
%% This is the same as {@link khepri:update/4} but inside the context of a
%% transaction function.
%%
%% @see khepri:update/4.

update(PathPattern, Data, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:update(PathPattern, Data, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% compare_and_swap().
%% -------------------------------------------------------------------

-spec compare_and_swap(PathPattern, DataPattern, Data) -> Ret when
      PathPattern :: khepri_path:pattern(),
      DataPattern :: ets:match_pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Ret :: khepri:minimal_ret().
%% @doc Updates an existing tree node with the given payload only if its data
%% matches the given pattern.
%%
%% This is the same as {@link khepri:compare_and_swap/4} but inside the context
%% of a transaction function.
%%
%% @see khepri:compare_and_swap/4.

compare_and_swap(PathPattern, DataPattern, Data) ->
    compare_and_swap(PathPattern, DataPattern, Data, #{}).

-spec compare_and_swap(PathPattern, DataPattern, Data, Options) ->
    Ret when
      PathPattern :: khepri_path:pattern(),
      DataPattern :: ets:match_pattern(),
      Data :: khepri_payload:payload() | khepri:data() | fun(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Updates an existing tree node with the given payload only if its data
%% matches the given pattern.
%%
%% This is the same as {@link khepri:compare_and_swap/5} but inside the context
%% of a transaction function.
%%
%% @see khepri:compare_and_swap/5.

compare_and_swap(PathPattern, DataPattern, Data, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:compare_and_swap(
            PathPattern, DataPattern, Data, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% delete().
%% -------------------------------------------------------------------

-spec delete(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the tree node pointed to by the given path pattern.
%%
%% This is the same as {@link khepri:delete/2} but inside the context
%% of a transaction function.
%%
%% @see khepri:delete/2.

delete(PathPattern) ->
    delete(PathPattern, #{}).

-spec delete(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the tree node pointed to by the given path pattern.
%%
%% This is the same as {@link khepri:delete/3} but inside the context
%% of a transaction function.
%%
%% @see khepri:delete/3.

delete(PathPattern, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:delete(PathPattern, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% delete_many().
%% -------------------------------------------------------------------

-spec delete_many(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:delete_many/2} but inside the context
%% of a transaction function.
%%
%% @see khepri:delete_many/2.

delete_many(PathPattern) ->
    delete_many(PathPattern, #{}).

-spec delete_many(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:delete_many/3} but inside the context
%% of a transaction function.
%%
%% @see khepri:delete_many/3.

delete_many(PathPattern, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:delete_many(PathPattern, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% clear_payload().
%% -------------------------------------------------------------------

-spec clear_payload(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the payload of the tree node pointed to by the given path
%% pattern.
%%
%% This is the same as {@link khepri:clear_payload/2} but inside the context
%% of a transaction function.
%%
%% @see khepri:clear_payload/2.

clear_payload(PathPattern) ->
    clear_payload(PathPattern, #{}).

-spec clear_payload(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the payload of the tree node pointed to by the given path
%% pattern.
%%
%% This is the same as {@link khepri:clear_payload/3} but inside the context
%% of a transaction function.
%%
%% @see khepri:clear_payload/3.

clear_payload(PathPattern, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:clear_payload(PathPattern, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% clear_many_payloads().
%% -------------------------------------------------------------------

-spec clear_many_payloads(PathPattern) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the payload of all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:clear_many_payloads/2} but inside the
%% context of a transaction function.
%%
%% @see khepri:clear_many_payloads/2.

clear_many_payloads(PathPattern) ->
    clear_many_payloads(PathPattern, #{}).

-spec clear_many_payloads(PathPattern, Options) -> Ret when
      PathPattern :: khepri_path:pattern(),
      Options :: khepri:tree_options() | khepri:put_options(),
      Ret :: khepri:minimal_ret().
%% @doc Deletes the payload of all tree nodes matching the given path pattern.
%%
%% This is the same as {@link khepri:clear_many_payloads/3} but inside the
%% context of a transaction function.
%%
%% @see khepri:clear_many_payloads/3.

clear_many_payloads(PathPattern, Options) ->
    Options1 = Options#{props_to_return => []},
    Ret = khepri_tx_adv:clear_many_payloads(PathPattern, Options1),
    ?result_ret_to_minimal_ret(Ret).

%% -------------------------------------------------------------------
%% abort().
%% -------------------------------------------------------------------

-spec abort(Reason) -> no_return() when
      Reason :: any().
%% @doc Aborts the transaction.
%%
%% Any changes so far are not committed to the store.
%%
%% {@link khepri:transaction/1} and friends will return {@link tx_abort()}.
%%
%% @param Reason term to return to caller of the transaction.

abort(Reason) ->
    throw(?TX_ABORT(Reason)).

%% -------------------------------------------------------------------
%% is_transaction().
%% -------------------------------------------------------------------

-spec is_transaction() -> boolean().
%% @doc Indicates if the calling function runs in the context of a transaction
%% function.
%%
%% @returns `true' if the calling code runs inside a transaction function,
%% `false' otherwise.

is_transaction() ->
    StateAndSideEffects = erlang:get(?TX_STATE_KEY),
    case StateAndSideEffects of
        {#khepri_machine{}, _SideEffects} -> true;
        _                                 -> false
    end.