-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 ).