C++ · Batches and Arenas

Batches and Arenas

#include <ustore/status.hpp>#include <ustore/db.hpp>#include <ustore/docs_collection.hpp>#include <ustore/graph_collection.hpp>#include <ustore/types.hpp>#include <ustore/blobs_collection.hpp>#include <ustore/ranges.hpp>#include <ustore/strings_collection.hpp> 1.0.0-rc.1 · d6d40961

§ Arenas

Shorthand and verbs trade convenience for reuse, as in every binding:

  • Shorthand makes a one-off arena. blobs[key].value() calls ustore_arena_init, reads into that arena, and hands it to its answer, which frees it on destruction, so nothing is copied and no handle keeps state. A scalar answer, such as a presence or a degree, is copied out and the arena freed at once. The price is one arena per call, so a loop reads through a verb instead.
  • Verbs reuse a caller’s arena. arena_t::make(executor) places one where its executor computes, or on host pages for calls on the calling thread, and a loop passing it to every call allocates once. Every call rewinds it, so its views last until the next call given that arena; to keep them longer, pass that call another arena.
auto [arena, arena_status] = arena_t::make(executor);
for (std::span<ustore_key_t const> batch : batches) {
    auto [values, read_status] = db.on(executor).find("blobs")->read(batch, arena);
    if (read_status.failed()) break;
    consume(values); // valid until the next call given `arena`
}

An owned_value_t derives from the value_view_t it holds, so it compares and converts like one, and view() slices that plain view off. Collections, transactions and snapshots hold no arena, so threads share one handle, each passing its own arena.

Walks split the same way: the keys, pairs and prefixed streams own one arena each, while blobs_cursor_t and strings_cursor_t take the caller’s on every pull, so a loop reuses one arena it keeps:

auto [cursor, cursor_status] = blobs_cursor_t::make(db.receiver(), blobs);
std::size_t const wanted = 256;
for (std::size_t pulled = wanted; cursor_status.succeeded() && pulled == wanted;) {
    auto [page, page_status] = cursor.next_pairs(wanted, arena);
    if (page_status.failed()) break;
    consume(page.values); // valid until the next call given `arena`
    pulled = page.keys.size();
}

make opens the walk at the view its receiver reads and from an optional start key, next pulls keys alone and next_pairs keys with their values, and a page shorter than wanted exhausts the walk for good. strings_cursor_t::make takes a prefix and an optional start string, and its next pulls strings in the order of their text.

§ Receivers and Deferred Calls

The receiver chooses where a call runs, and the verb stays. db.on(executor), transaction->on(executor) and snapshot->on(executor) hand out the same collections, proxies and operators, whose calls block on the executor:

auto [executor, executor_status] = executor_handle_t::for_ram({.threads = 4});
blobs_collection_t on_executor = *db.on(executor).find("blobs");
status = on_executor[key_from_integer(42)].assign("purpose of life");

deferred(executor) hands out the deferred twin of every collection, whose verbs mirror the C calls and answer a deferred<verb> once submitted. find<graph_collection_t>(name) picks the twin by modality, read-only at a snapshot, while the deferred database’s [] takes a binary collection by the identifier create or find resolved, so addressing it never blocks. status() polls without blocking, std::move(call).wait() answers what the call produced, and a deferred dropped unwaited waits in its destructor:

deferred_database_t later = db.deferred(executor);
auto [arena, arena_status] = arena_t::make(executor);
std::array<ustore_key_t, 2> const keys {key_from_integer(1), key_from_integer(2)};
std::array<sz::string_view_t, 2> const texts {R"({"person": "Alice"})", R"({"person": "Bob"})"};

sz::expected<values_tape_t> values = sz::strings_view_t(texts).gather();
if (!values) return;
deferred<blobs_write_t> writing = later[blobs].write(keys, std::move(values.value));
status = std::move(writing).wait();
deferred<blobs_read_t> reading = later[blobs].read(keys, arena);
expected<values_view_t> read = std::move(reading).wait();
for (value_view_t found : *read)
    std::printf("%zu bytes\n", found.size());

Documents are written and read whole or by field, under any modification, and a gather answers a table together with the rows and columns it views outside the arena:

