Packages

macula

2.0.0
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_mri_ets.erl
Raw

src/macula_mri_ets.erl

%%%-------------------------------------------------------------------
%%% @doc MRI ETS Storage Adapter
%%%
%%% In-memory storage adapter using ETS tables.
%%% Implements both macula_mri_store and macula_mri_graph behaviours.
%%%
%%% Suitable for development, testing, and single-node deployments.
%%% For distributed deployments, use macula_mri_khepri.
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_mri_ets).
-behaviour(gen_server).
-behaviour(macula_mri_store).
-behaviour(macula_mri_graph).
-compile({no_auto_import, [register/2]}).
%% macula_mri_store callbacks
-export([register/2, lookup/1, update/2, delete/1, exists/1]).
-export([list_children/1, list_descendants/1, list_by_type/2, list_by_realm/1]).
-export([import/1, export/0]).
%% macula_mri_graph callbacks
-export([create_relationship/3, create_relationship/4, delete_relationship/3]).
-export([related_to/2, related_from/2, all_related/1]).
-export([traverse_transitive/3, get_relationship/3]).
-export([instances_of/1, instances_of_transitive/1, classes_of/1]).
-export([subclasses/1, superclasses/1]).
%% API
-export([start_link/0, start_link/1, stop/0]).
-export([clear/0, stats/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-define(SERVER, ?MODULE).
-define(MRI_TABLE, macula_mri_ets_mris).
-define(REL_FORWARD_TABLE, macula_mri_ets_rel_forward).
-define(REL_REVERSE_TABLE, macula_mri_ets_rel_reverse).
-define(TYPE_INDEX_TABLE, macula_mri_ets_type_idx).
-define(REALM_INDEX_TABLE, macula_mri_ets_realm_idx).
-record(state, {}).
-record(mri_entry, {
mri :: binary(),
type :: atom(),
realm :: binary(),
path :: [binary()],
metadata :: map(),
registered_at :: integer()
}).
-record(rel_entry, {
key :: {binary(), atom() | {custom, binary()}, binary()}, %% {Subject, Predicate, Object}
subject :: binary(),
predicate :: atom() | {custom, binary()},
object :: binary(),
metadata :: map(),
created_at :: integer()
}).
%%===================================================================
%% API
%%===================================================================
%% @doc Start the ETS adapter.
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
start_link([]).
-spec start_link(list()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Opts, []).
%% @doc Stop the ETS adapter.
-spec stop() -> ok.
stop() ->
gen_server:stop(?SERVER).
%% @doc Clear all data.
-spec clear() -> ok.
clear() ->
gen_server:call(?SERVER, clear).
%% @doc Get statistics.
-spec stats() -> map().
stats() ->
#{
mri_count => ets:info(?MRI_TABLE, size),
relationship_count => ets:info(?REL_FORWARD_TABLE, size)
}.
%%===================================================================
%% macula_mri_store callbacks
%%===================================================================
-spec register(binary(), map()) -> ok | {error, term()}.
register(MRI, Metadata) when is_binary(MRI), is_map(Metadata) ->
case macula_mri:parse(MRI) of
{ok, #{type := Type, realm := Realm, path := Path}} ->
Entry = #mri_entry{
mri = MRI,
type = Type,
realm = Realm,
path = Path,
metadata = Metadata,
registered_at = erlang:system_time(millisecond)
},
true = ets:insert(?MRI_TABLE, Entry),
%% Update indexes
true = ets:insert(?TYPE_INDEX_TABLE, {{Type, Realm, MRI}, true}),
true = ets:insert(?REALM_INDEX_TABLE, {{Realm, MRI}, true}),
ok;
{error, _} = Err ->
Err
end.
-spec lookup(binary()) -> {ok, map()} | {error, not_found | term()}.
lookup(MRI) when is_binary(MRI) ->
case ets:lookup(?MRI_TABLE, MRI) of
[#mri_entry{metadata = Metadata, registered_at = Ts}] ->
{ok, Metadata#{registered_at => Ts}};
[] ->
{error, not_found}
end.
-spec update(binary(), map()) -> ok | {error, term()}.
update(MRI, Metadata) when is_binary(MRI), is_map(Metadata) ->
case ets:lookup(?MRI_TABLE, MRI) of
[#mri_entry{} = Entry] ->
Updated = Entry#mri_entry{metadata = Metadata},
true = ets:insert(?MRI_TABLE, Updated),
ok;
[] ->
{error, not_found}
end.
-spec delete(binary()) -> ok | {error, term()}.
delete(MRI) when is_binary(MRI) ->
case ets:lookup(?MRI_TABLE, MRI) of
[#mri_entry{type = Type, realm = Realm}] ->
true = ets:delete(?MRI_TABLE, MRI),
true = ets:delete(?TYPE_INDEX_TABLE, {Type, Realm, MRI}),
true = ets:delete(?REALM_INDEX_TABLE, {Realm, MRI}),
ok;
[] ->
{error, not_found}
end.
-spec exists(binary()) -> boolean().
exists(MRI) when is_binary(MRI) ->
ets:member(?MRI_TABLE, MRI).
-spec list_children(binary()) -> [binary()].
list_children(MRI) when is_binary(MRI) ->
case macula_mri:parse(MRI) of
{ok, #{realm := Realm, path := Path}} ->
ParentDepth = length(Path),
%% Find all MRIs that have this as parent
ets:foldl(
fun(#mri_entry{mri = ChildMRI, realm = R, path = P}, Acc)
when R =:= Realm, length(P) =:= ParentDepth + 1 ->
case lists:prefix(Path, P) of
true -> [ChildMRI | Acc];
false -> Acc
end;
(_, Acc) ->
Acc
end,
[],
?MRI_TABLE
);
{error, _} ->
[]
end.
-spec list_descendants(binary()) -> [binary()].
list_descendants(MRI) when is_binary(MRI) ->
case macula_mri:parse(MRI) of
{ok, #{realm := Realm, path := Path}} ->
ParentDepth = length(Path),
%% Find all MRIs that have this as ancestor
ets:foldl(
fun(#mri_entry{mri = DescMRI, realm = R, path = P}, Acc)
when R =:= Realm, length(P) > ParentDepth ->
case lists:prefix(Path, P) of
true -> [DescMRI | Acc];
false -> Acc
end;
(_, Acc) ->
Acc
end,
[],
?MRI_TABLE
);
{error, _} ->
[]
end.
-spec list_by_type(atom(), binary()) -> [binary()].
list_by_type(Type, Realm) when is_atom(Type), is_binary(Realm) ->
Pattern = {{Type, Realm, '$1'}, '_'},
[MRI || [MRI] <- ets:match(?TYPE_INDEX_TABLE, Pattern)].
-spec list_by_realm(binary()) -> [binary()].
list_by_realm(Realm) when is_binary(Realm) ->
Pattern = {{Realm, '$1'}, '_'},
[MRI || [MRI] <- ets:match(?REALM_INDEX_TABLE, Pattern)].
-spec import([{binary(), map()}]) -> ok | {error, term()}.
import(Entries) when is_list(Entries) ->
lists:foreach(
fun({MRI, Metadata}) -> register(MRI, Metadata) end,
Entries
),
ok.
-spec export() -> {ok, [{binary(), map()}]} | {error, term()}.
export() ->
Entries = ets:foldl(
fun(#mri_entry{mri = MRI, metadata = Meta}, Acc) ->
[{MRI, Meta} | Acc]
end,
[],
?MRI_TABLE
),
{ok, Entries}.
%%===================================================================
%% macula_mri_graph callbacks
%%===================================================================
-spec create_relationship(binary(), atom() | {custom, binary()}, binary()) ->
ok | {error, term()}.
create_relationship(Subject, Predicate, Object) ->
create_relationship(Subject, Predicate, Object, #{}).
-spec create_relationship(binary(), atom() | {custom, binary()}, binary(), map()) ->
ok | {error, term()}.
create_relationship(Subject, Predicate, Object, Metadata)
when is_binary(Subject), is_binary(Object), is_map(Metadata) ->
Key = {Subject, Predicate, Object},
Entry = #rel_entry{
key = Key,
subject = Subject,
predicate = Predicate,
object = Object,
metadata = Metadata,
created_at = erlang:system_time(millisecond)
},
%% Insert into both forward and reverse indexes
true = ets:insert(?REL_FORWARD_TABLE, Entry),
ReverseKey = {Object, Predicate, Subject},
true = ets:insert(?REL_REVERSE_TABLE, Entry#rel_entry{key = ReverseKey}),
ok.
-spec delete_relationship(binary(), atom() | {custom, binary()}, binary()) ->
ok | {error, term()}.
delete_relationship(Subject, Predicate, Object)
when is_binary(Subject), is_binary(Object) ->
ForwardKey = {Subject, Predicate, Object},
ReverseKey = {Object, Predicate, Subject},
true = ets:delete(?REL_FORWARD_TABLE, ForwardKey),
true = ets:delete(?REL_REVERSE_TABLE, ReverseKey),
ok.
-spec related_to(binary(), atom() | {custom, binary()}) -> [binary()].
related_to(Subject, Predicate) when is_binary(Subject) ->
Pattern = #rel_entry{key = {Subject, Predicate, '_'}, object = '$1', _ = '_'},
[Object || [Object] <- ets:match(?REL_FORWARD_TABLE, Pattern)].
-spec related_from(binary(), atom() | {custom, binary()}) -> [binary()].
related_from(Object, Predicate) when is_binary(Object) ->
%% Use reverse index: find all subjects that relate to this object
Pattern = #rel_entry{key = {Object, Predicate, '_'}, subject = '$1', _ = '_'},
[Subject || [Subject] <- ets:match(?REL_REVERSE_TABLE, Pattern)].
-spec all_related(binary()) -> [{atom() | {custom, binary()}, binary()}].
all_related(Subject) when is_binary(Subject) ->
ets:foldl(
fun(#rel_entry{subject = S, predicate = P, object = O}, Acc) when S =:= Subject ->
[{P, O} | Acc];
(_, Acc) ->
Acc
end,
[],
?REL_FORWARD_TABLE
).
-spec traverse_transitive(binary(), atom() | {custom, binary()}, forward | reverse) ->
[binary()].
traverse_transitive(Start, Predicate, Direction) when is_binary(Start) ->
traverse_transitive(Start, Predicate, Direction, sets:new(), []).
traverse_transitive(Current, Predicate, Direction, Visited, Acc) ->
case sets:is_element(Current, Visited) of
true ->
Acc;
false ->
NewVisited = sets:add_element(Current, Visited),
Neighbors = case Direction of
forward -> related_to(Current, Predicate);
reverse -> related_from(Current, Predicate)
end,
NewAcc = Neighbors ++ Acc,
lists:foldl(
fun(Next, AccIn) ->
traverse_transitive(Next, Predicate, Direction, NewVisited, AccIn)
end,
NewAcc,
Neighbors
)
end.
-spec get_relationship(binary(), atom() | {custom, binary()}, binary()) ->
{ok, map()} | {error, not_found | term()}.
get_relationship(Subject, Predicate, Object)
when is_binary(Subject), is_binary(Object) ->
Key = {Subject, Predicate, Object},
case ets:lookup(?REL_FORWARD_TABLE, Key) of
[#rel_entry{subject = S, predicate = P, object = O, metadata = M, created_at = Ts}] ->
{ok, #{subject => S, predicate => P, object => O, metadata => M, created_at => Ts}};
[] ->
{error, not_found}
end.
%% Taxonomy helpers
-spec instances_of(binary()) -> [binary()].
instances_of(Class) when is_binary(Class) ->
related_from(Class, instance_of).
-spec instances_of_transitive(binary()) -> [binary()].
instances_of_transitive(Class) when is_binary(Class) ->
%% Get direct instances + instances of all subclasses
DirectInstances = instances_of(Class),
SubclassInstances = lists:flatmap(
fun(Subclass) -> instances_of_transitive(Subclass) end,
subclasses(Class)
),
lists:usort(DirectInstances ++ SubclassInstances).
-spec classes_of(binary()) -> [binary()].
classes_of(Instance) when is_binary(Instance) ->
related_to(Instance, instance_of).
-spec subclasses(binary()) -> [binary()].
subclasses(Class) when is_binary(Class) ->
related_from(Class, subclass_of).
-spec superclasses(binary()) -> [binary()].
superclasses(Class) when is_binary(Class) ->
related_to(Class, subclass_of).
%%===================================================================
%% gen_server callbacks
%%===================================================================
init(_Opts) ->
%% Create ETS tables
ets:new(?MRI_TABLE, [
named_table, set, public,
{keypos, #mri_entry.mri},
{read_concurrency, true},
{write_concurrency, true}
]),
ets:new(?REL_FORWARD_TABLE, [
named_table, set, public,
{keypos, #rel_entry.key},
{read_concurrency, true},
{write_concurrency, true}
]),
ets:new(?REL_REVERSE_TABLE, [
named_table, set, public,
{keypos, #rel_entry.key},
{read_concurrency, true},
{write_concurrency, true}
]),
ets:new(?TYPE_INDEX_TABLE, [
named_table, set, public,
{read_concurrency, true},
{write_concurrency, true}
]),
ets:new(?REALM_INDEX_TABLE, [
named_table, set, public,
{read_concurrency, true},
{write_concurrency, true}
]),
{ok, #state{}}.
handle_call(clear, _From, State) ->
ets:delete_all_objects(?MRI_TABLE),
ets:delete_all_objects(?REL_FORWARD_TABLE),
ets:delete_all_objects(?REL_REVERSE_TABLE),
ets:delete_all_objects(?TYPE_INDEX_TABLE),
ets:delete_all_objects(?REALM_INDEX_TABLE),
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
%% Tables are owned by this process and will be deleted automatically
ok.