-module(ets_cas). -behaviour(gen_server). %% API exports -export([new/1, close/1, get_doc/2, create_doc/4, put_doc/2, put_doc_cas/2, delete_doc/2, delete_doc_cas/2]). -export([set_val/2, get_val/1, set_map/2, get_map/1, set_mapval/3, get_mapval/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -record(doc, {id, rev, map, val}). -type doc() :: #doc{}. -type tab() :: atom(). -export_types([doc/0]). -include_lib("stdlib/include/ms_transform.hrl"). %%==================================================================== %% API functions %%==================================================================== %% @doc create a new table. -spec new(tab()) -> {ok, pid()}. new(Name) when is_atom(Name) -> gen_server:start_link({local, Name}, ?MODULE, [Name], []). %% @doc close a table -spec close(tab()) -> ok. close(Name) -> gen_server:call(Name, stop). %% @doc get a document from the table by its ID. -spec get_doc(tab(), term()) -> {ok, doc()} | error. get_doc(Name, Id) when is_binary(Id) -> case ets:lookup(Name, Id) of [Doc] -> {ok, Doc}; [] -> error end; get_doc(_, _) -> erlang:error(badarg). %% @doc create a new document and store it -spec create_doc(tab(), term(), map(), any()) -> {ok, doc()} | false. create_doc(Name, Key, Map, Val) -> Doc = #doc{ id=Key,rev=uniqid(), map=Map, val=Val }, case ets:insert_new(Name, Doc) of true -> {ok, Doc}; false -> false end. %% @doc replace a new doc without checking its version -spec put_doc(Tab :: tab(), Doc :: doc()) -> {ok, Doc1::doc()} | error. put_doc(Name, Doc0) -> Doc1 = Doc0#doc{ rev=uniqid() }, case ets:insert(Name, Doc1) of true -> {ok, Doc1}; Error -> Error end. -ifdef(USE_SELECT_REPLACE). %% @doc replace a new doc if we try to update the latest version -spec put_doc_cas(Tab :: tab(), Doc :: doc()) -> {ok, Doc1::doc()} | error. put_doc_cas(Name, Doc = #doc{ id=Id, rev=Rev, val=Val, map=Map } ) -> NewRev = uniqid(), MS = ets:fun2ms( fun(#doc{ id=FoundId, rev=FoundRev}=FoundDoc) when FoundId =:= Id, FoundRev =:= Rev -> FoundDoc#doc{ rev = NewRev, val=Val, map=Map } end), Replaced = (1 =:= ets:select_replace(Name, MS)), case Replaced of true -> {ok, Doc#doc{ rev=NewRev}}; false -> false end. -else. %% @doc replace a new doc if we try to update the latest version -spec put_doc_cas(Tab :: tab(), Doc :: doc()) -> {ok, Doc1::doc()} | error. put_doc_cas(Name, Doc) -> gen_server:call(Name, {put_cas, Doc}). -endif. %% @doc delete without checkiing the version -spec delete_doc(tab(), doc()) -> ok. delete_doc(Name, #doc{ id= Id }) -> ets:delete(Name, Id); delete_doc(Name, Id) when is_binary(Id) -> ets:delete(Name, Id); delete_doc(_, _) -> erlang:error(badarg). -spec delete_doc_cas(Tab :: tab(), Doc :: doc()) -> ok | error. delete_doc_cas(Name, #doc{id=Id, rev=Rev}) -> MS = ets:fun2ms( fun(#doc{ id=FoundId, rev=FoundRev}) when FoundId =:= Id, FoundRev =:= Rev -> true end), Deleted = (1 =:= ets:select_delete(Name, MS)), case Deleted of true -> ok; false -> error end. %% get the value from doc -spec get_val(Doc :: doc()) -> Val :: term(). get_val(#doc{ val=Val } ) -> Val. %% @doc set the value of the doc -spec set_val(Val :: term(), Doc :: doc()) -> Doc1 :: doc(). set_val(Val, Doc) -> Doc#doc{ val = Val }. %% @doc get the map of the doc -spec get_map(Doc :: doc()) -> Map :: map(). get_map(#doc{ map=Map }) -> Map. %% @doc set the map of the doc -spec set_map(Map :: map(), Doc :: doc()) -> Doc1 :: map(). set_map(Map, Doc) when is_map(Map) -> Doc#doc{map=Map}; set_map(_, _) -> erlang:error(badarg). %% @doc get the map an dvalue of a doc -spec get_mapval(Doc :: doc()) -> { Map :: map(), Val :: term()}. get_mapval(#doc{ map=Map, val=Val }) -> {Map, Val}. %% @doc set the map and value of a doc -spec set_mapval(Map :: map(), Val :: term(), Doc :: doc()) -> Doc1 :: doc(). set_mapval(Map, Val, Doc) when is_map(Val) -> Doc#doc{ map=Map, val=Val }; set_mapval(_, _, _) -> erlang:error(badarg). %%==================================================================== %% gen_server callbacks %%==================================================================== init([Name]) -> _ = ets:new(Name, [ordered_set, public, named_table, {keypos, #doc.id}, {read_concurrency, true}, {write_concurrency, true} ]), {ok, Name}. handle_call({put_cas, Doc = #doc{id=Id, rev=Rev}}, _From, State) -> Reply = case ets:lookup(State, Id) of [#doc{ rev=Rev }] -> NewDoc = Doc#doc{ rev = uniqid() }, case ets:insert(State, NewDoc) of true -> {ok, NewDoc}; _Error -> false end; _ -> false end, {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Msg, _From, State) -> {reply, badcall, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Msg, State) -> {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. terminate(_Reason, _State) -> ok. %% ------------------ %% helpers uniqid() -> <> = uuid:get_v4(), Id.