deferred_docs_collection_t people = *later.find<docs_collection_t>("people");
sz::expected<values_tape_t> documents = sz::strings_view_t(texts).gather();
if (!documents) return;
status = people.write(keys, std::move(documents.value), {}, ustore_docs_modify_merge_k).wait();

std::array<sz::string_view_t, 1> const columns {"person"};
sz::expected<values_tape_t> fields = sz::strings_view_t(columns).gather();
if (!fields) return;
auto gathered = people.gather(keys, std::move(fields.value), std::array {ustore_docs_field_type_string_k}, arena).wait();
auto names = gathered->table.column(0).as<std::string_view>();

Graphs read edges and degrees for one role of every vertex, and write and remove edges and vertices:

deferred_graph_collection_t graph = *later.find<graph_collection_t>("graph");
std::array<edge_t, 2> const chain {edge_t {key_from_integer(1), key_from_integer(2)}, edge_t {key_from_integer(2), key_from_integer(3)}};
status = graph.write_edges(chain).wait();
expected<edges_view_t> leaving = graph.read_edges(keys, arena, ustore_vertex_role_source_k).wait();
expected<std::span<ustore_vertex_degree_t const>> degrees = graph.degrees(keys, arena).wait();

Strings are written and read in batches, and a scan answers a page of strings for every prefix:

deferred_strings_collection_t words = *later.find<strings_collection_t>("words");
std::array<sz::string_view_t, 2> const spellings {"apple", "apricot"};
sz::expected<values_tape_t> strings = sz::strings_view_t(spellings).gather();
sz::expected<values_tape_t> meanings = sz::strings_view_t(texts).gather();
sz::expected<values_tape_t> prefixes = sz::strings_view_t(std::span {spellings}.first(1)).gather();
if (!strings || !meanings || !prefixes) return;
status = words.write(std::move(strings.value), std::move(meanings.value)).wait();
expected<strings_scan_t::finished_t> scanned = words.scan(std::move(prefixes.value), 16, arena).wait();

A transaction defers its own verbs too, reset among them:

deferred_transaction_t deferred_transaction = transaction->deferred(executor);
status = deferred_transaction.find("blobs")->remove(keys).wait();
expected<read_stamp_t> staged = deferred_transaction.stage().wait();
expected<commit_stamp_t> stamp = deferred_transaction.commit().wait();

The deferred database also creates, drops and lists collections, and describes, clears and compacts itself, while the deferred snapshot saves itself through save. Opening blocks instead, as every constructor does: init_ram, open_ram, open_rocksdb, view_rocksdb and open_leveldb take the executor to block on as their last argument, while open_nvme blocks on the executor its options name and reads how to move bytes from it once:

expected<database_t> opened = database_t::open_rocksdb({.directory = bytes_of("/var/lib/ustore")}, executor);

Either way the database runs its own calls on the calling thread until a receiver names an executor.

  • Deferred inputs are owned, arenas borrowed. Keys, vertices, edges and column types come as a std::array or an st::vector, and values, strings and fields as a values_tape_t, StringZilla’s sz::strings_t that sz::strings_view_t::gather fills, so a borrowed span or view does not compile. The arena comes as an arena_t&, borrowed until the call is final, which is safe because freeing an arena waits for the deferred calls naming it, while a temporary arena does not compile.
  • Submission copies the arguments. A deferred<verb> keeps one heap block that never moves, holding the outcome, the owned inputs and the outputs the call writes through, while the C argument structure dies on the stack it was filled on. status() polls through ustore_outcome_status, and wait() and the destructor wait.
  • Answers view the arena. A finished read answers the views a blocking one does, valid until the next call given that arena, and a finished gather owns its rows, columns and field names beside a table whose cells stay in the arena.
  • A transaction serves one call at a time. Wait on one deferred call through it before submitting the next, which otherwise finishes at once with status_code_t::contended_k.
  • The wrappers have no life of their own. They start no threads, keep no counts and hold no hidden arena: freeing a database, transaction or snapshot waits in C for the deferred calls naming it.

API

Types

deferred_blobs_read_collection_tA binary collection that only reads, whose verbs submit to an executor and answer deferred calls, as a snapshot offers it.
blobs_collection.hpp:244 ↗
class deferred_blobs_read_collection_t

