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, find_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 find_error() :: not_found |
{unable_to_decode, list(gleam@dynamic@decode:decode_error())}.
-type select_option(EII) :: skip | {continue, EII} | {done, EII}.
-type migrate_options(EIJ) :: {update, EIJ} | keep | delete.
-file("src\\database.gleam", 128).
?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 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_table(name: table_name, decode_with: table_decoder)\n"
" // -> Ok(Table(Pet))\n"
" }\n"
" ```\n"
).
-spec create_table(gleam@erlang@atom:atom_(), gleam@dynamic@decode:decoder(EJW)) -> {ok,
table(EJW)} |
{error, file_error()}.
create_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, {table, Name, Att, Path, Decoder}}
end;
{error, _} ->
{error, unable_to_open}
end.
-file("src\\database.gleam", 165).
?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"
" Ok(_) -> True\n"
" Error(_) -> 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", 193).
?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", 213).
?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", 235).
?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", 257).
?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(EKQ), binary()) -> {ok, EKQ} | {error, find_error()}.
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} ->
{ok, Val};
{error, Errors} ->
{error, {unable_to_decode, Errors}}
end;
_ ->
{error, not_found}
end.
-file("src\\database.gleam", 284).
?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", 325).
?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(EKX), fun(({binary(), EKX}) -> select_option(EKZ))) -> list(EKZ).
select(Transac, Select_fn) ->
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 ->
database_ffi:skip();
Res ->
Res
end;
_ ->
database_ffi:skip()
end
end,
dets:traverse(erlang:element(2, Transac), New_fn).
-file("src\\database.gleam", 381).
?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(ELC),
fun((gleam@dynamic:dynamic_()) -> migrate_options(ELC))
) -> {ok, nil} | {error, file_error()}.
migrate(Table, Migration) ->
transaction(
Table,
fun(Transac) ->
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}
),
database_ffi:skip();
delete ->
_ = dets:delete(erlang:element(2, Transac), Id),
database_ffi:skip();
keep ->
database_ffi:skip()
end
end,
_ = dets:traverse(erlang:element(2, Transac), Func),
nil
end
).
-file("src\\database.gleam", 416).
?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(ELH),
fun((ELH) -> gleam@dynamic@decode:decoder(ELJ))
) -> gleam@dynamic@decode:decoder(ELJ).
field(Field_index, Field_decoder, Next) ->
gleam@dynamic@decode:field(Field_index + 1, Field_decoder, Next).
-file("src\\database.gleam", 437).
?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(ELM), ELM) -> gleam@dynamic@decode:decoder(ELM).
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
).