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]).
-define(FILEPATH, "src\\database.gleam").
-export([create_table/2, transaction/2, insert/2, update/3, delete/2, find/2, drop_table/1, select/2, migrate/2, field/3, enum/2]).
-export_type([storage/0, table_attributes/0, table_ref/1, transaction/1, table/1, file_error/0, select_option/1, 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"
" DETS (Disk-based Erlang Term Storage) 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 storage() :: set.
-type table_attributes() :: {file, gleam@erlang@charlist:charlist()} |
{type, storage()} |
{keypos, integer()}.
-type table_ref(EIF) :: any() | {gleam_phantom, EIF}.
-opaque transaction(EIG) :: {transaction,
table_ref({binary(), EIG}),
gleam@dynamic@decode:decoder(EIG)}.
-opaque table(EIH) :: {table,
gleam@erlang@atom:atom_(),
list(table_attributes()),
gleam@erlang@charlist:charlist(),
gleam@dynamic@decode:decoder(EIH)}.
-type file_error() :: unable_to_open | unable_to_close.
-type select_option(EII) :: skip | {continue, EII} | {done, EII}.
-type migrate_options(EIJ) :: {update, EIJ} | keep | delete.
-file("src\\database.gleam", 120).
?DOC(
" Creats a table.\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 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"
" database.create_table(name: \"pets\", decode_with: decoder)\n"
" // -> Ok(Table(Pet))\n"
" }\n"
" ```\n"
).
-spec create_table(binary(), gleam@dynamic@decode:decoder(EJW)) -> {ok,
table(EJW)} |
{error, file_error()}.
create_table(Name, Decoder) ->
Name_atom = erlang:binary_to_atom(Name),
Path = unicode:characters_to_list(<<Name/binary, ".dets"/utf8>>),
Att = [{file, Path}, {type, set}, {keypos, 1}],
case dets:open_file(Name_atom, Att) of
{ok, Tab} ->
case dets:close(Tab) of
{error, _} ->
{error, unable_to_close};
_ ->
{ok, {table, Name_atom, Att, Path, Decoder}}
end;
{error, _} ->
{error, unable_to_open}
end.
-file("src\\database.gleam", 159).
?DOC(
" Allows you to interact with the table.\n"
"\n"
" 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"
"\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"
" Some(_) -> True\n"
" None -> False\n"
" }\n"
" }\n"
" ```\n"
).
-spec transaction(table(EKB), fun((transaction(EKB)) -> EKE)) -> {ok, EKE} |
{error, file_error()}.
transaction(Table, Procedure) ->
case dets:open_file(erlang:element(2, Table), erlang:element(3, Table)) of
{error, _} ->
{error, unable_to_open};
{ok, Ref} ->
Resp = Procedure({transaction, Ref, erlang:element(5, Table)}),
case dets:close(Ref) of
{error, _} ->
{error, unable_to_close};
_ ->
{ok, Resp}
end
end.
-file("src\\database.gleam", 187).
?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) -> String {\n"
" let pet = Pet(name, animal)\n"
" use ref <- database.transaction(table)\n"
" database.insert(ref, pet) \n"
" }\n"
" ```\n"
).
-spec insert(transaction(EKH), EKH) -> {ok, binary()} | {error, nil}.
insert(Transac, Value) ->
Id = begin
_pipe = crypto:strong_rand_bytes(16),
gleam@bit_array:base64_url_encode(_pipe, false)
end,
case dets:insert(erlang:element(2, Transac), {Id, Value}) of
{error, _} ->
{error, nil};
_ ->
{ok, Id}
end.
-file("src\\database.gleam", 207).
?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(EKK), binary(), EKK) -> {ok, binary()} | {error, nil}.
update(Transac, Id, Value) ->
case dets:lookup(erlang:element(2, Transac), Id) of
[_] ->
case dets:insert(erlang:element(2, Transac), {Id, Value}) of
{error, _} ->
{error, nil};
_ ->
{ok, Id}
end;
_ ->
{error, nil}
end.
-file("src\\database.gleam", 229).
?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 dets:delete(erlang:element(2, Transac), Id) of
{error, _} ->
{error, nil};
_ ->
{ok, nil}
end.
-file("src\\database.gleam", 251).
?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"
" None -> Error(PlutoNotFoundBlameTheAstronomers)\n"
" Some(pluto) -> Ok(play_with(pluto))\n"
" }\n"
" }\n"
" ```\n"
).
-spec find(transaction(EKQ), binary()) -> gleam@option:option(EKQ).
find(Transac, Id) ->
case dets:lookup(erlang:element(2, Transac), Id) of
[{_, Resp}] ->
case gleam@dynamic@decode:run(Resp, erlang:element(3, Transac)) of
{ok, Val} ->
{some, Val};
_ ->
none
end;
_ ->
none
end.
-file("src\\database.gleam", 278).
?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 file:delete(erlang:element(4, Table)) of
{error, _} ->
{error, nil};
_ ->
{ok, nil}
end.
-file("src\\database.gleam", 319).
?DOC(
" Searches for somethig on the table.\n"
"\n"
" # Example\n"
" \n"
" ```gleam\n"
" pub fn fetch_all_parrots(table: Table(Pet)) {\n"
" use ref <- database.transaction(table)\n"
" use value <- database.select(ref)\n"
" case value {\n"
" #(_id, Pet(_name, Parrot)) -> Continue(value)\n"
" _ -> Skip\n"
" }\n"
" }\n"
" ```\n"
"\n"
" # IMPORTANT\n"
" **DETS 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(EKW), fun(({binary(), EKW}) -> select_option(EKY))) -> list(EKY).
select(Transac, Select_fn) ->
Continue = erlang:binary_to_atom(<<"continue"/utf8>>),
New_fn = fun(Tab_value) ->
{Id, Dyn_value} = Tab_value,
case gleam@dynamic@decode:run(Dyn_value, erlang:element(3, Transac)) of
{ok, Value} ->
case Select_fn({Id, Value}) of
skip ->
Continue;
Res ->
Res
end;
_ ->
Continue
end
end,
dets:traverse(erlang:element(2, Transac), New_fn).
-file("src\\database.gleam", 376).
?DOC(
" Migrates the table to a new structure.\n"
" When the type stored in the table changes, this function\n"
" allows you to update the table to the new structure.\n"
"\n"
" # IMPORTANT\n"
" **To avoid conflicts, this function will start its own\n"
" transaction, so you should not use it inside another.**\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(
table(ELB),
fun((gleam@dynamic:dynamic_()) -> migrate_options(ELB))
) -> {ok, nil} | {error, file_error()}.
migrate(Table, Migration) ->
transaction(
Table,
fun(Transac) ->
Continue = erlang:binary_to_atom(<<"continue"/utf8>>),
Func = fun(Tab_value) ->
{Id, Dyn_value} = Tab_value,
case Migration(Dyn_value) of
{update, New_value} ->
_ = dets:insert(
erlang:element(2, Transac),
{Id, New_value}
),
Continue;
delete ->
_ = dets:delete(erlang:element(2, Transac), Id),
Continue;
keep ->
Continue
end
end,
_ = dets:traverse(erlang:element(2, Transac), Func),
nil
end
).
-file("src\\database.gleam", 412).
?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(ELG),
fun((ELG) -> gleam@dynamic@decode:decoder(ELI))
) -> gleam@dynamic@decode:decoder(ELI).
field(Field_index, Field_decoder, Next) ->
gleam@dynamic@decode:field(Field_index + 1, Field_decoder, Next).
-file("src\\database.gleam", 433).
?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(ELL), ELL) -> gleam@dynamic@decode:decoder(ELL).
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
).