-module(database). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -define(FILEPATH, "src\\database.gleam"). -export([transaction/2, insert/2, delete/2, find/2, drop_table/1, create_table/2, select/2, field/3]). -export_type([storage/0, table_attributes/0, table_ref/1, transaction/1, table/1, table_error/0, select_option/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(EIC) :: any() | {gleam_phantom, EIC}. -opaque transaction(EID) :: {transaction, table_ref(EID), gleam@dynamic@decode:decoder(EID)}. -opaque table(EIE) :: {table, gleam@erlang@atom:atom_(), list(table_attributes()), gleam@erlang@charlist:charlist(), gleam@dynamic@decode:decoder(EIE)}. -type table_error() :: badarg | unable_to_open | unable_to_close. -type select_option(EIF) :: skip | {continue, EIF} | {done, EIF}. -file("src\\database.gleam", 185). ?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), petname: String) {\n" " use ref <- database.transaction(table)\n" " case database.find(ref, petname) {\n" " Some(_) -> True\n" " None -> False\n" " }\n" " }\n" " ```\n" ). -spec transaction(table(EJY), fun((transaction(EJY)) -> EKB)) -> {ok, EKB} | {error, table_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", 222). ?DOC( " Inserts a value into a table.\n" "\n" " DETS tables do not have support for update, only for upsert.\n" " So if you have to change a value, just insert a new value\n" " with the same index, and it will replace the previous value.\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" " let op = database.transaction(table, fn(ref) {\n" " database.insert(ref, pet)\n" " })\n" " case op {\n" " Ok(_) -> Ok(pet)\n" " Error(reason) -> Error(reason)\n" " }\n" " }\n" " ```\n" ). -spec insert(transaction(EKE), EKE) -> {ok, nil} | {error, any()}. insert(Transac, Value) -> case dets:insert(erlang:element(2, Transac), Value) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 240). ?DOC( " Deletes a value from a table.\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn delete_pet(table: Table(Pet) petname: String) {\n" " use ref <- database.transaction(table)\n" " database.delete(ref, petname)\n" " }\n" " ```\n" ). -spec delete(transaction(any()), any()) -> {ok, nil} | {error, any()}. delete(Transac, Index) -> case dets:delete(erlang:element(2, Transac), Index) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 262). ?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, \"Pluto\")\n" " case resp {\n" " None -> Error(PlutoNotFoundBlameTheAstronomers)\n" " Some(pluto) -> Ok(play_with(pluto))\n" " }\n" " }\n" " ```\n" ). -spec find(transaction(EKL), any()) -> gleam@option:option(EKL). find(Transac, Index) -> case dets:lookup(erlang:element(2, Transac), Index) 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", 289). ?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, any()}. drop_table(Table) -> case file:delete(erlang:element(4, Table)) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 296). -spec is_record(any()) -> boolean(). is_record(Value) -> erlang:is_tuple(Value) andalso erlang:is_atom(erlang:element(1, Value)). -file("src\\database.gleam", 135). ?DOC( " Creats a table.\n" "\n" " ## Important\n" " **THE TABLE IS ENTIRELY DEPENDENT ON THE DEFINITION PROVIDED,\n" " ANY CHANGE TO DEFINITION - EVEN IF EVERYTHING REMAINS OF THE SAME\n" " TYPE - WILL CAUSE ANOTHER TABLE TO BE CREATED.**\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_def = Pet(name: \"Pluto\", animal: Dog)\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(definition: table_def, decode_with: decoder)\n" " // -> Ok(Table(Pet))\n" " }\n" " ```\n" ). -spec create_table(EJT, gleam@dynamic@decode:decoder(EJT)) -> {ok, table(EJT)} | {error, table_error()}. create_table(Definition, Decoder) -> case is_record(Definition) of true -> case erlang:tuple_size(Definition) < 2 of true -> {error, badarg}; false -> Name_atom = erlang:element(1, Definition), Name = erlang:atom_to_binary(Name_atom), Path = unicode:characters_to_list( <> ), Att = [{file, Path}, {type, set}, {keypos, 2}], 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 end; false -> {error, badarg} end. -file("src\\database.gleam", 334). ?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" " 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(EKU), fun((EKU) -> select_option(EKW))) -> list(EKW). select(Transac, Select_fn) -> Continue = erlang:binary_to_atom(<<"continue"/utf8>>), New_fn = fun(Dyn_value) -> case gleam@dynamic@decode:run(Dyn_value, erlang:element(3, Transac)) of {ok, Value} -> case Select_fn(Value) of skip -> Continue; Res -> Res end; _ -> Continue end end, dets:traverse(erlang:element(2, Transac), New_fn). -file("src\\database.gleam", 364). ?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(EKZ), fun((EKZ) -> gleam@dynamic@decode:decoder(ELB)) ) -> gleam@dynamic@decode:decoder(ELB). field(Field_index, Field_decoder, Next) -> gleam@dynamic@decode:field(Field_index + 1, Field_decoder, Next).