Current section
Files
Jump to
Current section
Files
src/shelf_ffi.erl
-module(shelf_ffi).
-export([
open_set/2, open_bag/2, open_duplicate_bag/2,
close/2,
insert/3, insert_list/3, insert_new/3,
lookup_set/2, lookup_bag/2, member/2,
delete_key/2, delete_object/3, delete_all/1,
to_list/1, fold/3, size/1,
save/2, load/2, sync_dets/1,
update_counter/3
]).
%% ── Open ────────────────────────────────────────────────────────────────
%% Creates an ETS table + opens a DETS file + loads DETS contents into ETS.
%% Returns {ok, {EtsRef, DetsRef}} or {error, Reason}.
open_set(Name, Path) ->
do_open(Name, Path, set).
open_bag(Name, Path) ->
do_open(Name, Path, bag).
open_duplicate_bag(Name, Path) ->
do_open(Name, Path, duplicate_bag).
do_open(Name, Path, Type) ->
EtsName = binary_to_atom(Name, utf8),
%% Use the path as the DETS table name (atom) to avoid collisions
DetsName = binary_to_atom(Path, utf8),
try
%% Open or create the DETS file
{ok, Dets} = dets:open_file(DetsName, [
{file, binary_to_list(Path)},
{type, Type},
{repair, true}
]),
%% Create the ETS table
Ets = ets:new(EtsName, [Type, public, named_table, {keypos, 1}]),
%% Load existing DETS data into ETS
true = ets:from_dets(Ets, Dets),
{ok, {Ets, Dets}}
catch
_:badarg ->
%% ETS table with this name likely already exists
case ets:whereis(EtsName) of
undefined ->
{error, {erlang_error, <<"Failed to create table">>}};
_ ->
{error, name_conflict}
end;
_:{badmatch, {error, Reason}} ->
{error, translate_error(Reason)};
_:Reason ->
{error, translate_error(Reason)}
end.
%% ── Close ───────────────────────────────────────────────────────────────
%% Save ETS→DETS, close DETS, delete ETS.
close(Ets, Dets) ->
try
%% Final save
_ = ets:to_dets(Ets, Dets),
%% Close DETS file
ok = dets:close(Dets),
%% Delete ETS table
true = ets:delete(Ets),
{ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Insert ──────────────────────────────────────────────────────────────
insert(Ets, _Dets, Object) ->
try ets:insert(Ets, Object) of
true -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
insert_list(Ets, _Dets, Objects) ->
try ets:insert(Ets, Objects) of
true -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
insert_new(Ets, _Dets, Object) ->
try ets:insert_new(Ets, Object) of
true -> {ok, nil};
false -> {error, key_already_present}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Lookup ──────────────────────────────────────────────────────────────
%% Always reads from ETS (fast path).
lookup_set(Ets, Key) ->
try ets:lookup(Ets, Key) of
[] -> {error, not_found};
[{_, Value} | _] -> {ok, Value}
catch
_:Reason -> {error, translate_error(Reason)}
end.
lookup_bag(Ets, Key) ->
try ets:lookup(Ets, Key) of
Results when is_list(Results) ->
Values = [V || {_, V} <- Results],
case Values of
[] -> {error, not_found};
_ -> {ok, Values}
end
catch
_:Reason -> {error, translate_error(Reason)}
end.
member(Ets, Key) ->
try {ok, ets:member(Ets, Key)}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Delete ──────────────────────────────────────────────────────────────
delete_key(Ets, Key) ->
try ets:delete(Ets, Key) of
true -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
delete_object(Ets, Key, Value) ->
try ets:delete_object(Ets, {Key, Value}) of
true -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
delete_all(Ets) ->
try ets:delete_all_objects(Ets) of
true -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Query ───────────────────────────────────────────────────────────────
to_list(Ets) ->
try {ok, ets:tab2list(Ets)}
catch
_:Reason -> {error, translate_error(Reason)}
end.
fold(Ets, Fun, Acc0) ->
try ets:foldl(Fun, Acc0, Ets) of
Result -> {ok, Result}
catch
_:Reason -> {error, translate_error(Reason)}
end.
size(Ets) ->
try {ok, ets:info(Ets, size)}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Persistence ─────────────────────────────────────────────────────────
%% Snapshot: replace all DETS contents with current ETS state.
save(Ets, Dets) ->
try ets:to_dets(Ets, Dets) of
Dets -> {ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% Reload: clear ETS, then load from DETS.
load(Ets, Dets) ->
try
true = ets:delete_all_objects(Ets),
true = ets:from_dets(Ets, Dets),
{ok, nil}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% Flush DETS write buffer to OS.
sync_dets(Dets) ->
try dets:sync(Dets) of
ok -> {ok, nil};
{error, Reason} -> {error, translate_error(Reason)}
catch
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Counters ────────────────────────────────────────────────────────────
update_counter(Ets, Key, Increment) ->
try {ok, ets:update_counter(Ets, Key, Increment)}
catch
error:badarg ->
case ets:lookup(Ets, Key) of
[] -> {error, not_found};
_ -> {error, {erlang_error, <<"update_counter failed: value is not an integer">>}}
end;
_:Reason -> {error, translate_error(Reason)}
end.
%% ── Error translation ──────────────────────────────────────────────────
translate_error(not_found) -> not_found;
translate_error(key_already_present) -> key_already_present;
translate_error(name_conflict) -> name_conflict;
translate_error(badarg) -> table_closed;
translate_error({file_error, _, enoent}) -> {file_error, <<"File not found">>};
translate_error({file_error, _, eacces}) -> {file_error, <<"Permission denied">>};
translate_error({file_error, _, Reason}) ->
{file_error, list_to_binary(io_lib:format("~p", [Reason]))};
translate_error({error, Reason}) -> translate_error(Reason);
translate_error(Reason) ->
{erlang_error, list_to_binary(io_lib:format("~p", [Reason]))}.