Skip to content

Commit

Permalink
Update API to be more comprehensive
Browse files Browse the repository at this point in the history
  • Loading branch information
chouzar committed Oct 29, 2024
1 parent ef90019 commit 1b6734e
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 141 deletions.
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,40 @@
A gleam library for operating and querying ETS tables.

```gleam
import lamb
import gleam/list
import lamb.{Set, Private}
import lamb/query.{v, i, a} as q
pub fn main() {
let assert Ok(table) = lamb.create_table(Private, "test_table")
// Create a table and insert 5 records.
let assert Ok(table) = lamb.create_table(Set, Private, "test_table")
lamb.insert(table, "a", 1)
lamb.insert(table, "b", 2)
lamb.insert(table, "c", 3)
lamb.insert(table, "d", 4)
lamb.insert(table, "e", 5)
lamb.insert(table, [
#("a", 1),
#("b", 2),
#("c", 3),
#("d", 4),
#("e", 5),
])
let assert Records([_, _] as a, step) = lamb.partial(table, by: 2)
// Retrieve just 1 record.
let assert Ok(3) = lamb.get(table, "c")
// This query syntax builds a matchspec to make queries on the table.
let query =
q.new()
|> q.bind(#(v(0), v(1)))
// Retrieve all rows but only return the record
let _records = lamb.all(table, query |> q.map(v(1)))
// Retrieve all rows but only return the record
let _records = lamb.all(table, query |> q.map(v(0)))
// Retrieve all records in batches of 2.
let assert Records([_, _] as a, step) = lamb.partial(table, by: 2, where: q.new())
let assert Records([_, _] as b, step) = lamb.continue(step)
let assert End([_] as c) = lamb.continue(step)
lamb.delete_table(table)
}
```

Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "lamb"
version = "0.0.3"
version = "0.1.0"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
Expand Down
110 changes: 73 additions & 37 deletions src/lamb.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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) {
Table(reference: TableId)
}
Expand All @@ -14,9 +17,8 @@ type TableId =
type Name =
atom.Atom

