database
An outrageously simple set of functions to interact with the BEAM ETS (Erlang Term Storage) and DETS (Disk-based ETS) API.
Good for small projects and POCs.
This project DOES NOT intend to serve as direct bindings to the DETS API, but rather to interact with it in a gleamy way:
- with a simple and concise interface;
- type-safely;
- no unexpected crashes, all errors are values.
Types
Possible error that may occur when using the DETS operations.
pub type FileError {
UnableToOpen
UnableToClose
}
Constructors
-
UnableToOpenA problem occurred when trying to open and lock the .dets file.
-
UnableToCloseA problem ocurred when trying to write into the .dets file and close it.
Error returned by the find function.
pub type FindError {
NotFound
UnableToDecode(List(decode.DecodeError))
}
Constructors
-
NotFoundNo data with the given ID was found.
-
UnableToDecode(List(decode.DecodeError))The data was found, but it could not be decoded with the provided decoder.
Operations to perform on a migration.
pub type MigrateOptions(a) {
Update(a)
Keep
Delete
}
Constructors
-
Update(a)Replaces the previous value with the new one.
-
KeepMaintains the value as it currently is.
Keep in mind that if the value does not conform to the provided decoder, it will be unaccessible for both
findandselectfunctions. Only remaining stored for future migrations. -
DeleteRemoves the value from the table.
Errors that can happen on a select()
pub type SelectError {
Badarg
}
Constructors
-
BadargThe pattern provided is invalid
A reference to an open table, required to interact with said table.
Obtained through the transaction(Table) function
pub opaque type Transaction(a)
Errors that can happen during a transaction() call
pub type TransactionError(detail) {
FileError(FileError)
Operation(detail)
}
Constructors
-
FileError(FileError)Errors related to the file (exclusive to the DETS tables)
-
Operation(detail)Errors that happened within the operation, and not on the transaction itself
Values
pub fn create_dets_table(
name name: atom.Atom,
decode_with decoder: decode.Decoder(a),
) -> Result(Table(a), FileError)
Creats a DETS table (on disk!).
If no .dets file exists for the provided definition, creates one. Otherwise, just checks whether the file is accessible and not corrupted.
Example
pub fn start_database() {
let table_decoder = {
use name <- database.field(0, decode.string)
use animal <- database.field(1, my_animal_decoder)
decode.success(Pet(name:, animal:))
}
let table_name = atom.create("pets")
database.create_dets_table(name: table_name, decode_with: table_decoder)
// -> Ok(Table(Pet))
}
pub fn create_ets_table(name name: atom.Atom) -> Table(a)
Creats a ETS table (in memory!).
Example
pub fn start_database() {
let table_name = atom.create("pets")
database.create_ets_table(name: table_name)
// -> Table(a)
}
pub fn create_table(
name name: atom.Atom,
decode_with decoder: decode.Decoder(a),
) -> Result(Table(a), FileError)
Deprecated: Use create_dets_table instead
pub fn delete(
transac: Transaction(a),
id: String,
) -> Result(Nil, Nil)
Deletes a value from a table.
Example
pub fn delete_pet(table: Table(Pet) pet_id: String) {
use ref <- database.transaction(table)
database.delete(ref, pet_id)
}
pub fn drop_table(table: Table(a)) -> Result(Nil, Nil)
Deletes the entire table file
Example
pub fn destroy_all_pets(table: Table(Pet), password: String) {
case password {
"Yes, I am evil." -> {
database.drop_table(table)
Ok(Nil)
}
_ -> Error(WrongPassword)
}
}
pub fn enum(values: List(a), zero_value: a) -> decode.Decoder(a)
Enum decoder
Example
let decoder = {
use name <- database.field(0, decode.string)
use animal <- database.field(1, database.enum([Dog, Cat, Parrot], Dog))
decode.success(Pet(name:, animal:))
}
pub fn field(
field_index: Int,
field_decoder: decode.Decoder(t),
next: fn(t) -> decode.Decoder(final),
) -> decode.Decoder(final)
Field decoder
Example
let decoder = {
use name <- database.field(0, decode.string)
use animal <- database.field(1, my_animal_decoder)
decode.success(Pet(name:, animal:))
}
pub fn find(
transac: Transaction(a),
id: String,
) -> Result(a, FindError)
Finds a value by its index
Example
pub fn play_with_pluto(table: Table(Pet)) {
use ref <- database.transaction(table)
let resp = database.find(ref, known_pluto_id)
case resp {
Error(_) -> Error(PlutoNotFoundBlameTheAstronomers)
Ok(pluto) -> Ok(play_with(pluto))
}
}
pub fn insert(
transac: Transaction(a),
value: a,
) -> Result(String, Nil)
Inserts a value into a table and return their generated id.
Example
pub fn new_pet(table: Table(Pet), animal: Animal, name: String) {
let pet = Pet(name, animal)
use ref <- database.transaction(table)
database.insert(ref, pet)
}
pub fn migrate_dets(
transac: Transaction(a),
migration: fn(dynamic.Dynamic) -> MigrateOptions(a),
) -> Result(Nil, Nil)
Migrates an DETS table to a new structure (does nothing to ETS tables, since those only exist in runtime). When the type stored in the table changes, this function allows you to update the table to the new structure.
This function will error if you try to use it on an ETS table.
Example
pub fn migrate_pets(table: Table(Pet)) {
use value <- database.migrate(table)
case decode.run(value, my_pet_decoder()) {
Ok(pet) -> Update(pet)
_ -> Delete
}
}
pub fn select(
transac: Transaction(a),
patterns: tuple,
) -> Result(List(#(String, a)), SelectError)
Searches for somethig on the table.
The native functions used to select values from tables (ets|dets:select|match_object)
rely on Erlang’s extremely loose type system, which goes against both Gleam’s and this projects’s principles.
So, in order to make it functional (and performatic), some compromises had to be done.
Most notably, this function receives an untyped (unused generic) parameter, which is a tuple with the patterns to be matched,
said patterns can be values to be pattern-matched or functions that return said values, as shown below.
For more examples, access the database_test.gleam file
Don’t worry, while the parameters for the result might be a bit loosey, its return is perfectly type safe,
as enforced by the Gleam compiler (on both ETS and DETS modes) and the decode API (only in DETS mode)
Example
pub fn fetch_all_parrots(table: Table(Pet)) {
use ref <- database.transaction(table)
let _ = database.insert(ref, Parrot("Kiwi"))
let _ = database.insert(ref, Cat("Hulu"))
let _ = database.insert(ref, Parrot("Tata"))
let _ = database.insert(ref, Dog("Mina"))
database.select(ref, #(Parrot(_)))
}
IMPORTANT
By default, DETS and ETS tables are not sorted in any deterministic way, so never assume that the last value inserted will be the last one on the table.
pub fn transaction(
table: Table(a),
procedure: fn(Transaction(a)) -> Result(b, c),
) -> Result(b, TransactionError(c))
Allows you to interact with the table.
For DETS tables, it opens and locks the .dets file, then execute your operations. Once the operations are done, it writes the changes into the file, closes and releases it. For ETS tables, it freezes the content of the table, then execute your operations. Once the operations are done, it pushes the possible new changes (from other parts of your code) into it and releases it.
Example
pub fn is_pet_registered(table: Table(Pet), pet_id: String) {
use ref <- database.transaction(table)
case database.find(ref, pet_id) {
Ok(_) -> True
Error(_) -> False
}
}
pub fn update(
transac: Transaction(a),
id: String,
value: a,
) -> Result(String, Nil)
Updates a value in a table.
Example
pub fn rename_pet(table: Table(Pet) id: String, pet: Pet, new_name: String) {
use transac <- database.transaction(table)
let pet = Pet(new_name, pet.animal)
database.update(transac, id, pet)
}
pub fn upsert(
transac: Transaction(a),
id: String,
value: a,
) -> Result(String, Nil)
Inserts a value into a table with a specific id. If the id already exists, the value is overwritten.
Example
pub fn save_user(table: Table(User), user: User) {
use ref <- database.transaction(table)
database.upsert(ref, user.email, user)
}