Every plain input a verb reads moves into the call, so nothing it borrowed can go before the call is final, while an arena is borrowed, since freeing it waits for the call.

deferred_blobs_collection_tA binary collection whose verbs submit to an executor and answer deferred calls, reading and writing the head or a transaction.
blobs_collection.hpp:286 ↗
class deferred_blobs_collection_t
deferred_docs_collection_tA collection of JSON documents whose verbs submit to an executor and answer deferred calls, reading and writing the head or a transaction.
docs_collection.hpp:481 ↗
class deferred_docs_collection_t
deferred_graph_read_collection_tA graph collection that only reads, whose verbs submit to an executor and answer deferred calls, as a snapshot offers it.
graph_collection.hpp:458 ↗
class deferred_graph_read_collection_t

Every plain input a verb reads moves into the call, so nothing it borrowed can go before the call is final, while an arena is borrowed, since freeing it waits for the call.

deferred_strings_read_collection_tA collection keyed by strings that only reads, whose verbs submit to an executor and answer deferred calls, as a snapshot offers it.
strings_collection.hpp:541 ↗
class deferred_strings_read_collection_t

Every plain input a verb reads moves into the call, so nothing it borrowed can go before the call is final, while an arena is borrowed, since freeing it waits for the call.

deferred_strings_collection_tA collection keyed by strings whose verbs submit to an executor and answer deferred calls, reading and writing the head or a transaction.
strings_collection.hpp:619 ↗
class deferred_strings_collection_t
arena_tOwns an arena made where an executor computes, which the C ABI fills with results, freeing it on destruction.
types.hpp:112 ↗
class arena_t
ownedA view of what one call wrote into an arena made for that call alone, which it frees on destruction, as shorthand with no place for an arena answers.
types.hpp:155 ↗
template <typename view_type_>
class owned

It moves but never copies, and view slices off the plain view.

receiver_tWhat every call made through one receiver names before its inputs: the database, the executor that runs it, and the transaction or snapshot its reads see and its writes go through.
types.hpp:198 ↗
struct receiver_t

Fields

databaseustore_database_tAn open database.
executorustore_executor_tExecutor that runs the calls, where nullptr runs them on the calling thread.
transactionustore_transaction_tTransaction the writes go through and the reads see, or nullptr for the head.
snapshotustore_snapshot_tSnapshot the reads see when no transaction is named, or nullptr for the head.
progress_arg_tA callable lent to a long call as the C ABI receives it, a callback and the state it reads, or neither for a call nothing watches.
types.hpp:291 ↗
class progress_arg_t

The call borrows the callable until it is final.

Fields

callbackustore_progress_callback_tReports the advance of the call, or nullptr for a call nothing watches.
statevoid *The callable that callback calls.
ClassdeferredA call submitted to an executor, owning the one heap block that holds its outcome, every plain input it reads and every output it writes through, which never moves until the call is final.6
status.hpp:35 ↗
template <typename verb_type_>
class deferred

Submission copies the C arguments, so they live only on the stack of submit, while everything they point to lives in the block. An arena is a handle the call borrows, since freeing it waits for the call. status polls without blocking, and wait blocks until the call is final and hands over what it produced. The destructor waits too, so nothing the call reads goes away under it, and a block that could not be allocated reads as a call that finished with status_code_t::out_of_memory_k.

deferred
status.hpp:66 ↗
deferred(deferred && other) noexcept;
deferred(deferred const &)=delete;
operator=
status.hpp:67 ↗
deferred & operator=(deferred && other) noexcept;
deferred & operator=(deferred const &)=delete;
~deferred
status.hpp:73 ↗
~deferred() noexcept;
statusThe status of the call, polled through ustore_outcome_status: status_code_t::deferred_k until final.
status.hpp:100 ↗
status_code_t status() const noexcept;
waitBlocks until the call is final, then answers what it produced, or the status explaining why it did not.
status.hpp:106 ↗
answer_t wait() &&noexcept;
submitMoves inputs into a new block, lets prepare fill the C arguments from them, and submits a copy.
status.hpp:82 ↗
template <typename prepare_type_, typename... inputs_types_>
deferred submit(
    prepare_type_ && prepare,
    inputs_types_... inputs) noexcept;

Parameters

