Packages

An embedded, crash-safe key-value store for Gleam, inspired by CubDB

Current section

Files

Jump to
trove src trove.erl
Raw

src/trove.erl

-module(trove).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/trove.gleam").
-export([list_keyspaces/1, put_in/4, get_in/3, delete_in/3, has_key_in/3, size_in/2, put_and_delete_multi_in/4, put_multi_in/3, delete_multi_in/3, close/1, compact/2, size/1, is_empty/1, dirt_factor/1, file_sync/1, get/2, put/3, delete/2, has_key/2, put_multi/2, delete_multi/2, put_and_delete_multi/3, transaction/3, tx_get/2, tx_put/3, tx_delete/2, tx_has_key/2, tx_get_in/3, tx_put_in/4, tx_delete_in/3, tx_has_key_in/3, with_snapshot/2, snapshot_get/2, snapshot_range/4, range/4, snapshot_get_in/3, snapshot_range_in/5, range_in/5, set_auto_compact/2, open/1, keyspace/5]).
-export_type([open_error/0, auto_compact/0, file_sync/0, config/2, db/2, keyspace/2, transaction_result/3]).
-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 embedded, crash-safe key-value store for Gleam.\n"
"\n"
" trove stores data in an append-only B+ tree on disk. Every write appends\n"
" new nodes and creates a new root. Old data is never overwritten, which\n"
" gives you crash safety, zero-cost MVCC snapshots, and single-writer /\n"
" multiple-reader concurrency backed by an OTP actor.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import gleam/string\n"
" import trove\n"
" import trove/codec\n"
"\n"
" let config = trove.Config(\n"
" path: \"./my_db\",\n"
" key_codec: codec.string(),\n"
" value_codec: codec.string(),\n"
" key_compare: string.compare,\n"
" auto_compact: trove.AutoCompact(min_dirt: 1000, min_dirt_factor: 0.25),\n"
" auto_file_sync: trove.AutoSync,\n"
" call_timeout: 5000,\n"
" )\n"
"\n"
" let assert Ok(db) = trove.open(config)\n"
" trove.put(db, key: \"language\", value: \"gleam\")\n"
" let assert Ok(\"gleam\") = trove.get(db, key: \"language\")\n"
" trove.close(db)\n"
" ```\n"
).
-type open_error() :: {directory_error, binary()} |
{store_error, binary()} |
{lock_error, binary()} |
actor_start_error.
-type auto_compact() :: {auto_compact, integer(), float()} | no_auto_compact.
-type file_sync() :: auto_sync | manual_sync.
-type config(MWM, MWN) :: {config,
binary(),
trove@codec:codec(MWM),
trove@codec:codec(MWN),
fun((MWM, MWM) -> gleam@order:order()),
auto_compact(),
file_sync(),
integer()}.
-opaque db(MWO, MWP) :: {db,
gleam@erlang@process:subject(trove@internal@db:message(MWO, MWP)),
integer()}.
-opaque keyspace(MWQ, MWR) :: {keyspace,
binary(),
trove@codec:codec(MWQ),
trove@codec:codec(MWR),
fun((MWQ, MWQ) -> gleam@order:order())}.
-type transaction_result(MWS, MWT, MWU) :: {commit,
trove@internal@tx:tx(MWS, MWT),
MWU} |
{cancel, MWU}.
-file("src/trove.gleam", 56).
-spec reraise(exception:exception()) -> any().
reraise(Ex) ->
{Class, Reason@3} = case Ex of
{errored, Reason} ->
{<<"error"/utf8>>, Reason};
{thrown, Reason@1} ->
{<<"throw"/utf8>>, Reason@1};
{exited, Reason@2} ->
{<<"exit"/utf8>>, Reason@2}
end,
erlang:raise(erlang:binary_to_atom(Class), Reason@3, []).
-file("src/trove.gleam", 198).
-spec adapt_compare(
trove@codec:codec(MXQ),
fun((MXQ, MXQ) -> gleam@order:order())
) -> fun((bitstring(), bitstring()) -> gleam@order:order()).
adapt_compare(Key_codec, Key_compare) ->
fun(A, B) ->
Decoded_a@1 = case (erlang:element(3, Key_codec))(A) of
{ok, Decoded_a} -> Decoded_a;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"adapt_compare"/utf8>>,
line => 203,
value => _assert_fail,
start => 6796,
'end' => 6842,
pattern_start => 6807,
pattern_end => 6820})
end,
Decoded_b@1 = case (erlang:element(3, Key_codec))(B) of
{ok, Decoded_b} -> Decoded_b;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"adapt_compare"/utf8>>,
line => 204,
value => _assert_fail@1,
start => 6847,
'end' => 6893,
pattern_start => 6858,
pattern_end => 6871})
end,
Key_compare(Decoded_a@1, Decoded_b@1)
end.
-file("src/trove.gleam", 220).
?DOC(
" List the names of every keyspace currently registered on this database.\n"
"\n"
" Returns names in sorted order. Includes every keyspace that has been\n"
" opened with `trove.keyspace(...)` in this session, plus every keyspace\n"
" that was persisted in the store file (even if not yet registered in this\n"
" session). Reading or writing a persisted-but-unregistered keyspace\n"
" without first calling `trove.keyspace(...)` panics.\n"
"\n"
" ```gleam\n"
" let names = trove.list_keyspaces(db)\n"
" ```\n"
).
-spec list_keyspaces(db(any(), any())) -> list(binary()).
list_keyspaces(Db) ->
trove@internal@db:list_keyspaces(
erlang:element(2, Db),
erlang:element(3, Db)
).
-file("src/trove.gleam", 231).
?DOC(
" Insert or update a key-value pair in a named keyspace.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.put_in(db, keyspace: users, key: \"alice\", value: \"admin\")\n"
" ```\n"
).
-spec put_in(db(any(), any()), keyspace(MYB, MYC), MYB, MYC) -> nil.
put_in(Db, Keyspace, Key, Value) ->
trove@internal@db:put_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key),
(erlang:element(2, erlang:element(4, Keyspace)))(Value),
erlang:element(3, Db)
).
-file("src/trove.gleam", 255).
?DOC(
" Look up a key in a named keyspace. Returns `Ok(value)` if found,\n"
" `Error(Nil)` if the key does not exist.\n"
"\n"
" **Panics** on store I/O or decode errors (e.g. file corruption, codec\n"
" mismatch against on-disk bytes).\n"
"\n"
" ```gleam\n"
" let assert Ok(\"admin\") = trove.get_in(db, keyspace: users, key: \"alice\")\n"
" ```\n"
).
-spec get_in(db(any(), any()), keyspace(MYJ, MYK), MYJ) -> {ok, MYK} |
{error, nil}.
get_in(Db, Keyspace, Key) ->
gleam@result:'try'(
trove@internal@db:get_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key),
erlang:element(3, Db)
),
fun(Value_bytes) ->
(erlang:element(3, erlang:element(4, Keyspace)))(Value_bytes)
end
).
-file("src/trove.gleam", 276).
?DOC(
" Remove a key from a named keyspace. No error if the key does not exist.\n"
"\n"
" **Panics** on store I/O errors.\n"
"\n"
" ```gleam\n"
" trove.delete_in(db, keyspace: users, key: \"alice\")\n"
" ```\n"
).
-spec delete_in(db(any(), any()), keyspace(MYT, any()), MYT) -> nil.
delete_in(Db, Keyspace, Key) ->
trove@internal@db:delete_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key),
erlang:element(3, Db)
).
-file("src/trove.gleam", 296).
?DOC(
" Check whether a key exists in a named keyspace.\n"
"\n"
" **Panics** on store I/O errors.\n"
"\n"
" ```gleam\n"
" let assert True = trove.has_key_in(db, keyspace: users, key: \"alice\")\n"
" ```\n"
).
-spec has_key_in(db(any(), any()), keyspace(MZB, any()), MZB) -> boolean().
has_key_in(Db, Keyspace, Key) ->
trove@internal@db:has_key_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key),
erlang:element(3, Db)
).
-file("src/trove.gleam", 314).
?DOC(
" Returns the number of live entries in a named keyspace.\n"
"\n"
" ```gleam\n"
" let n = trove.size_in(db, keyspace: users)\n"
" ```\n"
).
-spec size_in(db(any(), any()), keyspace(any(), any())) -> integer().
size_in(Db, Keyspace) ->
trove@internal@db:size_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
erlang:element(3, Db)
).
-file("src/trove.gleam", 366).
?DOC(
" Atomically insert and delete entries in a named keyspace under a single\n"
" header write. Puts are applied first, then deletes.\n"
"\n"
" **Panics** on store I/O errors.\n"
"\n"
" ```gleam\n"
" trove.put_and_delete_multi_in(\n"
" db,\n"
" keyspace: users,\n"
" puts: [#(\"bob\", \"admin\")],\n"
" deletes: [\"alice\"],\n"
" )\n"
" ```\n"
).
-spec put_and_delete_multi_in(
db(any(), any()),
keyspace(NAJ, NAK),
list({NAJ, NAK}),
list(NAJ)
) -> nil.
put_and_delete_multi_in(Db, Keyspace, Puts, Deletes) ->
Encoded_puts = gleam@list:map(
Puts,
fun(Pair) ->
{(erlang:element(2, erlang:element(3, Keyspace)))(
erlang:element(1, Pair)
),
(erlang:element(2, erlang:element(4, Keyspace)))(
erlang:element(2, Pair)
)}
end
),
Encoded_deletes = gleam@list:map(
Deletes,
erlang:element(2, erlang:element(3, Keyspace))
),
trove@internal@db:put_and_delete_multi_in(
erlang:element(2, Db),
erlang:element(2, Keyspace),
Encoded_puts,
Encoded_deletes,
erlang:element(3, Db)
).
-file("src/trove.gleam", 330).
?DOC(
" Atomically insert multiple key-value pairs into a named keyspace. A\n"
" single header write covers the entire batch.\n"
"\n"
" **Panics** on store I/O errors.\n"
"\n"
" ```gleam\n"
" trove.put_multi_in(\n"
" db,\n"
" keyspace: users,\n"
" entries: [#(\"alice\", \"admin\"), #(\"bob\", \"member\")],\n"
" )\n"
" ```\n"
).
-spec put_multi_in(db(any(), any()), keyspace(MZR, MZS), list({MZR, MZS})) -> nil.
put_multi_in(Db, Keyspace, Entries) ->
put_and_delete_multi_in(Db, Keyspace, Entries, []).
-file("src/trove.gleam", 345).
?DOC(
" Atomically delete multiple keys from a named keyspace.\n"
"\n"
" **Panics** on store I/O errors.\n"
"\n"
" ```gleam\n"
" trove.delete_multi_in(db, keyspace: users, keys: [\"alice\", \"bob\"])\n"
" ```\n"
).
-spec delete_multi_in(db(any(), any()), keyspace(NAA, any()), list(NAA)) -> nil.
delete_multi_in(Db, Keyspace, Keys) ->
put_and_delete_multi_in(Db, Keyspace, [], Keys).
-file("src/trove.gleam", 425).
?DOC(
" Close the database and release the file handle and path lock. Does not\n"
" fsync. If using `ManualSync`, call `file_sync` before closing to ensure\n"
" durability. The `Db` handle must not be used after calling this.\n"
"\n"
" **Panics** if the store file handle cannot be closed.\n"
"\n"
" ```gleam\n"
" trove.close(db)\n"
" ```\n"
).
-spec close(db(any(), any())) -> nil.
close(Db) ->
trove@internal@db:close(erlang:element(2, Db), erlang:element(3, Db)).
-file("src/trove.gleam", 440).
?DOC(
" Trigger a manual compaction. Rebuilds the store file keeping only live\n"
" entries, resetting the dirt factor to zero. Returns `Ok(Nil)` on success\n"
" or `Error(reason)` if compaction failed. On failure the database remains\n"
" functional with the original store file.\n"
"\n"
" The timeout is separate from `call_timeout` because compaction can take\n"
" much longer than normal operations.\n"
"\n"
" ```gleam\n"
" let assert Ok(Nil) = trove.compact(db, timeout: 60_000)\n"
" ```\n"
).
-spec compact(db(any(), any()), integer()) -> {ok, nil} | {error, binary()}.
compact(Db, Timeout) ->
trove@internal@db:compact(erlang:element(2, Db), Timeout).
-file("src/trove.gleam", 449).
?DOC(
" Returns the number of live entries in the database.\n"
"\n"
" ```gleam\n"
" let count = trove.size(db)\n"
" ```\n"
).
-spec size(db(any(), any())) -> integer().
size(Db) ->
trove@internal@db:size(erlang:element(2, Db), erlang:element(3, Db)).
-file("src/trove.gleam", 458).
?DOC(
" Returns `True` if the database contains no entries.\n"
"\n"
" ```gleam\n"
" let empty = trove.is_empty(db)\n"
" ```\n"
).
-spec is_empty(db(any(), any())) -> boolean().
is_empty(Db) ->
size(Db) =:= 0.
-file("src/trove.gleam", 475).
?DOC(
" Returns the current dirt factor: a float between 0.0 and 1.0 that\n"
" approximates how much of the store file is occupied by superseded data.\n"
" Overwrites and deletes increment the dirt counter because they write new\n"
" nodes that make old ones unreachable. New inserts do not increment dirt\n"
" since they don't supersede existing data. The formula is\n"
" `dirt / (1 + size + dirt)`; the `+1` ensures the result is always\n"
" well-defined, even for an empty tree. The value approaches but never\n"
" reaches 1.0. Higher values mean more wasted space that compaction would\n"
" reclaim.\n"
"\n"
" ```gleam\n"
" let df = trove.dirt_factor(db)\n"
" ```\n"
).
-spec dirt_factor(db(any(), any())) -> float().
dirt_factor(Db) ->
trove@internal@db:dirt_factor(erlang:element(2, Db), erlang:element(3, Db)).
-file("src/trove.gleam", 490).
?DOC(
" Force an fsync of the store file to disk. Useful when `auto_file_sync`\n"
" is set to `ManualSync` and you want to control when data is flushed.\n"
"\n"
" **Panics** if the fsync system call fails.\n"
"\n"
" ```gleam\n"
" let config = trove.Config(..config, auto_file_sync: trove.ManualSync)\n"
" let assert Ok(db) = trove.open(config)\n"
" trove.put(db, key: \"hello\", value: \"world\")\n"
" trove.file_sync(db)\n"
" ```\n"
).
-spec file_sync(db(any(), any())) -> nil.
file_sync(Db) ->
trove@internal@db:file_sync(erlang:element(2, Db), erlang:element(3, Db)).
-file("src/trove.gleam", 515).
?DOC(
" Look up a key. Returns `Ok(value)` if found, `Error(Nil)` if the\n"
" key does not exist.\n"
"\n"
" **Panics** on store I/O or decode errors (e.g. file corruption).\n"
"\n"
" ```gleam\n"
" let assert Ok(\"world\") = trove.get(db, key: \"hello\")\n"
" ```\n"
).
-spec get(db(NCB, NCC), NCB) -> {ok, NCC} | {error, nil}.
get(Db, Key) ->
trove@internal@db:get(erlang:element(2, Db), Key, erlang:element(3, Db)).
-file("src/trove.gleam", 526).
?DOC(
" Insert or update a key-value pair.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.put(db, key: \"hello\", value: \"world\")\n"
" ```\n"
).
-spec put(db(NCH, NCI), NCH, NCI) -> nil.
put(Db, Key, Value) ->
trove@internal@db:put(
erlang:element(2, Db),
Key,
Value,
erlang:element(3, Db)
).
-file("src/trove.gleam", 537).
?DOC(
" Remove a key. No error if the key does not exist.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.delete(db, key: \"hello\")\n"
" ```\n"
).
-spec delete(db(NCL, any()), NCL) -> nil.
delete(Db, Key) ->
trove@internal@db:delete(erlang:element(2, Db), Key, erlang:element(3, Db)).
-file("src/trove.gleam", 548).
?DOC(
" Check whether a key exists in the database.\n"
"\n"
" **Panics** on store I/O or decode errors (e.g. file corruption).\n"
"\n"
" ```gleam\n"
" let assert True = trove.has_key(db, key: \"hello\")\n"
" ```\n"
).
-spec has_key(db(NCP, any()), NCP) -> boolean().
has_key(Db, Key) ->
trove@internal@db:has_key(erlang:element(2, Db), Key, erlang:element(3, Db)).
-file("src/trove.gleam", 560).
?DOC(
" Atomically insert multiple key-value pairs. A single header write covers\n"
" the entire batch.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.put_multi(db, entries: [#(\"a\", \"1\"), #(\"b\", \"2\")])\n"
" ```\n"
).
-spec put_multi(db(NCT, NCU), list({NCT, NCU})) -> nil.
put_multi(Db, Entries) ->
trove@internal@db:put_multi(
erlang:element(2, Db),
Entries,
erlang:element(3, Db)
).
-file("src/trove.gleam", 571).
?DOC(
" Atomically delete multiple keys.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.delete_multi(db, keys: [\"a\", \"b\"])\n"
" ```\n"
).
-spec delete_multi(db(NCY, any()), list(NCY)) -> nil.
delete_multi(Db, Keys) ->
trove@internal@db:delete_multi(
erlang:element(2, Db),
Keys,
erlang:element(3, Db)
).
-file("src/trove.gleam", 587).
?DOC(
" Atomically insert and delete entries in a single operation. Puts are\n"
" applied first, then deletes, all under a single header write.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.put_and_delete_multi(\n"
" db,\n"
" puts: [#(\"new_key\", \"value\")],\n"
" deletes: [\"old_key\"],\n"
" )\n"
" ```\n"
).
-spec put_and_delete_multi(db(NDD, NDE), list({NDD, NDE}), list(NDD)) -> nil.
put_and_delete_multi(Db, Puts, Deletes) ->
trove@internal@db:put_and_delete_multi(
erlang:element(2, Db),
Puts,
Deletes,
erlang:element(3, Db)
).
-file("src/trove.gleam", 722).
-spec drain_latest_loop(
gleam@erlang@process:subject(NDW),
gleam@option:option(NDW)
) -> gleam@option:option(NDW).
drain_latest_loop(Subject, Acc) ->
case gleam@erlang@process:'receive'(Subject, 0) of
{ok, Value} ->
drain_latest_loop(Subject, {some, Value});
{error, nil} ->
Acc
end.
-file("src/trove.gleam", 718).
-spec drain_latest(gleam@erlang@process:subject(NDT)) -> gleam@option:option(NDT).
drain_latest(Subject) ->
drain_latest_loop(Subject, none).
-file("src/trove.gleam", 655).
?DOC(
" Run an atomic transaction. The callback receives a `Tx` handle and must\n"
" return `Commit(tx:, result: value)` to apply writes or\n"
" `Cancel(result: value)` to discard. The transaction holds exclusive\n"
" write access for its duration.\n"
"\n"
" The `timeout` parameter (in milliseconds) controls how long the caller\n"
" waits for the transaction to complete, including queue wait time and\n"
" callback execution. Choose a value appropriate for your workload.\n"
" Queued operations or auto-compaction may delay the start, and a\n"
" long-running callback consumes the remaining budget.\n"
"\n"
" **Important:** The callback runs inside the database actor. Do not call\n"
" any `trove` functions (such as `get`, `put`, `compact`, etc.) on the\n"
" same `Db` handle from within the callback; this will deadlock the actor\n"
" until the call timeout fires. Use the `Tx` handle (`tx_get`, `tx_put`,\n"
" `tx_delete`) for all reads and writes inside the transaction.\n"
"\n"
" **Panics** if the `Commit` variant contains a stale or replaced `Tx`\n"
" handle (e.g. the original handle instead of the latest one returned by\n"
" `tx_put`/`tx_delete`).\n"
"\n"
" **Non-escaping:** The `Tx` handle is only valid inside the callback.\n"
" Do not store it in a variable, send it to another process, or return it.\n"
" Using a `Tx` after the callback returns will panic or produce undefined\n"
" behavior.\n"
"\n"
" **Timeout semantics:** If the timeout fires while the callback is still\n"
" executing, the caller panics but the actor continues running the callback\n"
" to completion. This means writes may be durably committed even though the\n"
" caller observes a timeout failure. Choose a timeout that accommodates your\n"
" expected callback duration and any queued operations ahead of it.\n"
"\n"
" ```gleam\n"
" let result = trove.transaction(db, timeout: 5000, callback: fn(tx) {\n"
" let tx = trove.tx_put(tx, key: \"key\", value: \"value\")\n"
" trove.Commit(tx:, result: \"done\")\n"
" })\n"
" ```\n"
).
-spec transaction(
db(NDJ, NDK),
integer(),
fun((trove@internal@tx:tx(NDJ, NDK)) -> transaction_result(NDJ, NDK, NDP))
) -> NDP.
transaction(Db, Timeout, Callback) ->
Result_subject = gleam@erlang@process:new_subject(),
Token = erlang:make_ref(),
Run = fun(Transaction) ->
Nonce_subject = gleam@erlang@process:new_subject(),
Transaction@1 = trove@internal@tx:set_token(Transaction, Token),
Transaction@2 = trove@internal@tx:set_nonce_tracker(
Transaction@1,
{some, Nonce_subject}
),
case exception_ffi:rescue(fun() -> Callback(Transaction@2) end) of
{ok, {commit, Tx_inner, Value}} ->
case exception_ffi:rescue(
fun() ->
case trove@internal@tx:token(Tx_inner) =:= Token of
true -> nil;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"transaction"/utf8>>,
line => 672,
value => _assert_fail,
start => 21596,
'end' => 21645,
pattern_start => 21607,
pattern_end => 21611})
end,
Latest_nonce = drain_latest(Nonce_subject),
case Latest_nonce of
none ->
nil;
{some, Expected} ->
case trove@internal@tx:nonce(Tx_inner) =:= Expected of
true -> nil;
_assert_fail@1 ->
erlang:error(
#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"transaction"/utf8>>,
line => 677,
value => _assert_fail@1,
start => 21827,
'end' => 21879,
pattern_start => 21838,
pattern_end => 21842}
)
end,
nil
end
end
) of
{ok, nil} ->
gleam@erlang@process:send(Result_subject, {ok, Value}),
{commit_outcome,
trove@internal@tx:get_tree(Tx_inner),
trove@internal@tx:get_other_trees(Tx_inner)};
{error, Ex} ->
gleam@erlang@process:send(Result_subject, {error, Ex}),
cancel_outcome
end;
{ok, {cancel, Value@1}} ->
gleam@erlang@process:send(Result_subject, {ok, Value@1}),
cancel_outcome;
{error, Ex@1} ->
gleam@erlang@process:send(Result_subject, {error, Ex@1}),
cancel_outcome
end
end,
trove@internal@db:transaction(erlang:element(2, Db), Timeout, Run),
Result@1 = case gleam@erlang@process:'receive'(Result_subject, 0) of
{ok, Result} -> Result;
_assert_fail@2 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"transaction"/utf8>>,
line => 711,
value => _assert_fail@2,
start => 22833,
'end' => 22891,
pattern_start => 22844,
pattern_end => 22854})
end,
case Result@1 of
{ok, Value@2} ->
Value@2;
{error, Ex@2} ->
reraise(Ex@2)
end.
-file("src/trove.gleam", 744).
?DOC(
" Read a key within a transaction. Sees writes made earlier in the same\n"
" transaction. Returns `Error(Nil)` if the key does not exist.\n"
"\n"
" **Panics** on store I/O or decode errors (e.g. file corruption).\n"
"\n"
" ```gleam\n"
" trove.transaction(db, timeout: 5000, callback: fn(tx) {\n"
" let assert Ok(current) = trove.tx_get(tx, key: \"counter\")\n"
" let tx = trove.tx_put(tx, key: \"counter\", value: current <> \"!\")\n"
" trove.Commit(tx:, result: Nil)\n"
" })\n"
" ```\n"
).
-spec tx_get(trove@internal@tx:tx(NEA, NEB), NEA) -> {ok, NEB} | {error, nil}.
tx_get(Tx, Key) ->
trove@internal@tx:get(Tx, Key).
-file("src/trove.gleam", 758).
?DOC(
" Write a key-value pair within a transaction. Returns the updated `Tx`.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.transaction(db, timeout: 5000, callback: fn(tx) {\n"
" let tx = trove.tx_put(tx, key: \"greeting\", value: \"hello\")\n"
" trove.Commit(tx:, result: Nil)\n"
" })\n"
" ```\n"
).
-spec tx_put(trove@internal@tx:tx(NEG, NEH), NEG, NEH) -> trove@internal@tx:tx(NEG, NEH).
tx_put(Tx, Key, Value) ->
trove@internal@tx:put(Tx, Key, Value).
-file("src/trove.gleam", 772).
?DOC(
" Delete a key within a transaction. Returns the updated `Tx`.\n"
"\n"
" **Panics** on store I/O errors (e.g. disk full, file corruption).\n"
"\n"
" ```gleam\n"
" trove.transaction(db, timeout: 5000, callback: fn(tx) {\n"
" let tx = trove.tx_delete(tx, key: \"old_key\")\n"
" trove.Commit(tx:, result: Nil)\n"
" })\n"
" ```\n"
).
-spec tx_delete(trove@internal@tx:tx(NEM, NEN), NEM) -> trove@internal@tx:tx(NEM, NEN).
tx_delete(Tx, Key) ->
trove@internal@tx:delete(Tx, Key).
-file("src/trove.gleam", 787).
?DOC(
" Check whether a key exists within a transaction. Sees writes made\n"
" earlier in the same transaction.\n"
"\n"
" **Panics** on store I/O or decode errors (e.g. file corruption).\n"
"\n"
" ```gleam\n"
" trove.transaction(db, timeout: 5000, callback: fn(tx) {\n"
" let exists = trove.tx_has_key(tx, key: \"counter\")\n"
" trove.Commit(tx:, result: exists)\n"
" })\n"
" ```\n"
).
-spec tx_has_key(trove@internal@tx:tx(NES, any()), NES) -> boolean().
tx_has_key(Tx, Key) ->
_pipe = tx_get(Tx, Key),
gleam@result:is_ok(_pipe).
-file("src/trove.gleam", 796).
?DOC(
" Look up a key in a named keyspace within a transaction. Sees writes made\n"
" earlier in the same transaction.\n"
"\n"
" **Panics** if the keyspace has not been registered in this session via\n"
" `trove.keyspace(...)`, or on store I/O or decode errors.\n"
).
-spec tx_get_in(trove@internal@tx:tx(any(), any()), keyspace(NFA, NFB), NFA) -> {ok,
NFB} |
{error, nil}.
tx_get_in(Tx, Keyspace, Key) ->
gleam@result:'try'(
trove@internal@tx:get_in(
Tx,
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key)
),
fun(Value_bytes) ->
(erlang:element(3, erlang:element(4, Keyspace)))(Value_bytes)
end
).
-file("src/trove.gleam", 814).
?DOC(
" Insert or update a key-value pair in a named keyspace within a\n"
" transaction. Returns the updated `Tx`.\n"
"\n"
" **Panics** if the keyspace has not been registered in this session via\n"
" `trove.keyspace(...)`, or on store I/O errors.\n"
).
-spec tx_put_in(trove@internal@tx:tx(NFG, NFH), keyspace(NFK, NFL), NFK, NFL) -> trove@internal@tx:tx(NFG, NFH).
tx_put_in(Tx, Keyspace, Key, Value) ->
trove@internal@tx:put_in(
Tx,
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key),
(erlang:element(2, erlang:element(4, Keyspace)))(Value)
).
-file("src/trove.gleam", 832).
?DOC(
" Delete a key from a named keyspace within a transaction.\n"
"\n"
" **Panics** if the keyspace has not been registered in this session via\n"
" `trove.keyspace(...)`, or on store I/O errors.\n"
).
-spec tx_delete_in(trove@internal@tx:tx(NFQ, NFR), keyspace(NFU, any()), NFU) -> trove@internal@tx:tx(NFQ, NFR).
tx_delete_in(Tx, Keyspace, Key) ->
trove@internal@tx:delete_in(
Tx,
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key)
).
-file("src/trove.gleam", 848).
?DOC(
" Check whether a key exists in a named keyspace within a transaction.\n"
"\n"
" **Panics** if the keyspace has not been registered in this session via\n"
" `trove.keyspace(...)`, or on store I/O or decode errors.\n"
).
-spec tx_has_key_in(
trove@internal@tx:tx(any(), any()),
keyspace(NGE, any()),
NGE
) -> boolean().
tx_has_key_in(Tx, Keyspace, Key) ->
_pipe = tx_get_in(Tx, Keyspace, Key),
gleam@result:is_ok(_pipe).
-file("src/trove.gleam", 873).
?DOC(
" Run a callback with a point-in-time snapshot. The snapshot sees the state\n"
" of the database at the moment it was acquired; subsequent writes are\n"
" invisible to it.\n"
"\n"
" **Non-escaping:** The `Snapshot` handle is only valid inside the callback.\n"
" Do not store it in a variable, send it to another process, or return it.\n"
" Using a `Snapshot` after the callback returns will panic or produce\n"
" undefined behavior because the underlying file handle is closed on exit.\n"
"\n"
" **Panics** if the snapshot file handle cannot be opened.\n"
"\n"
" ```gleam\n"
" let result = trove.with_snapshot(db, fn(snap) {\n"
" trove.snapshot_get(snapshot: snap, key: \"my_key\")\n"
" })\n"
" // result: Result(String, Nil)\n"
" ```\n"
).
-spec with_snapshot(
db(NGI, NGJ),
fun((trove@internal@snapshot:snapshot(NGI, NGJ)) -> NGO)
) -> NGO.
with_snapshot(Db, Callback) ->
Snap@1 = case trove@internal@db:acquire_snapshot(
erlang:element(2, Db),
erlang:element(3, Db)
) of
{ok, Snap} -> Snap;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"with_snapshot"/utf8>>,
line => 877,
value => _assert_fail,
start => 27987,
'end' => 28079,
pattern_start => 27998,
pattern_end => 28006})
end,
exception_ffi:defer(
fun() -> trove@internal@snapshot:close(Snap@1) end,
fun() -> Callback(Snap@1) end
).
-file("src/trove.gleam", 894).
?DOC(
" Look up a key in a snapshot. Returns `Error(Nil)` if the key does\n"
" not exist.\n"
"\n"
" **Panics** on store read or decode errors (e.g. file corruption).\n"
"\n"
" ```gleam\n"
" trove.with_snapshot(db, fn(snap) {\n"
" let assert Ok(value) = trove.snapshot_get(snapshot: snap, key: \"my_key\")\n"
" value\n"
" })\n"
" ```\n"
).
-spec snapshot_get(trove@internal@snapshot:snapshot(NGP, NGQ), NGP) -> {ok, NGQ} |
{error, nil}.
snapshot_get(Snapshot, Key) ->
trove@internal@snapshot:get(Snapshot, Key).
-file("src/trove.gleam", 931).
?DOC(
" Iterate over entries in a snapshot within optional key bounds.\n"
" Returns a lazy `Yielder` that streams entries from disk on demand,\n"
" reading only one leaf node at a time.\n"
"\n"
" The yielder holds a reference to the snapshot's file handle, so it\n"
" must be consumed before the snapshot is closed. For large ranges,\n"
" prefer this over `range` to avoid loading all entries into memory.\n"
"\n"
" **Panics** on store read or decode errors during iteration\n"
" (e.g. file corruption).\n"
"\n"
" Use `range.Inclusive(key)` or `range.Exclusive(key)` for bounds,\n"
" or `option.None` for unbounded. Use `range.Forward` or `range.Reverse`\n"
" for direction.\n"
"\n"
" ```gleam\n"
" import gleam/option.{None, Some}\n"
" import gleam/yielder\n"
" import trove/range\n"
"\n"
" let entries = trove.with_snapshot(db, fn(snap) {\n"
" let y = trove.snapshot_range(\n"
" snapshot: snap,\n"
" min: Some(range.Inclusive(\"a\")),\n"
" max: None,\n"
" direction: range.Forward,\n"
" )\n"
" yielder.to_list(y)\n"
" })\n"
" ```\n"
).
-spec snapshot_range(
trove@internal@snapshot:snapshot(NGV, NGW),
gleam@option:option(trove@range:bound(NGV)),
gleam@option:option(trove@range:bound(NGV)),
trove@range:direction()
) -> gleam@yielder:yielder({NGV, NGW}).
snapshot_range(Snapshot, Min, Max, Direction) ->
trove@internal@snapshot:range(Snapshot, Min, Max, Direction).
-file("src/trove.gleam", 965).
?DOC(
" Iterate over entries in the database within optional key bounds.\n"
" Returns a `List` of key-value pairs.\n"
"\n"
" For large result sets, use `with_snapshot` and `snapshot_range` instead\n"
" to stream entries lazily without loading them all at once.\n"
"\n"
" **Panics** if the snapshot file handle cannot be opened, or on store\n"
" read/decode errors during iteration.\n"
"\n"
" Use `range.Inclusive(key)` or `range.Exclusive(key)` for bounds,\n"
" or `option.None` for unbounded. Use `range.Forward` or `range.Reverse`\n"
" for direction.\n"
"\n"
" ```gleam\n"
" import gleam/option.{Some}\n"
" import trove/range\n"
"\n"
" let results =\n"
" trove.range(\n"
" db,\n"
" min: Some(range.Inclusive(\"a\")),\n"
" max: Some(range.Exclusive(\"z\")),\n"
" direction: range.Forward,\n"
" )\n"
" ```\n"
).
-spec range(
db(NHE, NHF),
gleam@option:option(trove@range:bound(NHE)),
gleam@option:option(trove@range:bound(NHE)),
trove@range:direction()
) -> list({NHE, NHF}).
range(Db, Min, Max, Direction) ->
Snap@1 = case trove@internal@db:acquire_snapshot(
erlang:element(2, Db),
erlang:element(3, Db)
) of
{ok, Snap} -> Snap;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"range"/utf8>>,
line => 971,
value => _assert_fail,
start => 30910,
'end' => 31002,
pattern_start => 30921,
pattern_end => 30929})
end,
exception_ffi:defer(
fun() -> trove@internal@snapshot:close(Snap@1) end,
fun() ->
_pipe = trove@internal@snapshot:range(Snap@1, Min, Max, Direction),
gleam@yielder:to_list(_pipe)
end
).
-file("src/trove.gleam", 988).
?DOC(
" Look up a key in a named keyspace within a snapshot.\n"
"\n"
" **Panics** if the keyspace was not registered before the snapshot was\n"
" acquired, or on store read or decode errors.\n"
"\n"
" ```gleam\n"
" trove.with_snapshot(db, fn(snap) {\n"
" trove.snapshot_get_in(snap, keyspace: users, key: \"alice\")\n"
" })\n"
" ```\n"
).
-spec snapshot_get_in(
trove@internal@snapshot:snapshot(any(), any()),
keyspace(NHR, NHS),
NHR
) -> {ok, NHS} | {error, nil}.
snapshot_get_in(Snapshot, Keyspace, Key) ->
gleam@result:'try'(
trove@internal@snapshot:get_in(
Snapshot,
erlang:element(2, Keyspace),
(erlang:element(2, erlang:element(3, Keyspace)))(Key)
),
fun(Value_bytes) ->
(erlang:element(3, erlang:element(4, Keyspace)))(Value_bytes)
end
).
-file("src/trove.gleam", 1043).
-spec encode_bound(
gleam@option:option(trove@range:bound(NIK)),
trove@codec:codec(NIK)
) -> gleam@option:option(trove@range:bound(bitstring())).
encode_bound(Bound, Key_codec) ->
gleam@option:map(Bound, fun(B) -> case B of
{inclusive, V} ->
{inclusive, (erlang:element(2, Key_codec))(V)};
{exclusive, V@1} ->
{exclusive, (erlang:element(2, Key_codec))(V@1)}
end end).
-file("src/trove.gleam", 1022).
?DOC(
" Iterate over entries in a named keyspace within a snapshot. Returns a\n"
" lazy `Yielder` streaming entries from disk.\n"
"\n"
" The yielder holds a reference to the snapshot's file handle; consume it\n"
" before the snapshot closes.\n"
"\n"
" **Panics** if the keyspace was not registered before the snapshot was\n"
" acquired, or on store read or decode errors during iteration.\n"
"\n"
" ```gleam\n"
" trove.with_snapshot(db, fn(snap) {\n"
" trove.snapshot_range_in(\n"
" snap,\n"
" keyspace: users,\n"
" min: Some(range.Inclusive(\"a\")),\n"
" max: None,\n"
" direction: range.Forward,\n"
" )\n"
" |> yielder.to_list\n"
" })\n"
" ```\n"
).
-spec snapshot_range_in(
trove@internal@snapshot:snapshot(any(), any()),
keyspace(NIB, NIC),
gleam@option:option(trove@range:bound(NIB)),
gleam@option:option(trove@range:bound(NIB)),
trove@range:direction()
) -> gleam@yielder:yielder({NIB, NIC}).
snapshot_range_in(Snapshot, Keyspace, Min, Max, Direction) ->
_pipe = trove@internal@snapshot:range_in(
Snapshot,
erlang:element(2, Keyspace),
encode_bound(Min, erlang:element(3, Keyspace)),
encode_bound(Max, erlang:element(3, Keyspace)),
Direction
),
gleam@yielder:map(
_pipe,
fun(Pair) ->
K@1 = case (erlang:element(3, erlang:element(3, Keyspace)))(
erlang:element(1, Pair)
) of
{ok, K} -> K;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"snapshot_range_in"/utf8>>,
line => 1037,
value => _assert_fail,
start => 32940,
'end' => 32992,
pattern_start => 32951,
pattern_end => 32956})
end,
V@1 = case (erlang:element(3, erlang:element(4, Keyspace)))(
erlang:element(2, Pair)
) of
{ok, V} -> V;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"snapshot_range_in"/utf8>>,
line => 1038,
value => _assert_fail@1,
start => 32997,
'end' => 33051,
pattern_start => 33008,
pattern_end => 33013})
end,
{K@1, V@1}
end
).
-file("src/trove.gleam", 1072).
?DOC(
" Iterate over entries in a named keyspace within optional key bounds.\n"
" Returns a `List` of key-value pairs. For large result sets, use\n"
" `with_snapshot` and `snapshot_range_in` instead.\n"
"\n"
" **Panics** if the keyspace has not been registered in this session via\n"
" `trove.keyspace(...)`, if the snapshot file handle cannot be opened,\n"
" or on store read or decode errors during iteration.\n"
"\n"
" ```gleam\n"
" let results = trove.range_in(\n"
" db,\n"
" keyspace: users,\n"
" min: Some(range.Inclusive(\"a\")),\n"
" max: Some(range.Exclusive(\"z\")),\n"
" direction: range.Forward,\n"
" )\n"
" ```\n"
).
-spec range_in(
db(any(), any()),
keyspace(NIU, NIV),
gleam@option:option(trove@range:bound(NIU)),
gleam@option:option(trove@range:bound(NIU)),
trove@range:direction()
) -> list({NIU, NIV}).
range_in(Db, Keyspace, Min, Max, Direction) ->
Snap@1 = case trove@internal@db:acquire_snapshot(
erlang:element(2, Db),
erlang:element(3, Db)
) of
{ok, Snap} -> Snap;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"range_in"/utf8>>,
line => 1079,
value => _assert_fail,
start => 34217,
'end' => 34309,
pattern_start => 34228,
pattern_end => 34236})
end,
exception_ffi:defer(
fun() -> trove@internal@snapshot:close(Snap@1) end,
fun() ->
_pipe = snapshot_range_in(Snap@1, Keyspace, Min, Max, Direction),
gleam@yielder:to_list(_pipe)
end
).
-file("src/trove.gleam", 1092).
-spec map_open_error(trove@internal@db:open_error()) -> open_error().
map_open_error(Error) ->
case Error of
{directory_error, Reason} ->
{directory_error, Reason};
{store_error, Reason@1} ->
{store_error, Reason@1};
{lock_error, Reason@2} ->
{lock_error, Reason@2};
actor_start_error ->
actor_start_error
end.
-file("src/trove.gleam", 1101).
-spec to_internal_auto_compact(auto_compact()) -> trove@internal@db:auto_compact().
to_internal_auto_compact(Setting) ->
case Setting of
{auto_compact, Min_dirt, Min_dirt_factor} ->
{auto_compact, Min_dirt, Min_dirt_factor};
no_auto_compact ->
no_auto_compact
end.
-file("src/trove.gleam", 499).
?DOC(
" Change the auto-compaction setting at runtime.\n"
"\n"
" ```gleam\n"
" trove.set_auto_compact(db, trove.AutoCompact(min_dirt: 1000, min_dirt_factor: 0.25))\n"
" ```\n"
).
-spec set_auto_compact(db(any(), any()), auto_compact()) -> nil.
set_auto_compact(Db, Setting) ->
trove@internal@db:set_auto_compact(
erlang:element(2, Db),
to_internal_auto_compact(Setting),
erlang:element(3, Db)
).
-file("src/trove.gleam", 1109).
-spec to_internal_file_sync(file_sync()) -> trove@internal@db:file_sync().
to_internal_file_sync(Setting) ->
case Setting of
auto_sync ->
auto_sync;
manual_sync ->
manual_sync
end.
-file("src/trove.gleam", 402).
?DOC(
" Open a database at the configured path. Creates the directory if it does\n"
" not exist. If a store file already exists, recovers the tree from the\n"
" latest valid header.\n"
"\n"
" ```gleam\n"
" let config = trove.Config(\n"
" path: \"./my_db\",\n"
" key_codec: codec.string(),\n"
" value_codec: codec.string(),\n"
" key_compare: string.compare,\n"
" auto_compact: trove.AutoCompact(min_dirt: 1000, min_dirt_factor: 0.25),\n"
" auto_file_sync: trove.AutoSync,\n"
" call_timeout: 5000,\n"
" )\n"
" let assert Ok(db) = trove.open(config)\n"
" ```\n"
).
-spec open(config(NAP, NAQ)) -> {ok, db(NAP, NAQ)} | {error, open_error()}.
open(Config) ->
_pipe = trove@internal@db:open(
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
to_internal_file_sync(erlang:element(7, Config)),
to_internal_auto_compact(erlang:element(6, Config)),
erlang:element(8, Config)
),
_pipe@1 = gleam@result:map(
_pipe,
fun(_capture) -> {db, _capture, erlang:element(8, Config)} end
),
gleam@result:map_error(_pipe@1, fun map_open_error/1).
-file("src/trove.gleam", 176).
?DOC(
" Obtain a typed handle to a named keyspace. First use of a name registers it\n"
" in this session; later uses update the codecs.\n"
"\n"
" **Panics** if `name` collides with the reserved default-keyspace sentinel.\n"
"\n"
" **Codec trust model.** Passing codecs that don't match those previously\n"
" used for the same keyspace is undefined behavior: reads will likely\n"
" produce garbage values or panics. Keep the\n"
" `(key_codec, value_codec, key_compare)` tuple stable across opens for a\n"
" given keyspace name. Matches the trust model of `Config.key_codec` and\n"
" `Config.value_codec`.\n"
"\n"
" ```gleam\n"
" let users =\n"
" trove.keyspace(\n"
" db,\n"
" name: \"users\",\n"
" key_codec: codec.string(),\n"
" value_codec: codec.string(),\n"
" key_compare: string.compare,\n"
" )\n"
" ```\n"
).
-spec keyspace(
db(any(), any()),
binary(),
trove@codec:codec(MXK),
trove@codec:codec(MXM),
fun((MXK, MXK) -> gleam@order:order())
) -> keyspace(MXK, MXM).
keyspace(Db, Name, Key_codec, Value_codec, Key_compare) ->
case Name /= <<"__trove_default__"/utf8>> of
true -> nil;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"trove"/utf8>>,
function => <<"keyspace"/utf8>>,
line => 183,
value => _assert_fail,
start => 6293,
'end' => 6349,
pattern_start => 6304,
pattern_end => 6308})
end,
trove@internal@db:register_keyspace(
erlang:element(2, Db),
Name,
adapt_compare(Key_codec, Key_compare),
erlang:element(3, Db)
),
{keyspace, Name, Key_codec, Value_codec, Key_compare}.