Current section

Files

Jump to
evoq src evoq_read_model_ets.erl
Raw

src/evoq_read_model_ets.erl

%% @doc ETS-based read model store implementation.
%%
%% Default in-memory implementation of evoq_read_model behavior.
%% Suitable for development, testing, and small-scale deployments.
%%
%% == Configuration ==
%%
%% - name: ETS table name (default: auto-generated)
%% - type: ETS table type (default: set)
%% - access: public | protected | private (default: public)
%%
%% Named tables are shared by default: if the table already exists,
%% new instances join it instead of crashing. This allows multiple
%% projections to write to the same read model while each maintaining
%% their own checkpoint.
%%
%% @author rgfaber
-module(evoq_read_model_ets).
-behaviour(evoq_read_model).
%% Behavior callbacks
-export([init/1, get/2, put/3, delete/2]).
-export([list/2, clear/1]).
-record(state, {
table :: ets:tid()
}).
%%====================================================================
%% Behavior callbacks
%%====================================================================
%% @doc Initialize ETS-backed read model.
-spec init(map()) -> {ok, #state{}} | {error, term()}.
init(Config) ->
Type = maps:get(type, Config, set),
Access = maps:get(access, Config, public),
case maps:get(name, Config, undefined) of
undefined ->
%% Anonymous table — isolated per instance
Table = ets:new(?MODULE, [Type, Access, {read_concurrency, true}]),
{ok, #state{table = Table}};
Name when is_atom(Name) ->
%% Named table — shared across projections
Table = case ets:whereis(Name) of
undefined ->
ets:new(Name, [Type, Access, named_table, {read_concurrency, true}]);
Tid ->
Tid
end,
{ok, #state{table = Table}}
end.
%% @doc Get a value by key.
-spec get(term(), #state{}) -> {ok, term()} | {error, not_found}.
get(Key, #state{table = Table}) ->
case ets:lookup(Table, Key) of
[{Key, Value}] ->
{ok, Value};
[] ->
{error, not_found}
end.
%% @doc Store a key-value pair.
-spec put(term(), term(), #state{}) -> {ok, #state{}}.
put(Key, Value, #state{table = Table} = State) ->
true = ets:insert(Table, {Key, Value}),
{ok, State}.
%% @doc Delete a key.
-spec delete(term(), #state{}) -> {ok, #state{}}.
delete(Key, #state{table = Table} = State) ->
true = ets:delete(Table, Key),
{ok, State}.
%% @doc List all key-value pairs with matching prefix.
%% For ETS, prefix matching works on tuple/list keys.
-spec list(term(), #state{}) -> {ok, [{term(), term()}]}.
list(Prefix, #state{table = Table}) ->
MatchSpec = case Prefix of
all ->
[{{'$1', '$2'}, [], [{{'$1', '$2'}}]}];
_ when is_binary(Prefix) ->
%% Binary prefix match
PrefixSize = byte_size(Prefix),
[{{'$1', '$2'},
[{'andalso',
{is_binary, '$1'},
{'=:=', {binary_part, '$1', 0, PrefixSize}, Prefix}}],
[{{'$1', '$2'}}]}];
_ when is_tuple(Prefix) ->
%% Tuple prefix match (first element)
FirstElement = element(1, Prefix),
[{{'$1', '$2'},
[{'andalso',
{is_tuple, '$1'},
{'=:=', {element, 1, '$1'}, FirstElement}}],
[{{'$1', '$2'}}]}];
_ ->
%% No prefix filtering
[{{'$1', '$2'}, [], [{{'$1', '$2'}}]}]
end,
Results = ets:select(Table, MatchSpec),
{ok, Results}.
%% @doc Clear all data from the table.
-spec clear(#state{}) -> {ok, #state{}}.
clear(#state{table = Table} = State) ->
true = ets:delete_all_objects(Table),
{ok, State}.