prepareinFills the C arguments from the verb and the owned inputs, pointing them only into the block, and answers a status that becomes the final one, with nothing submitted, when it fails.
inputsinEverything the call reads, which the block owns until the call is final.
Classdeferred_database_tThe head of a database, whose verbs submit to an executor and answer deferred calls, as database_ref_t::deferred hands it out.9
db.hpp:402 ↗
class deferred_database_t
deferred_database_t
db.hpp:406 ↗
deferred_database_t(receiver_t receiver) noexcept;
findThe collection named name at the head, as the deferred twin of collection_type_.
db.hpp:410 ↗
template <typename collection_type_ = blobs_collection_t>
expected<typename collection_type_::deferred_t> find(
    std::string_view name) const noexcept;
operator[]The binary collection collection identifies, resolved beforehand, so addressing it never blocks.
db.hpp:417 ↗
deferred_blobs_collection_t operator[](
    ustore_collection_id_t collection) const noexcept;
createCreates a collection holding the modality of collection_type_ under name, which the call copies and no other collection may carry, answering its identifier.
db.hpp:424 ↗
template <typename collection_type_ = blobs_collection_t>
deferred<create_t> create(std::string_view name) const noexcept;
dropDrops what mode names of the collection collection identifies.
db.hpp:438 ↗
deferred<drop_t> drop(
    ustore_collection_id_t collection,
    ustore_drop_mode_t mode = ustore_drop_collection_k) const noexcept;
collectionsLists the collections at the head, with their identifiers in arena, borrowed until the call is final.
db.hpp:450 ↗
deferred<list_t> collections(arena_t & arena) const noexcept;
describeReports the engine of the database, what it supports and how much it holds.
db.hpp:462 ↗
deferred<describe_t> describe() const noexcept;
clearEmpties every collection at once, as mode names.
db.hpp:472 ↗
deferred<clear_t> clear(ustore_drop_mode_t mode) const noexcept;
compactReclaims the space overwritten and removed data still takes, reporting its advance to progress, borrowed until the call is final.
db.hpp:483 ↗
deferred<compact_t> compact(
    progress_arg_t progress = {}) const noexcept;
Classdeferred_docs_read_collection_tA collection of JSON documents that only reads, whose verbs submit to an executor and answer deferred calls, as a snapshot offers it.4
docs_collection.hpp:350 ↗
class deferred_docs_read_collection_t

Every plain input a verb reads moves into the call, so nothing it borrowed can go before the call is final, while an arena is borrowed, since freeing it waits for the call.

deferred_docs_read_collection_t
docs_collection.hpp:356 ↗
deferred_docs_read_collection_t() noexcept=default;
deferred_docs_read_collection_t(
    receiver_t receiver,
    ustore_collection_id_t collection) noexcept;
readReads the documents of keys, or the fields fields name inside them, into arena.
docs_collection.hpp:374 ↗
template <owned_array_concept<ustore_key_t> keys_type_>
deferred<docs_read_t> read(
    keys_type_ keys,
    arena_t & arena,
    values_tape_t fields = {},
    ustore_docs_field_type_t type = ustore_docs_field_type_json_k,
    ustore_watch_t watch = ustore_watch_keys_k) const noexcept;

Parameters

keysinKeys to read, owned by the call until it is final.
arenainMemory the values land in, borrowed until the call is final, which the answer views.
fieldsinNone for whole documents, one field for every key, or one per key.
typeinFormat the values are exported in.
watchinWhether a read inside a transaction records the keys for conflicts.

Statuses

  • status_code_t::invalid_argument_k The fields are neither none, one, nor one per key.
gistLists the unique paths to the leaves of the documents of keys into arena.
docs_collection.hpp:409 ↗
template <owned_array_concept<ustore_key_t> keys_type_>
deferred<docs_gist_t> gist(
    keys_type_ keys,
    arena_t & arena,
    ustore_watch_t watch = ustore_watch_keys_k) const noexcept;

Parameters

