-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, delete/2, find/2, drop_table/1, select/2, migrate/2, field/3]). -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(EKR) :: any() | {gleam_phantom, EKR}. -opaque transaction(EKS) :: {transaction, table_ref({binary(), EKS}), gleam@dynamic@decode:decoder(EKS)}. -opaque table(EKT) :: {table, gleam@erlang@atom:atom_(), list(table_attributes()), gleam@erlang@charlist:charlist(), gleam@dynamic@decode:decoder(EKT)}. -type file_error() :: unable_to_open | unable_to_close. -type select_option(EKU) :: skip | {continue, EKU} | {done, EKU}. -type migrate_options(EKV) :: {update, EKV} | keep | delete. -file("src\\database.gleam", 116). ?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(EMG)) -> {ok, table(EMG)} | {error, file_error()}. create_table(Name, Decoder) -> Name_atom = erlang:binary_to_atom(Name), Path = unicode:characters_to_list(<>), 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", 155). ?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(EML), fun((transaction(EML)) -> EMO)) -> {ok, EMO} | {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" " 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) -> String {\n" " let pet = Pet(name, animal)\n" " use ref <- database.transaction(table)\n" " database.insert(ref, pet) \n" " }\n" " ```\n" ). -spec insert(transaction(EMR), EMR) -> {ok, binary()} | {error, any()}. 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, Reason} -> {error, Reason}; _ -> {ok, Id} end. -file("src\\database.gleam", 206). ?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, any()}. delete(Transac, Id) -> case dets:delete(erlang:element(2, Transac), Id) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 228). ?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(EMX), binary()) -> gleam@option:option(EMX). 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", 255). ?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). ?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(END), fun(({binary(), END}) -> select_option(ENF))) -> list(ENF). 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", 353). ?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(ENI), fun((gleam@dynamic:dynamic_()) -> migrate_options(ENI)) ) -> {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", 389). ?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(ENN), fun((ENN) -> gleam@dynamic@decode:decoder(ENP)) ) -> gleam@dynamic@decode:decoder(ENP). field(Field_index, Field_decoder, Next) -> gleam@dynamic@decode:field(Field_index + 1, Field_decoder, Next).