import gleam/erlang import gleam/erlang/atom import gleam/result import lamb/query.{type Query} // ------ Table API ------ // // TODO: Make tables of different types. Set, OrderedSet, Bag, Duplicate Bag. // TODO: Think about enabling the heir option. // TODO: Think about enabling th give away option (which is independent from heir). pub opaque type Table(index, record) { TableSet(reference: TableId) TableBag(reference: TableId) } type TableId = erlang.Reference type Name = atom.Atom pub type Store { Set Bag } // TODO: Private and Protected should probably carry a subject. // Or we should validate the owner and access before making a query. pub type Access { Public Protected Private } pub type Error(index) { NotSupported(Store) NotFound(index) AlreadyExists(Access, Name) } pub fn create_table( store: Store, access: Access, name: String, ) -> Result(Table(index, record), Error(index)) { // https://www.erlang.org/doc/apps/stdlib/ets.html#new/2 let name = atom.create_from_string(name) case access { Private -> { // If its a private table don't pay attention to naming let table_id = ffi_create_table(store, access, name) table_id |> build_table(store) |> Ok } Protected | Public -> { // If its a public table first check if table exists case ffi_table_id(name) { // If already exists, do not create Ok(_table_id) -> { Error(AlreadyExists(access, name)) } // If it does not exist, create Error(Nil) -> { let name = ffi_create_table(store, access, name) let assert Ok(table_id) = ffi_table_id(name) table_id |> build_table(store) |> Ok } } } } } pub fn delete_table(table: Table(index, record)) -> Nil { case is_alive(table) { True -> { table |> get_table_id() |> ffi_delete_table() Nil } False -> { Nil } } } pub fn from_name(name: String) -> Result(TableId, Nil) { case atom.from_string(name) { Ok(atom) -> ffi_table_id(atom) Error(_error) -> Error(Nil) } } pub fn is_alive(table: Table(index, record)) -> Bool { let table_id = get_table_id(table) case ffi_table_id(table_id) { Ok(_table_id) -> True Error(_error) -> False } } // ------ Record API ------ // // TODO: When inserting, updating, deleting, querying. Check the table type and // who is making the request, who the process owner is to validate that the // query can be made. // // Check if ets already gives us a useful type to work with. // // TODO: We can make all these operations work o lists. However the behaviour will // need to be modified to guarantee insertion. // // > If the list contains more than one object with matching keys and the table // > type is set, one is inserted, which one is not defined. The same holds for // > table type ordered_set if the keys compare equal. pub fn insert(table: Table(index, record), rows: List(#(index, record))) -> Nil { let table_id = get_table_id(table) let assert True = ffi_insert(table_id, rows) Nil } // TODO: When is an update really needed? pub fn update( table: Table(index, record), where query: Query(index, record), ) -> Result(Int, Error(index)) { // TODO: Consider these scenarios for bat tables: // // https://www.erlang.org/doc/apps/stdlib/ets.html#update_element/4 // // >The function fails with reason badarg in the following situations: // - The table type is not set or ordered_set. // - The element to update is also the key. // // https://www.erlang.org/doc/apps/stdlib/ets.html#select_replace/2 // // > For the moment, due to performance and semantic constraints, tables of type bag // are not yet supported. case table { TableSet(table_id) -> Ok(ffi_update(table_id, [query])) TableBag(_table_id) -> { // TODO: An option here is to do a select, then map through the results. Error(NotSupported(Bag)) } } } pub fn delete( table: Table(index, record), where query: Query(index, record), ) -> Int { let table_id = get_table_id(table) ffi_delete(table_id, [query |> query.map(True)]) } // ------ Query API ------ // // TODO: For tables of type bag, instead of doing a different module we could // just do a runtime check before ther request. I think this could make // the API way more ergonomic. pub fn get( table: Table(index, record), index: index, ) -> Result(record, Error(index)) { case table { TableSet(table_id) -> { ffi_get(table_id, index) |> result.replace_error(NotFound(index)) } TableBag(_table_id) -> { // TODO: Could default to limit the result to the 1st record and log a warning. Error(NotSupported(Bag)) } } } pub fn all( from table: Table(index, record), where query: Query(index, record), ) -> List(x) { let table_id = get_table_id(table) ffi_search(table_id, [query]) } pub type Partial(record) { Records(List(record), Step) End(List(record)) } pub fn batch( from table: Table(index, record), by limit: Int, where query: Query(index, record), ) -> Partial(x) { let table_id = get_table_id(table) ffi_search_partial(table_id, limit, [query]) } pub fn continue(step: Step) -> Partial(x) { ffi_search_partial_continue(step) } pub fn count( from table: Table(index, record), where query: Query(index, record), ) -> Int { let table_id = get_table_id(table) ffi_count(table_id, [query |> query.map(True)]) } // ------ Helpers ------ // fn build_table(table_id: TableId, store: Store) -> Table(index, record) { case store { Set -> TableSet(table_id) Bag -> TableBag(table_id) } } fn get_table_id(table: Table(index, record)) -> TableId { case table { TableSet(table_id) -> table_id TableBag(table_id) -> table_id } } // ------ FFI Helpers ------ // pub type Step @external(erlang, "lamb_erlang_ffi", "table_id") fn ffi_table_id(table: name_or_ref) -> Result(TableId, Nil) @external(erlang, "lamb_erlang_ffi", "create_table") fn ffi_create_table(store: Store, access: Access, name: Name) -> name_or_ref // Will run into exception if table does not exist. @external(erlang, "ets", "delete") fn ffi_delete_table(table: TableId) -> TableId @external(erlang, "ets", "insert") fn ffi_insert(table: TableId, rows: List(#(index, record))) -> Bool @external(erlang, "ets", "select_replace") fn ffi_update(table: TableId, queries: List(Query(index, record))) -> Int @external(erlang, "ets", "select_delete") fn ffi_delete(table: TableId, queries: List(Query(index, record))) -> Int @external(erlang, "lamb_erlang_ffi", "get") fn ffi_get(table: TableId, index: index) -> Result(record, Nil) @external(erlang, "ets", "select") fn ffi_search(table: TableId, queries: List(Query(index, record))) -> List(x) @external(erlang, "lamb_erlang_ffi", "search") fn ffi_search_partial( table: TableId, limit: Int, queries: List(Query(index, record)), ) -> Partial(x) @external(erlang, "lamb_erlang_ffi", "search") fn ffi_search_partial_continue(step: Step) -> Partial(x) @external(erlang, "ets", "select_count") fn ffi_count(table: TableId, queries: List(Query(index, record))) -> Int