keysinKeys of the documents, owned by the call until it is final.
arenainMemory the paths land in, borrowed until the call is final, which the answer views.
watchinWhether a read inside a transaction records the keys for conflicts.
gatherGathers the fields fields name from the documents of keys into arena, as a table typed at runtime.
docs_collection.hpp:441 ↗
template <owned_array_concept<ustore_key_t> keys_type_, owned_array_concept<ustore_docs_field_type_t> types_type_>
deferred<docs_gather_t> gather(
    keys_type_ keys,
    values_tape_t fields,
    types_type_ types,
    arena_t & arena,
    ustore_watch_t watch = ustore_watch_keys_k) const noexcept;

Parameters

keysinKeys of the documents, one per row.
fieldsinFields to gather, one per column, which the finished gather hands back.
typesinType every column converts its field into, one per field.
arenainMemory the columns land in, borrowed until the call is final, which the table views.
watchinWhether a read inside a transaction records the keys for conflicts.

Statuses

  • status_code_t::invalid_argument_k The types and fields differ in count.
Classdeferred_graph_collection_tA graph collection whose verbs submit to an executor and answer deferred calls, reading and writing the head or a transaction.4
graph_collection.hpp:530 ↗
class deferred_graph_collection_t
write_edgesWrites edges between existing or new vertices.
graph_collection.hpp:558 ↗
template <owned_array_concept<edge_t> edges_type_>
deferred<graph_write_edges_t> write_edges(
    edges_type_ edges,
    ustore_durability_t durability = ustore_durability_buffered_k) const noexcept;

Parameters

edgesinEdges to write, owned by the call until it is final.
durabilityinWhether the call finishes only after the data reaches the disk.
remove_edgesRemoves edges, keeping their vertices.
graph_collection.hpp:574 ↗
template <owned_array_concept<edge_t> edges_type_>
deferred<graph_remove_edges_t> remove_edges(
    edges_type_ edges,
    ustore_durability_t durability = ustore_durability_buffered_k) const noexcept;

Parameters

edgesinEdges to remove, owned by the call until it is final.
durabilityinWhether the call finishes only after the data reaches the disk.
write_verticesWrites vertices without edges, keeping those already present.
graph_collection.hpp:590 ↗
template <owned_array_concept<ustore_key_t> vertices_type_>
deferred<graph_write_vertices_t> write_vertices(
    vertices_type_ vertices,
    ustore_durability_t durability = ustore_durability_buffered_k) const noexcept;

Parameters

verticesinVertices to write, owned by the call until it is final.
durabilityinWhether the call finishes only after the data reaches the disk.
remove_verticesRemoves vertices together with every edge they belong to.
graph_collection.hpp:615 ↗
template <owned_array_concept<ustore_key_t> vertices_type_>
deferred<graph_remove_vertices_t> remove_vertices(
    vertices_type_ vertices,
    ustore_vertex_role_t role = ustore_vertex_role_any_k,
    ustore_durability_t durability = ustore_durability_buffered_k) const noexcept;

Parameters

verticesinVertices to remove, owned by the call until it is final.
roleinRole every vertex plays, which only joining graphs need.
durabilityinWhether the call finishes only after the data reaches the disk.
Classdeferred_snapshot_tA snapshot whose verbs submit to an executor and answer deferred calls, reading only, as snapshot_ref_t::deferred hands it out.4
db.hpp:358 ↗
class deferred_snapshot_t
deferred_snapshot_t
db.hpp:362 ↗
deferred_snapshot_t(receiver_t receiver) noexcept;
findThe collection named name at the snapshot, as the deferred twin of the read-only twin of collection_type_.
db.hpp:367 ↗
template <typename collection_type_ = blobs_collection_t>
expected<typename collection_type_::read_only_t::deferred_t> find(
    std::string_view name) const noexcept;
saveSaves the snapshot as a new database at path, which the call copies, reporting the bytes saved to progress, borrowed until the call is final.
db.hpp:377 ↗
deferred<save_t> save(
    std::string_view path,
    progress_arg_t progress = {}) const noexcept;
flushMakes every commit that returned before this call durable.
db.hpp:391 ↗
deferred<flush_t> flush() const noexcept;
Classdeferred_transaction_tA transaction whose verbs submit to an executor and answer deferred calls, as transaction_ref_t::deferred hands it out.6
db.hpp:294 ↗
class deferred_transaction_t

A transaction serves one call at a time, so a caller waits on one deferred call before submitting the next.

