Current section
Files
Jump to
Current section
Files
src/database.erl
-module(database).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/database.gleam").
-export([create_dets_table/2, create_table/2, create_ets_table/1, transaction/2, upsert/3, insert/2, update/3, delete/2, find/2, drop_table/1, select/2, migrate_dets/2, field/3, enum/2]).
-export_type([table_attributes/0, continue/0, table_ref/1, transaction/1, table/1, file_error/0, find_error/0, transaction_error/1, select_error/0, migrate_options/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" An outrageously simple set of functions to interact with the BEAM\n"
" ETS (Erlang Term Storage) and DETS (Disk-based ETS) API.\n"
" \n"
" Good for small projects and POCs.\n"
"\n"
"\n"
" This project DOES NOT intend to serve as direct bindings to the \n"
" DETS API, but rather to interact with it in a gleamy way:\n"
" 1. with a simple and concise interface;\n"
" 2. type-safely;\n"
" 3. no unexpected crashes, all errors are values.\n"
).
-type table_attributes() :: {file, gleam@erlang@charlist:charlist()} |
{type, table_attributes()} |
{keypos, integer()} |
public |
set.
-type continue() :: continue.
-type table_ref(DXH) :: any() | {gleam_phantom, DXH}.
-opaque transaction(DXI) :: {disk_transaction,
table_ref({binary(), DXI}),
gleam@dynamic@decode:decoder(DXI)} |
{memory_transaction, table_ref({binary(), DXI})}.
-opaque table(DXJ) :: {disk_table,
gleam@erlang@atom:atom_(),
list(table_attributes()),
gleam@erlang@charlist:charlist(),
gleam@dynamic@decode:decoder(DXJ)} |
{memory_table, table_ref({binary(), DXJ})}.
-type file_error() :: unable_to_open | unable_to_close.
-type find_error() :: not_found |
{unable_to_decode, list(gleam@dynamic@decode:decode_error())}.
-type transaction_error(DXK) :: {file_error, file_error()} | {operation, DXK}.
-type select_error() :: badarg.
-type migrate_options(DXL) :: {update, DXL} | keep | delete.
-file("src/database.gleam", 164).
?DOC(
" Creats a DETS table (on disk!).\n"
"\n"
" If no .dets file exists for the provided definition, creates one.\n"
" Otherwise, just checks whether the file is accessible and not\n"
" corrupted.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn start_database() {\n"
" let table_decoder = {\n"
" use name <- database.field(0, decode.string)\n"
" use animal <- database.field(1, my_animal_decoder)\n"
" decode.success(Pet(name:, animal:))\n"
" }\n"
" let table_name = atom.create(\"pets\")\n"
" database.create_dets_table(name: table_name, decode_with: table_decoder)\n"
" // -> Ok(Table(Pet))\n"
" }\n"
" ```\n"
).
-spec create_dets_table(
gleam@erlang@atom:atom_(),
gleam@dynamic@decode:decoder(EAA)
) -> {ok, table(EAA)} | {error, file_error()}.
create_dets_table(Name, Decoder) ->
Path = unicode:characters_to_list(
<<(erlang:atom_to_binary(Name))/binary, ".dets"/utf8>>
),
Att = [{file, Path}, {type, set}, {keypos, 1}],
case dets:open_file(Name, Att) of
{ok, Tab} ->
case dets:close(Tab) of
{error, _} ->
{error, unable_to_close};
_ ->
{ok, {disk_table, Name, Att, Path, Decoder}}
end;
{error, _} ->
{error, unable_to_open}
end.
-file("src/database.gleam", 184).
-spec create_table(gleam@erlang@atom:atom_(), gleam@dynamic@decode:decoder(EAF)) -> {ok,
table(EAF)} |
{error, file_error()}.
create_table(Name, Decoder) ->
create_dets_table(Name, Decoder).
-file("src/database.gleam", 203).
?DOC(
" Creats a ETS table (in memory!).\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn start_database() {\n"
" let table_name = atom.create(\"pets\")\n"
" database.create_ets_table(name: table_name)\n"
" // -> Table(a)\n"
" }\n"
" ```\n"
).
-spec create_ets_table(gleam@erlang@atom:atom_()) -> table(any()).
create_ets_table(Name) ->
Att = [set, public, {keypos, 1}],
{memory_table, ets:new(Name, Att)}.
-file("src/database.gleam", 237).
?DOC(
" Allows you to interact with the table.\n"
"\n"
" For DETS tables, it opens and locks the .dets file, then execute your operations.\n"
" Once the operations are done, it writes the changes into the file,\n"
" closes and releases it.\n"
" For ETS tables, it freezes the content of the table, then execute your operations.\n"
" Once the operations are done, it pushes the possible new changes (from other parts of your code)\n"
" into it and releases it.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn is_pet_registered(table: Table(Pet), pet_id: String) {\n"
" use ref <- database.transaction(table)\n"
" case database.find(ref, pet_id) {\n"
" Ok(_) -> True\n"
" Error(_) -> False\n"
" }\n"
" }\n"
" ```\n"
).
-spec transaction(
table(EAM),
fun((transaction(EAM)) -> {ok, EAP} | {error, EAQ})
) -> {ok, EAP} | {error, transaction_error(EAQ)}.
transaction(Table, Procedure) ->
case Table of
{disk_table, Tabname, Attributes, _, _} ->
case dets:open_file(Tabname, Attributes) of
{error, _} ->
{error, {file_error, unable_to_open}};
{ok, Ref} ->
Resp = Procedure(
{disk_transaction, Ref, erlang:element(5, Table)}
),
case dets:close(Ref) of
{error, _} ->
{error, {file_error, unable_to_close}};
_ ->
gleam@result:map_error(
Resp,
fun(Field@0) -> {operation, Field@0} end
)
end
end;
{memory_table, Ref@1} ->
ets:safe_fixtable(Ref@1, true),
Resp@1 = Procedure({memory_transaction, Ref@1}),
ets:safe_fixtable(Ref@1, false),
gleam@result:map_error(
Resp@1,
fun(Field@0) -> {operation, Field@0} end
)
end.
-file("src/database.gleam", 289).
?DOC(
" Inserts a value into a table with a specific id. If the id already exists, the value is overwritten.\n"
"\n"
" # Example\n"
" ```gleam\n"
" pub fn save_user(table: Table(User), user: User) {\n"
" use ref <- database.transaction(table)\n"
" database.upsert(ref, user.email, user)\n"
" }\n"
" ```\n"
).
-spec upsert(transaction(EBA), binary(), EBA) -> {ok, binary()} | {error, nil}.
upsert(Transac, Id, Value) ->
case Transac of
{disk_transaction, Ref, _} ->
case dets:insert(Ref, {Id, Value}) of
{error, _} ->
{error, nil};
_ ->
{ok, Id}
end;
{memory_transaction, Ref@1} ->
case ets:insert(Ref@1, {Id, Value}) of
false ->
{error, nil};
true ->
{ok, Id}
end
end.
-file("src/database.gleam", 274).
?DOC(
" Inserts a value into a table and return their generated id.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn new_pet(table: Table(Pet), animal: Animal, name: String) {\n"
" let pet = Pet(name, animal)\n"
" use ref <- database.transaction(table)\n"
" database.insert(ref, pet) \n"
" }\n"
" ```\n"
).
-spec insert(transaction(EAW), EAW) -> {ok, binary()} | {error, nil}.
insert(Transac, Value) ->
Id = begin
_pipe = crypto:strong_rand_bytes(16),
gleam@bit_array:base64_url_encode(_pipe, false)
end,
upsert(Transac, Id, Value).
-file("src/database.gleam", 320).
?DOC(
" Updates a value in a table.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn rename_pet(table: Table(Pet) id: String, pet: Pet, new_name: String) {\n"
" use transac <- database.transaction(table)\n"
" let pet = Pet(new_name, pet.animal)\n"
" database.update(transac, id, pet)\n"
" }\n"
" ```\n"
).
-spec update(transaction(EBE), binary(), EBE) -> {ok, binary()} | {error, nil}.
update(Transac, Id, Value) ->
case Transac of
{disk_transaction, Ref, _} ->
case dets:lookup(Ref, Id) of
[_] ->
upsert(Transac, Id, Value);
_ ->
{error, nil}
end;
{memory_transaction, Ref@1} ->
case ets:lookup(Ref@1, Id) of
[_] ->
upsert(Transac, Id, Value);
_ ->
{error, nil}
end
end.
-file("src/database.gleam", 350).
?DOC(
" Deletes a value from a table.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn delete_pet(table: Table(Pet) pet_id: String) {\n"
" use ref <- database.transaction(table)\n"
" database.delete(ref, pet_id)\n"
" }\n"
" ```\n"
).
-spec delete(transaction(any()), binary()) -> {ok, nil} | {error, nil}.
delete(Transac, Id) ->
case Transac of
{disk_transaction, Ref, _} ->
case dets:delete(Ref, Id) of
{error, _} ->
{error, nil};
_ ->
{ok, nil}
end;
{memory_transaction, Ref@1} ->
case ets:delete(Ref@1, Id) of
false ->
{error, nil};
true ->
{ok, nil}
end
end.
-file("src/database.gleam", 380).
?DOC(
" Finds a value by its index\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn play_with_pluto(table: Table(Pet)) {\n"
" use ref <- database.transaction(table)\n"
" let resp = database.find(ref, known_pluto_id)\n"
" case resp {\n"
" Error(_) -> Error(PlutoNotFoundBlameTheAstronomers)\n"
" Ok(pluto) -> Ok(play_with(pluto))\n"
" }\n"
" }\n"
" ```\n"
).
-spec find(transaction(EBM), binary()) -> {ok, EBM} | {error, find_error()}.
find(Transac, Id) ->
case Transac of
{disk_transaction, Ref, Decoder} ->
case dets:lookup(Ref, Id) of
[{_, Resp}] ->
case gleam@dynamic@decode:run(Resp, Decoder) of
{ok, Val} ->
{ok, Val};
{error, Errors} ->
{error, {unable_to_decode, Errors}}
end;
_ ->
{error, not_found}
end;
{memory_transaction, Ref@1} ->
case ets:lookup(Ref@1, Id) of
[{_, Val@1}] ->
{ok, Val@1};
_ ->
{error, not_found}
end
end.
-file("src/database.gleam", 415).
?DOC(
" Deletes the entire table file\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn destroy_all_pets(table: Table(Pet), password: String) {\n"
" case password {\n"
" \"Yes, I am evil.\" -> {\n"
" database.drop_table(table)\n"
" Ok(Nil)\n"
" }\n"
" _ -> Error(WrongPassword)\n"
" }\n"
" }\n"
" ```\n"
).
-spec drop_table(table(any())) -> {ok, nil} | {error, nil}.
drop_table(Table) ->
case Table of
{disk_table, _, _, Path, _} ->
case file:delete(Path) of
{error, _} ->
{error, nil};
_ ->
{ok, nil}
end;
{memory_table, Ref} ->
case ets:delete(Ref) of
false ->
{error, nil};
true ->
{ok, nil}
end
end.
-file("src/database.gleam", 466).
?DOC(
" Searches for somethig on the table.\n"
"\n"
" The native functions used to select values from tables (`ets|dets:select|match_object`)\n"
" rely on Erlang's extremely loose type system, which goes against both Gleam's and this projects's principles.\n"
" So, in order to make it functional (and performatic), some compromises had to be done.\n"
" Most notably, this function receives an untyped (unused generic) parameter, which is a tuple with the patterns to be matched,\n"
" said patterns can be values to be pattern-matched or functions that return said values, as shown below.\n"
" For more examples, access the `database_test.gleam` file\n"
"\n"
" Don't worry, while the parameters for the result might be a bit loosey, its return is perfectly type safe,\n"
" as enforced by the Gleam compiler (on both ETS and DETS modes) and the `decode` API (only in DETS mode)\n"
"\n"
" # Example\n"
" \n"
" ```gleam\n"
" pub fn fetch_all_parrots(table: Table(Pet)) {\n"
" use ref <- database.transaction(table)\n"
" let _ = database.insert(ref, Parrot(\"Kiwi\"))\n"
" let _ = database.insert(ref, Cat(\"Hulu\"))\n"
" let _ = database.insert(ref, Parrot(\"Tata\"))\n"
" let _ = database.insert(ref, Dog(\"Mina\"))\n"
" database.select(ref, #(Parrot(_)))\n"
" }\n"
" ```\n"
"\n"
" # IMPORTANT\n"
" **By default, DETS and ETS tables are not sorted in any deterministic way, so\n"
" never assume that the last value inserted will be the last\n"
" one on the table.**\n"
).
-spec select(transaction(EBU), any()) -> {ok, list({binary(), EBU})} |
{error, select_error()}.
select(Transac, Patterns) ->
case database_ffi:format_select_pattern(Patterns) of
{error, _} ->
{error, badarg};
{ok, Patterns@1} ->
case Transac of
{memory_transaction, Ref} ->
{ok, ets:select(Ref, Patterns@1)};
{disk_transaction, Ref@1, Decoder} ->
case database_ffi:safe_dets_select(Ref@1, Patterns@1) of
{error, _} ->
{ok, []};
{ok, Dyn_values} ->
{ok,
gleam@list:filter_map(
Dyn_values,
fun(Tpl) ->
{Id, Dyn_value} = Tpl,
case gleam@dynamic@decode:run(
Dyn_value,
Decoder
) of
{ok, Value} ->
{ok, {Id, Value}};
{error, _} ->
{error, nil}
end
end
)}
end
end
end.
-file("src/database.gleam", 530).
?DOC(
" Migrates an DETS table to a new structure (does nothing to ETS tables, since those only exist in runtime).\n"
" When the type stored in the table changes, this function\n"
" allows you to update the table to the new structure.\n"
" \n"
" This function will error if you try to use it on an ETS table.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" pub fn migrate_pets(table: Table(Pet)) {\n"
" use value <- database.migrate(table)\n"
" case decode.run(value, my_pet_decoder()) {\n"
" Ok(pet) -> Update(pet)\n"
" _ -> Delete\n"
" } \n"
" }\n"
" ```\n"
).
-spec migrate_dets(
transaction(ECA),
fun((gleam@dynamic:dynamic_()) -> migrate_options(ECA))
) -> {ok, nil} | {error, nil}.
migrate_dets(Transac, Migration) ->
case Transac of
{memory_transaction, _} ->
{error, nil};
{disk_transaction, Ref, _} ->
Func = fun(Tab_value) ->
{Id, Dyn_value} = Tab_value,
case Migration(Dyn_value) of
{update, New_value} ->
_ = dets:insert(Ref, {Id, New_value}),
continue;
delete ->
_ = dets:delete(Ref, Id),
continue;
keep ->
continue
end
end,
_ = dets:traverse(Ref, Func),
{ok, nil}
end.
-file("src/database.gleam", 569).
?DOC(
" Field decoder\n"
" \n"
" # Example\n"
" \n"
" ```gleam\n"
" let decoder = {\n"
" use name <- database.field(0, decode.string)\n"
" use animal <- database.field(1, my_animal_decoder)\n"
" decode.success(Pet(name:, animal:))\n"
" }\n"
" ```\n"
).
-spec field(
integer(),
gleam@dynamic@decode:decoder(ECF),
fun((ECF) -> gleam@dynamic@decode:decoder(ECH))
) -> gleam@dynamic@decode:decoder(ECH).
field(Field_index, Field_decoder, Next) ->
gleam@dynamic@decode:field(Field_index + 1, Field_decoder, Next).
-file("src/database.gleam", 590).
?DOC(
" Enum decoder\n"
" \n"
" # Example\n"
" \n"
" ```gleam\n"
" let decoder = {\n"
" use name <- database.field(0, decode.string)\n"
" use animal <- database.field(1, database.enum([Dog, Cat, Parrot], Dog))\n"
" decode.success(Pet(name:, animal:))\n"
" }\n"
" ```\n"
).
-spec enum(list(ECK), ECK) -> gleam@dynamic@decode:decoder(ECK).
enum(Values, Zero_value) ->
gleam@dynamic@decode:new_primitive_decoder(
<<"Enum"/utf8>>,
fun(Data) ->
case gleam@list:find(
Values,
fun(_capture) -> database_ffi:compare(_capture, Data) end
) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error, Zero_value}
end
end
).