pub type Partial(record) {
Records(List(record), Step)
End(List(record))
pub type Store {
Set
}

// TODO: Private and Protected should probably carry a subject.
Expand All @@ -28,6 +30,7 @@ pub type Access {
}

pub fn create_table(
store: Store,
access: Access,
name: String,
) -> Result(Table(index, record), Nil) {
Expand All @@ -36,14 +39,14 @@ pub fn create_table(

case access {
Private -> {
let table_id = ffi_create_table(access, name)
let table_id = ffi_create_table(store, access, name)
Ok(Table(table_id))
}

Protected | Public -> {
case ffi_table_id(name) {
Ok(_table_id) -> Error(Nil)
Error(Nil) -> ffi_create_table(access, name) |> from_atom()
Error(Nil) -> ffi_create_table(store, access, name) |> from_atom()
}
}
}
Expand Down Expand Up @@ -85,50 +88,84 @@ pub fn is_alive(table: Table(index, record)) -> Bool {

// ------ Record API ------ //

pub fn insert(table: Table(index, record), index: index, record: record) -> Nil {
// 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(table_id) = table
let assert True = ffi_insert(table_id, #(index, record))
let assert True = ffi_insert(table_id, rows)

Nil
}

pub fn delete(table: Table(index, record), index: index) -> Nil {
pub fn update(
table: Table(index, record),
where query: Query(index, record),
) -> Int {
// 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.
let Table(table_id) = table
let assert True = ffi_delete(table_id, index)
ffi_update(table_id, [query])
}

Nil
pub fn delete(
table: Table(index, record),
where query: Query(index, record),
) -> Int {
let Table(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, Nil) {
let Table(table_id) = table
ffi_get(table_id, index)
}

pub fn all(
from table: Table(index, record),
where queries: List(Query(index, record)),
where query: Query(index, record),
) -> List(x) {
let Table(table_id) = table
ffi_search(table_id, [query])
}

case queries {
[] -> ffi_search(table_id, [query.new()])
queries -> ffi_search(table_id, queries)
}
pub type Partial(record) {
Records(List(record), Step)
End(List(record))
}

pub fn batch(
from table: Table(index, record),
by limit: Int,
where queries: List(Query(index, record)),
) -> Partial(record) {
where query: Query(index, record),
) -> Partial(x) {
let Table(table_id) = table

case queries {
[] -> ffi_search_partial(table_id, limit, [query.new()])
queries -> ffi_search_partial(table_id, limit, queries)
}
ffi_search_partial(table_id, limit, [query])
}

pub fn continue(step: Step) -> Partial(x) {
Expand All @@ -137,14 +174,10 @@ pub fn continue(step: Step) -> Partial(x) {

pub fn count(
from table: Table(index, record),
where queries: List(Query(index, record)),
where query: Query(index, record),
) -> Int {
let Table(table_id) = table

case queries {
[] -> ffi_count(table_id, [query.new()])
queries -> ffi_count(table_id, queries)
}
ffi_count(table_id, [query |> query.map(True)])
}

// ------ FFI Helpers ------ //
Expand All @@ -155,32 +188,35 @@ pub type Step
fn ffi_table_id(table: name_or_ref) -> Result(TableId, Nil)

@external(erlang, "lamb_erlang_ffi", "create_table")
fn ffi_create_table(access: Access, name: Name) -> name_or_ref
fn ffi_create_table(store: Store, access: Access, name: Name) -> name_or_ref

@external(erlang, "ets", "delete")
fn ffi_delete_table(table: TableId) -> TableId

@external(erlang, "ets", "insert")
fn ffi_insert(table: TableId, record: record) -> Bool
fn ffi_insert(table: TableId, rows: List(#(index, record))) -> Bool

@external(erlang, "ets", "delete")
fn ffi_delete(table: TableId, index: index) -> 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, "lamb_erlang_ffi", "search")
fn ffi_search(table: TableId, query: List(Query(index, record))) -> List(x)
@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,
query: List(Query(index, record)),
queries: List(Query(index, record)),
) -> Partial(x)

@external(erlang, "lamb_erlang_ffi", "search")
fn ffi_search_partial_continue(step: Step) -> Partial(x)

@external(erlang, "lamb_erlang_ffi", "count")
fn ffi_count(table: TableId, query: List(Query(index, record))) -> Int
@external(erlang, "ets", "select_count")
fn ffi_count(table: TableId, queries: List(Query(index, record))) -> Int
31 changes: 29 additions & 2 deletions src/lamb/query.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import gleam/erlang/atom.{type Atom}
import gleam/erlang/charlist.{type Charlist}
import gleam/int
import gleam/list
import gleam/string

pub type Query(index, record)

Expand Down Expand Up @@ -41,11 +40,36 @@ pub fn v(at position: Int) -> Atom {
}
}

// TODO: Can these be made Head or Body specific?
pub fn a(name: String) -> Atom {
let name = string.lowercase(name)
atom.create_from_string(name)
}

pub fn t1(a: a) {
#(#(a))
}

pub fn t2(a: a, b: b) {
#(#(a, b))
}

pub fn t3(a: a, b: b, c: c) {
#(#(a, b, c))
}

pub fn t4(a: a, b: b, c: c, d: d) {
#(#(a, b, c, d))
}

pub fn t5(a: a, b: b, c: c, d: d, e: e) {
#(#(a, b, c, d, e))
}

pub fn r(name: String, tuple: tuple) -> x {
let tag = a(name)
ffi_body_record(tag, tuple)
}

pub fn validate(
query: Query(index, record),
) -> Result(Query(index, record), List(String)) {
Expand All @@ -72,3 +96,6 @@ type Test(body) {

@external(erlang, "lamb_query_erlang_ffi", "test_query")
fn ffi_test_query(query: Query(index, record), row: row) -> Test(body)

@external(erlang, "lamb_query_erlang_ffi", "body_record")
fn ffi_body_record(name: Atom, body: body) -> x
21 changes: 5 additions & 16 deletions src/lamb_erlang_ffi.erl
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
-module(lamb_erlang_ffi).

-export([create_table/2, table_id/1, get/2, search/1, search/2, search/3, count/2]).

create_table(Access, Name) when is_atom(Access), is_atom(Name) ->
DefaultOptions = [set],
-export([create_table/3, table_id/1, get/2, search/1, search/3]).

create_table(Store, Access, Name) when is_atom(Access), is_atom(Name) ->
Options =
case Access of
private ->
[private | DefaultOptions];
[Store, private];
protected ->
[protected, named_table | DefaultOptions];
[Store, protected, named_table];
public ->
[public, named_table | DefaultOptions]
[Store, public, named_table]
end,

ets:new(Name, Options).
Expand All @@ -37,9 +35,6 @@ search(Step) ->
Result = ets:select(Step),
partial_handle(Result).

search(Step, MatchExpression) ->
ets:select(Step, MatchExpression).

search(TableId, Limit, MatchExpression) ->
Result = ets:select(TableId, MatchExpression, Limit),
partial_handle(Result).
Expand All @@ -53,9 +48,3 @@ partial_handle(Result) ->
'$end_of_table' ->
{'end', []}
end.

count(TableId, MatchExpression) ->
NewMatchExpression =
lists:map(fun({Head, Conditions, _Body}) -> {Head, Conditions, [true]} end,
MatchExpression),
ets:select_count(TableId, NewMatchExpression).
18 changes: 15 additions & 3 deletions test/artifacts/record.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,24 @@ pub fn random(id: Int) -> Record {

pub fn generate(enum: Int, id: Int) -> Record {
case enum {
0 -> User(id: id, name: gen_name(), age: gen_age(), bio: gen_bio())
1 -> Client(id: id, x: gen_location(), y: gen_location())
_ -> Admin(id: id)
0 -> generate_user(id)
1 -> generate_client(id)
_ -> generate_admin(id)
}
}

pub fn generate_user(id: Int) -> Record {
User(id: id, name: gen_name(), age: gen_age(), bio: gen_bio())
}

pub fn generate_client(id: Int) -> Record {
Client(id: id, x: gen_location(), y: gen_location())
}

pub fn generate_admin(id: Int) -> Record {
Admin(id: id)
}

fn gen_name() -> String {
let assert [name, ..] = list.shuffle(["Raúl", "Carlos", "César", "Adrián"])
name
Expand Down
Loading

0 comments on commit 1b6734e

Please sign in to comment.