deferred_transaction_t
db.hpp:298 ↗
deferred_transaction_t(receiver_t receiver) noexcept;
findThe collection named name, read and written through the transaction, as the deferred twin of the read-only twin of collection_type_.
db.hpp:303 ↗
template <typename collection_type_ = blobs_collection_t>
expected<typename collection_type_::deferred_t> find(
    std::string_view name) const noexcept;
resetClears the transaction for reuse, keeping its memory and starting a new generation.
db.hpp:312 ↗
deferred<reset_t> reset() const noexcept;
stageStages the transaction as the first phase of a two-phase commit, answering the stamp it reads at.
db.hpp:323 ↗
deferred<stage_t> stage() const noexcept;
commitCommits every update of the transaction, or none of them, answering the commit's stamp.
db.hpp:334 ↗
deferred<commit_t> commit(
    ustore_durability_t durability = ustore_durability_buffered_k) const noexcept;
rollbackWithdraws a stage, keeping the reads and writes.
db.hpp:346 ↗
deferred<rollback_t> rollback() const noexcept;
Classexecutor_handle_tOwns an executor, the threads or I/O rings that run calls on any number of databases, and frees it once drained.8
types.hpp:223 ↗
class executor_handle_t

It never copies, and outlives every call submitted to it.

executor_handle_t
types.hpp:239 ↗
executor_handle_t() noexcept=default;
executor_handle_t(executor_handle_t const &)=delete;
executor_handle_t(executor_handle_t && other) noexcept;
operator=
types.hpp:241 ↗
executor_handle_t & operator=(executor_handle_t const &)=delete;
executor_handle_t & operator=(executor_handle_t && other) noexcept;
~executor_handle_t
types.hpp:247 ↗
~executor_handle_t() noexcept;
drainWaits for every call submitted before the drain, leaving the threads in mood whenever they go idle.
types.hpp:274 ↗
status_t drain(ustore_mood_t mood = ustore_mood_parked_k) noexcept;
for_ramSpawns host threads for the RAM, RocksDB and LevelDB engines.
types.hpp:252 ↗
expected<executor_handle_t> for_ram(
    ustore_executor_for_ram_t options = {}) noexcept;
on_poolBorrows the ForkUnion pool options name for the RAM, RocksDB and LevelDB engines.
types.hpp:257 ↗
expected<executor_handle_t> on_pool(
    ustore_executor_on_pool_t options) noexcept;
for_nvmeSpawns threads for the NVMe engine, each owning its own I/O ring, which tell the databases opened on them how to move bytes and borrow the VRAM executor options name for compactions.
types.hpp:263 ↗
expected<executor_handle_t> for_nvme(
    ustore_executor_for_nvme_t options = {}) noexcept;
for_vramSpawns one host thread driving the NVIDIA or AMD device options name by its UStore ordinal, on which an NVMe database compacts when the device is NVIDIA.
types.hpp:269 ↗
expected<executor_handle_t> for_vram(
    ustore_executor_for_vram_t options = {}) noexcept;

Functions

transform_nWrites the first count transformed elements into output.
ranges.hpp:849 ↗
template <typename output_iterator_type_, typename iterator_type_, typename transform_type_ = std::identity>
void transform_n(
    iterator_type_ begin,
    std::size_t count,
    output_iterator_type_ output,
    transform_type_ transform = {});
all_ascendingWhether the first count elements strictly ascend.
ranges.hpp:856 ↗
template <typename iterator_type_>
bool all_ascending(iterator_type_ begin, std::size_t count);
sort_and_deduplicateSorts the elements, gathers the distinct ones at the front, and returns how many there are.
ranges.hpp:865 ↗
template <std::random_access_iterator iterator_type_>
requires std::sortable<iterator_type_>
std::size_t sort_and_deduplicate(
    iterator_type_ begin,
    iterator_type_ end);
in_one_off_arenaRuns verb on an arena made for it alone where executor computes, as shorthand with no place for an arena does, answering a scalar copied out of that arena or a view that owns it.
types.hpp:175 ↗
template <typename verb_type_>
auto in_one_off_arena(
    ustore_executor_t executor,
    verb_type_ && verb) noexcept;
TypesMember types3
deferred::verb_tverb_type_
deferred::arguments_ttypename verb_type_::arguments_t
deferred::answer_ttypename verb_type_::answer_t