aboutsummaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-05-13 01:59:22 -0300
committerGitHub <noreply@github.com>2026-05-13 01:59:22 -0300
commitaf6073dedffd1a16aff7a283ded50545dc40b179 (patch)
treea4b8fbfb4842f5ac27bf7fa641555de3fac6b7f3 /include
parentc1b70cfe745d935bded3053fba8c1f9573aa07aa (diff)
downloadRNT-af6073dedffd1a16aff7a283ded50545dc40b179.tar.gz
RNT-af6073dedffd1a16aff7a283ded50545dc40b179.zip
Extern handles for embedded runtime
Diffstat (limited to '')
-rw-r--r--include/CursorManager.h40
-rw-r--r--include/IStorageBackend.h51
-rw-r--r--include/InMemoryBackend.h22
-rw-r--r--include/Merkle.h164
-rw-r--r--include/ObjectManager.h27
-rw-r--r--include/RNT_C_API.h378
-rw-r--r--include/SqliteBackend.h23
-rw-r--r--include/Types.h28
8 files changed, 649 insertions, 84 deletions
diff --git a/include/CursorManager.h b/include/CursorManager.h
index 092229c..839bf5e 100644
--- a/include/CursorManager.h
+++ b/include/CursorManager.h
@@ -6,6 +6,7 @@
#include "Types.h"
#include <cstddef>
+#include <string>
#include <vector>
/**
@@ -25,6 +26,11 @@ namespace nt
* Tuples are never loaded all at once. Open() fetches the first page;
* each subsequent Next() call transparently fetches the next page from
* the backend once the current page is exhausted.
+ *
+ * Pagination is driven by the Merkle B-tree: each cursor carries the hex
+ * hash of the relation's Merkle root at open time, and LoadPage calls
+ * Merkle::Page(backend, merkle_root, fetch_offset, PAGE_SIZE) to retrieve
+ * the next page of tuple hashes without loading sibling subtrees.
*/
class NT_API CursorManager
{
@@ -43,10 +49,19 @@ namespace nt
*
* The pointer returned by Next() is valid only until the next Next()
* or Close() call — a page replacement invalidates all prior pointers.
+ *
+ * merkle_root is seeded from ObjectManager::Relation::merkle_root at
+ * open time and does not change during iteration; the cursor reads the
+ * snapshot captured when it was opened.
*/
struct cursor {
HandlerManager::handle* handle = nullptr;
/**
+ * Hex hash of the relation's Merkle B-tree root node.
+ * Empty string means the relation contains no tuples.
+ */
+ std::string merkle_root;
+ /**
* Bound argument values for EPHEMERAL_RELATION cursors.
* Written by the JOIN operator before each probe; ignored for
* stored RELATION cursors.
@@ -54,17 +69,24 @@ namespace nt
std::vector<std::string> args;
std::vector<Tuple> page;
std::size_t page_position = 0;
- std::size_t fetch_offset = 0; // next TupleHashes() call starts here
+ std::size_t fetch_offset = 0; // next Merkle::Page() call starts here
bool exhausted = false;
};
/**
* @brief Opens a cursor on the relation referenced by the handle.
*
- * Fetches the first page immediately. Returns nullptr if the relation
- * has no tuples or the handle does not refer to a RELATION.
+ * Fetches the first page immediately for non-empty relations. Returns
+ * a valid exhausted cursor when @p merkle_root is empty (empty relation).
+ * Returns nullptr when the handle does not refer to a RELATION or
+ * EPHEMERAL_RELATION.
+ *
+ * @param handle Open RELATION or EPHEMERAL_RELATION handle.
+ * @param merkle_root Hex hash of the relation's Merkle root at open
+ * time. Ignored for EPHEMERAL_RELATION handles.
*/
- cursor* Open(HandlerManager::handle* handle);
+ cursor* Open(HandlerManager::handle* handle,
+ const std::string& merkle_root = "");
/**
* @brief Pulls the next tuple from the cursor.
@@ -83,8 +105,12 @@ namespace nt
private:
IStorageBackend& backend_;
- /** Fetches a page of hashes, resolves each from the KV store, and
- * deserializes them into cursor->page. Clears the existing page. */
- void LoadPage(cursor* c, const std::vector<std::string>& path);
+ /**
+ * Fetches a page of hashes via Merkle::Page, resolves each from the
+ * KV store, and deserializes them into cursor->page. Clears the
+ * existing page. Advances fetch_offset by the number of hashes
+ * returned; sets exhausted when fewer than PAGE_SIZE hashes come back.
+ */
+ void LoadPage(cursor* c);
};
}
diff --git a/include/IStorageBackend.h b/include/IStorageBackend.h
index 884af2d..e408c95 100644
--- a/include/IStorageBackend.h
+++ b/include/IStorageBackend.h
@@ -7,27 +7,27 @@
/**
* @file IStorageBackend.h
- * @brief Abstract KV storage backend consumed by CursorManager.
+ * @brief Abstract KV storage backend consumed by CursorManager and Merkle.
*
* Storage is strictly content-addressed: every value is stored under its
- * SHA256 hex digest. This mirrors the Sakura physical layer in which the
- * key is always `Sha256.to_hex (Sha256.string serialized_bytes)` and the
- * value is the raw serialized bytes.
- *
- * Relation membership (which tuple hashes belong to a given relation) is
- * tracked via a separate index managed by LinkTuple / TupleHashes. This
- * corresponds to Sakura's B+ tree of tuple hashes stored under each relation.
+ * SHA256 hex digest. Relation membership (which tuple hashes belong to a
+ * given relation) is now tracked via the Merkle B-tree maintained by the
+ * Merkle class, not by this interface. Only Put and Get are required.
*/
namespace nt
{
/**
* @interface IStorageBackend
- * @brief Contract between CursorManager and a physical (or mock) storage engine.
+ * @brief Contract between CursorManager / Merkle and a physical storage engine.
+ *
+ * Two primitives are sufficient:
+ * - Put: store bytes, get back the content-address (SHA256 hex).
+ * - Get: retrieve bytes by content-address.
*
- * Storage is strictly content-addressed: every value lives under its own
- * SHA256 digest. Relation membership is tracked separately via
- * LinkTuple / TupleHashes, mirroring Sakura's per-relation B+ tree.
+ * Both are idempotent. The Merkle class stores node blobs via Put/Get and
+ * maintains insertion-order-independent relation membership on top of these
+ * two primitives.
*/
class IStorageBackend
{
@@ -38,7 +38,7 @@ namespace nt
* @brief Stores a serialized value and returns its SHA256 hex hash.
*
* Idempotent: storing the same bytes twice does not create a duplicate.
- * The hash is the only valid key for subsequent Get calls.
+ * The returned hash is the only valid key for subsequent Get calls.
*/
virtual std::string Put(std::vector<uint8_t> value) = 0;
@@ -47,30 +47,5 @@ namespace nt
* @return The stored bytes, or std::nullopt when the hash is unknown.
*/
virtual std::optional<std::vector<uint8_t>> Get(const std::string& hash) = 0;
-
- /**
- * @brief Records that a tuple (by hash) belongs to a relation (by path).
- *
- * Idempotent. Corresponds to appending a tuple hash to a relation's
- * B+ tree in Sakura's physical layer.
- */
- virtual void LinkTuple(const std::vector<std::string>& relation_path,
- const std::string& tuple_hash) = 0;
-
- /**
- * @brief Returns a page of tuple hashes for a relation.
- *
- * Pagination is tuple-level: @p offset and @p limit count whole tuples.
- * Hashes are returned in insertion order.
- *
- * @param relation_path Logical relation path.
- * @param offset Zero-based index of the first tuple to return.
- * @param limit Maximum number of tuple hashes to return.
- * @return Ordered list of SHA256 hex hashes. Empty when past the end.
- */
- virtual std::vector<std::string> TupleHashes(
- const std::vector<std::string>& relation_path,
- std::size_t offset,
- std::size_t limit) = 0;
};
}
diff --git a/include/InMemoryBackend.h b/include/InMemoryBackend.h
index 8f7f50a..55472a8 100644
--- a/include/InMemoryBackend.h
+++ b/include/InMemoryBackend.h
@@ -11,33 +11,23 @@
/**
* @file InMemoryBackend.h
- * @brief In-process KV storage backend backed by unordered_maps.
+ * @brief In-process KV storage backend backed by an unordered_map.
+ *
+ * Intended for unit tests where SQLite is not appropriate. Data does not
+ * survive process exit and is not thread-safe.
+ *
+ * Relation membership is tracked by the Merkle class, not this backend.
*/
namespace nt
{
- /**
- * @interface InMemoryBackend
- * @brief IStorageBackend implementation that keeps all data in process memory.
- *
- * Intended for unit tests where SQLite is not appropriate. Data does not
- * survive process exit and is not thread-safe.
- */
class NT_API InMemoryBackend : public IStorageBackend
{
public:
std::string Put(std::vector<uint8_t> value) override;
std::optional<std::vector<uint8_t>> Get(const std::string& hash) override;
- void LinkTuple(const std::vector<std::string>& relation_path,
- const std::string& tuple_hash) override;
- std::vector<std::string> TupleHashes(const std::vector<std::string>& relation_path,
- std::size_t offset,
- std::size_t limit) override;
private:
std::unordered_map<std::string, std::vector<uint8_t>> kv_;
- std::unordered_map<std::string, std::vector<std::string>> index_;
-
- static std::string PathKey(const std::vector<std::string>& path);
};
}
diff --git a/include/Merkle.h b/include/Merkle.h
new file mode 100644
index 0000000..8380093
--- /dev/null
+++ b/include/Merkle.h
@@ -0,0 +1,164 @@
+#pragma once
+
+#include "IStorageBackend.h"
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+/**
+ * @file Merkle.h
+ * @brief Sorted content-addressed B-tree over SHA-256 tuple hash hex strings.
+ *
+ * Each node is stored as an immutable blob in IStorageBackend via Put/Get.
+ * All three operations load only the nodes on the path from root to the target
+ * leaf — O(log_B(n)) nodes in memory at once, never the whole tree.
+ *
+ * **Branching factor B = 64**: leaves hold up to 64 hashes; internal nodes hold
+ * up to 64 child entries. At B=64 and 1 billion tuples the tree is at most
+ * 5 levels deep (~20 KB peak working set).
+ *
+ * **Immutability**: nodes are never overwritten once written (content-addressed).
+ * Insert and Remove produce new nodes only along the modified path; old nodes
+ * remain valid for any snapshot that still holds their root hash.
+ *
+ * **Internal node entry layout** (leaf_count | min_hash | child_hash):
+ * - `leaf_count` enables O(log_B(n) + limit) offset navigation for Page()
+ * without loading sibling subtrees.
+ * - `min_hash` (smallest tuple hash in the subtree) is the routing key.
+ * - `child_hash` is the content-addressed hash of the child node blob.
+ *
+ * **Invariants** (asserted in debug builds):
+ * - No leaf or internal node is ever empty.
+ * - Internal node entries are sorted ascending by min_hash.
+ * - All public entry points that accept a hex hash throw `std::invalid_argument`
+ * when the string is not exactly 64 lowercase hex characters.
+ *
+ * @todo Add bounds checking in decode_leaf / decode_internal: validate that the
+ * reported entry count does not exceed the actual blob size before reading.
+ * Currently a truncated or corrupt blob causes out-of-bounds reads.
+ */
+
+namespace nt {
+
+class Merkle {
+public:
+ static constexpr size_t B = 64;
+
+ /**
+ * @brief Insert a tuple hash into the tree.
+ * @param store KV backend for node serialization.
+ * @param root_hex Current root hash (empty string = empty tree).
+ * @param hash_hex 64-char hex SHA-256 of the tuple to insert.
+ * @return New root hash. Equal to root_hex when the hash was already present.
+ */
+ static std::string Insert(IStorageBackend& store,
+ const std::string& root_hex,
+ const std::string& hash_hex);
+
+ /**
+ * @brief Remove a tuple hash from the tree.
+ * @param store KV backend.
+ * @param root_hex Current root hash.
+ * @param hash_hex 64-char hex SHA-256 of the tuple to remove.
+ * @return New root hash. Empty string when the tree becomes empty.
+ */
+ static std::string Remove(IStorageBackend& store,
+ const std::string& root_hex,
+ const std::string& hash_hex);
+
+ /**
+ * @brief Page through tuple hashes in sorted order.
+ *
+ * Descends to the leaf at logical position @p offset using cumulative
+ * leaf_counts stored in internal nodes, then walks forward collecting at
+ * most @p limit hashes. Only the nodes on the active path are loaded;
+ * sibling subtrees are never touched.
+ *
+ * @param store KV backend.
+ * @param root_hex Current root hash.
+ * @param offset Zero-based logical tuple offset.
+ * @param limit Maximum number of hashes to return.
+ * @return Hex hash strings in sorted order. Empty when past the end.
+ */
+ static std::vector<std::string> Page(IStorageBackend& store,
+ const std::string& root_hex,
+ size_t offset,
+ size_t limit);
+
+private:
+ using Hash32 = std::array<uint8_t, 32>;
+
+ struct LeafNode {
+ std::vector<Hash32> hashes; // sorted ascending, len ≤ B
+ };
+
+ // Wire: 8-byte leaf_count | 32-byte min_hash | 32-byte child_hash
+ struct ChildEntry {
+ uint64_t leaf_count; // number of tuple hashes in this subtree
+ Hash32 min_hash; // smallest tuple hash in the subtree
+ Hash32 child_hash; // content-addressed hash of the child node
+ };
+
+ struct InternalNode {
+ std::vector<ChildEntry> entries; // sorted by min_hash, len ≤ B
+ };
+
+ // Wire format
+ static std::vector<uint8_t> encode_leaf(const LeafNode& n);
+ static std::vector<uint8_t> encode_internal(const InternalNode& n);
+ static LeafNode decode_leaf(const std::vector<uint8_t>& bytes);
+ static InternalNode decode_internal(const std::vector<uint8_t>& bytes);
+ static bool is_leaf_bytes(const std::vector<uint8_t>& bytes);
+
+ // Hex / binary
+ static Hash32 hex_to_bin(const std::string& hex);
+ static std::string bin_to_hex(const Hash32& bin);
+
+ // Storage helpers
+ static std::vector<uint8_t> load_node(IStorageBackend& store,
+ const std::string& hash_hex);
+ static std::string store_leaf(IStorageBackend& store, const LeafNode& n);
+ static std::string store_internal(IStorageBackend& store, const InternalNode& n);
+
+ // Returns the minimum hash of the subtree rooted at node_hex.
+ static Hash32 subtree_min(IStorageBackend& store, const std::string& node_hex);
+
+ // Recursive insert result: new node hash, subtree leaf count, optional split.
+ struct InsertResult {
+ std::string new_hash;
+ uint64_t leaf_count = 0;
+ bool did_split = false;
+ uint64_t split_leaf_count = 0;
+ Hash32 split_min_hash = {};
+ std::string split_hash;
+ };
+
+ static InsertResult insert_into(IStorageBackend& store,
+ const std::string& node_hex,
+ const Hash32& tuple_hash);
+
+ // Recursive remove result: new node hash (empty = node gone), leaf count.
+ struct RemoveResult {
+ std::string new_hash;
+ uint64_t leaf_count = 0;
+ };
+
+ static RemoveResult remove_from(IStorageBackend& store,
+ const std::string& node_hex,
+ const Hash32& tuple_hash);
+
+ // Page collection — offset consumed as we descend.
+ static void page_from(IStorageBackend& store,
+ const std::string& node_hex,
+ size_t& offset,
+ size_t limit,
+ std::vector<Hash32>& out);
+
+ // Find which child to route into (rightmost whose min_hash ≤ target).
+ static size_t route(const InternalNode& node, const Hash32& target);
+};
+
+} // namespace nt
diff --git a/include/ObjectManager.h b/include/ObjectManager.h
index 32b3ae9..e30ff1b 100644
--- a/include/ObjectManager.h
+++ b/include/ObjectManager.h
@@ -35,12 +35,35 @@ namespace nt
/** @brief Abstract registry object representing a multigroup. */
class Multigroup : public IObject {};
- /** @brief Abstract registry object representing a relation. */
- class Relation : public IObject {};
+ /**
+ * @brief Registry object representing a stored relation.
+ *
+ * merkle_root is the hex hash of the Merkle B-tree root node stored in
+ * the KV backend. Empty string means the relation contains no tuples.
+ * Updated atomically by rnt_link_tuple / rnt_unlink_tuple / rnt_clear_relation.
+ */
+ class Relation : public IObject {
+ public:
+ std::string merkle_root;
+ };
/** @brief Abstract registry object representing a transaction. */
class Transaction : public IObject {};
/**
+ * @brief A named mutable reference to a multigroup state.
+ *
+ * The payload field carries the serialized multigroup bytes in whatever
+ * format the caller registered them with. The C API returns these bytes
+ * verbatim when a handle to the branch is opened; Sakura is responsible
+ * for deserializing them into a Multigroup object.
+ */
+ class Branch : public IObject {
+ public:
+ std::string name;
+ std::vector<uint8_t> payload;
+ };
+
+ /**
* @brief Describes the behavior and capabilities of an object category.
*
* @todo Replace `methods` with typed callback fields: `OpenProcedure`,
diff --git a/include/RNT_C_API.h b/include/RNT_C_API.h
new file mode 100644
index 0000000..ddf6a97
--- /dev/null
+++ b/include/RNT_C_API.h
@@ -0,0 +1,378 @@
+#pragma once
+
+/**
+ * @file RNT_C_API.h
+ * @brief C-callable surface over the RNT manager pipeline for OCaml ctypes.
+ *
+ * All types are opaque void pointers on the C side; the implementation casts
+ * them to the appropriate C++ types internally. Callers must never dereference
+ * handle or cursor pointers directly.
+ *
+ * ## Memory contract
+ * - Strings returned via out-parameters (char**) are heap-allocated by the
+ * API and must be released with rnt_free_string().
+ * - Payloads returned via (uint8_t**, size_t*) are heap-allocated and must
+ * be released with rnt_free_bytes().
+ * - Handles and cursors are owned by the caller and must be closed before
+ * the program exits.
+ *
+ * ## Error convention
+ * - Functions returning int use 0 for success and a negative value for error.
+ * - Functions returning a pointer return NULL on failure.
+ *
+ * ## Thread safety
+ * This API is **not thread-safe**. The global runtime (g_rt) is a single
+ * in-process instance; all callers share the same ObjectManager and storage
+ * backend. The expected usage model is a single OCaml thread (or domain)
+ * driving the API at a time. If concurrent access is needed in the future,
+ * a per-object or per-relation mutex strategy should be introduced.
+ *
+ * @todo Implement AUTH_CLAIM::READ/WRITE enforcement in rnt_open_handle once
+ * PermissionsManager::Access is wired to a real policy engine. Currently
+ * all handles open with full access regardless of the claims parameter.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Opaque handle to a registry object opened through HandlerManager. */
+typedef void* rnt_handle_t;
+
+/** Opaque cursor over a relation, driven by the VM or used directly. */
+typedef void* rnt_cursor_t;
+
+/* ------------------------------------------------------------------ */
+/* Runtime lifecycle */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Initialises the RNT runtime with the selected storage backend.
+ *
+ * Must be called before any other API function. Once the runtime is
+ * successfully initialised, subsequent calls are no-ops and return 0.
+ * If initialisation fails (returns negative), the call may be retried
+ * with corrected parameters — the runtime is left in a clean state.
+ *
+ * @param driver Storage driver to use: "sqlite" or "memory".
+ * "sqlite" persists data to @p storage_path.
+ * "memory" keeps all data in process memory (ignores
+ * @p storage_path); intended for tests.
+ * @param storage_path File path for the SQLite database, or ":memory:" for
+ * an ephemeral SQLite store. Ignored when driver is
+ * "memory".
+ * @return 0 on success, negative on error.
+ */
+int rnt_init(const char* driver, const char* storage_path);
+
+/* ------------------------------------------------------------------ */
+/* Authentication */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Runs PermissionsManager::Firewall for a connection.
+ *
+ * @param auth_method "plain_text" or "certificate".
+ * @param claims_out Set to a heap-allocated, newline-separated claim string.
+ * Release with rnt_free_string(). NULL on error.
+ * @return 0 on success, negative when authentication is rejected.
+ */
+int rnt_firewall(const char* auth_method, char** claims_out);
+
+/* ------------------------------------------------------------------ */
+/* Handle lifecycle */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Opens a handle to the object at the given slash-separated path.
+ *
+ * Runs the full HandlerManager::Open pipeline:
+ * ObjectManager::Find → PermissionsManager::Access →
+ * IdentityManager::CanOpen → LifecycleManager::Contention →
+ * LifecycleManager::Monitor.
+ *
+ * @param path Slash-separated logical path, e.g. "/system/branches/main".
+ * @param claims Claim string returned by rnt_firewall (may be NULL).
+ * @return Handle pointer, or NULL when the path is not found or access is denied.
+ */
+rnt_handle_t rnt_open_handle(const char* path, const char* claims);
+
+/**
+ * @brief Closes a handle, running HandlerManager::Close and Unmonitor.
+ * @return 0 on success, negative on error.
+ */
+int rnt_close_handle(rnt_handle_t handle);
+
+/**
+ * @brief Reads the raw payload bytes stored in a BRANCH object.
+ *
+ * Only valid for handles opened on BRANCH objects. The caller uses the bytes
+ * to reconstruct the in-memory multigroup (e.g. via Sakura's deserializer).
+ *
+ * @param handle Handle to a BRANCH object.
+ * @param payload_out Set to a heap-allocated copy of the payload bytes.
+ * Release with rnt_free_bytes().
+ * @param len_out Set to the number of bytes in payload_out.
+ * @return 0 on success, negative when the handle is not a BRANCH.
+ */
+int rnt_branch_payload(rnt_handle_t handle,
+ uint8_t** payload_out,
+ size_t* len_out);
+
+/**
+ * @brief Writes new payload bytes into the BRANCH object referenced by handle.
+ *
+ * Updates the in-memory branch object. Callers are responsible for persisting
+ * the multigroup state to the storage backend separately via tuple link
+ * operations.
+ *
+ * Write exclusion is structural: BRANCH objects carry @c exclusive=true in
+ * their object_type, so LifecycleManager::Contention prevents a second handle
+ * from being opened while a writer holds the branch. AUTH_CLAIM::WRITE is not
+ * checked at the C API boundary.
+ *
+ * @param handle Open BRANCH handle.
+ * @param payload New serialized multigroup bytes.
+ * @param len Number of bytes in payload.
+ * @return 0 on success, negative on error.
+ */
+int rnt_branch_set_payload(rnt_handle_t handle,
+ const uint8_t* payload,
+ size_t len);
+
+/* ------------------------------------------------------------------ */
+/* Object registration */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Registers a RELATION object at the given path.
+ *
+ * Idempotent: if an object already exists at the path, the call succeeds
+ * without re-registering.
+ *
+ * @param path Slash-separated path, e.g. "/system/branches/main/relations/foo".
+ * @return 0 on success, negative on error.
+ */
+int rnt_register_relation(const char* path);
+
+/**
+ * @brief Registers a BRANCH object at the given path with an initial payload.
+ *
+ * If a BRANCH already exists at this path, returns 0 without modifying it.
+ *
+ * @param path Slash-separated path, e.g. "/system/branches/main".
+ * @param payload Initial serialized multigroup bytes (may be NULL for empty).
+ * @param payload_len Byte count of payload.
+ * @return 0 on success, negative on error.
+ */
+int rnt_register_branch(const char* path,
+ const uint8_t* payload,
+ size_t payload_len);
+
+/* ------------------------------------------------------------------ */
+/* Tuple storage */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Stores a tuple and links it to the relation at the given path.
+ *
+ * Tuples are encoded as a flat key=value string, one attribute per line:
+ * "name=Blathers\nprofession=Museum Curator\n"
+ * Attributes are sorted by name before hashing to ensure content-addressing
+ * is order-independent.
+ *
+ * @param relation_path Slash-separated relation path.
+ * @param kv_attrs Newline-delimited "key=value" attribute string.
+ * @param hash_out Set to the 64-character hex SHA-256 hash of the tuple.
+ * Release with rnt_free_string(). NULL on error.
+ * @return 0 on success, negative on error.
+ */
+int rnt_link_tuple(const char* relation_path,
+ const char* kv_attrs,
+ char** hash_out);
+
+/**
+ * @brief Removes a tuple from the relation's Merkle tree and tuple store.
+ *
+ * Calls Merkle::Remove on the relation's current root and updates
+ * ObjectManager::Relation::merkle_root atomically. The tuple bytes remain in
+ * the KV store (they may be referenced by older snapshots); only the
+ * membership in the current tree is removed.
+ *
+ * @param relation_path Slash-separated relation path.
+ * @param tuple_hash 64-character hex SHA-256 of the tuple to remove.
+ * @return 0 on success, negative when the relation is not found.
+ */
+int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash);
+
+/**
+ * @brief Resets a relation's Merkle root to the empty-tree state.
+ *
+ * Sets ObjectManager::Relation::merkle_root to the empty string. Existing
+ * tuple bytes remain in the KV store. After this call the relation contains
+ * no tuples from the perspective of cursor iteration.
+ *
+ * @param relation_path Slash-separated relation path.
+ * @return 0 on success, negative when the relation is not found.
+ */
+int rnt_clear_relation(const char* relation_path);
+
+/**
+ * @brief Returns the current Merkle root hash for a relation.
+ *
+ * OCaml calls this after rnt_link_tuple or rnt_unlink_tuple to read the
+ * updated root back and store it in the in-memory multigroup (tree_pointer).
+ * An empty string is returned for a relation that contains no tuples.
+ *
+ * @param relation_path Slash-separated relation path.
+ * @param root_hash_out Set to the 64-character hex root hash, or to an empty
+ * heap-allocated string for an empty relation.
+ * Release with rnt_free_string().
+ * @return 0 on success, negative when the relation is not found.
+ */
+int rnt_relation_root(const char* relation_path, char** root_hash_out);
+
+/**
+ * @brief Writes a Merkle root hash into a relation's ObjectManager entry.
+ *
+ * Called by Sakura's open_branch during multigroup reconstruction: after
+ * deserializing the branch payload, OCaml calls this function once per
+ * relation to restore the persisted Merkle root into the in-memory
+ * ObjectManager so that subsequent cursors see the correct tuple set.
+ *
+ * @param relation_path Slash-separated relation path.
+ * @param root_hash 64-character hex Merkle root, or empty string to mark
+ * the relation as empty.
+ * @return 0 on success, negative when the relation is not found.
+ */
+int rnt_set_relation_root(const char* relation_path, const char* root_hash);
+
+/* ------------------------------------------------------------------ */
+/* Cursor and VM */
+/* ------------------------------------------------------------------ */
+
+/**
+ * @brief Opens a cursor on the relation referenced by handle.
+ *
+ * The returned cursor is positioned before the first tuple. Advance it with
+ * rnt_cursor_next(). The cursor must be closed with rnt_cursor_close() before
+ * closing the handle.
+ *
+ * @param handle Open RELATION handle.
+ * @return Cursor pointer, or NULL on error.
+ */
+rnt_cursor_t rnt_cursor_open(rnt_handle_t handle);
+
+/**
+ * @brief Advances the cursor and returns the next tuple as a kv string.
+ *
+ * The tuple is encoded as a newline-delimited "key=value" string matching the
+ * format accepted by rnt_link_tuple:
+ * "name=Blathers\nprofession=Museum Curator\n"
+ *
+ * @param cursor Open cursor.
+ * @param tuple_out Set to a heap-allocated kv string, or NULL when exhausted.
+ * Release with rnt_free_string() when non-NULL.
+ * @return 1 when a tuple was returned, 0 when exhausted, negative on error.
+ */
+int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out);
+
+/**
+ * @brief Closes a cursor and releases its resources.
+ * @return 0 on success, negative on error.
+ */
+int rnt_cursor_close(rnt_cursor_t cursor);
+
+/* ------------------------------------------------------------------ */
+/* VM plan builder */
+/* ------------------------------------------------------------------ */
+
+/** Opaque plan node tree, built incrementally via rnt_plan_* calls. */
+typedef void* rnt_plan_t;
+
+/**
+ * @brief Creates a SCAN plan node that reads all tuples from a stored relation.
+ *
+ * Opens a RELATION handle and cursor internally. Both are transferred to the
+ * plan and released when the resulting VM cursor is closed.
+ *
+ * @param relation_path Absolute slash-separated path to the relation, e.g.
+ * "/system/branches/main/relations/public:users".
+ * @return Plan node, or NULL when the relation does not exist or cannot be opened.
+ */
+rnt_plan_t rnt_plan_scan(const char* relation_path);
+
+/**
+ * @brief Creates a nested-loop JOIN plan node.
+ *
+ * Takes ownership of both @p left and @p right. On success the caller must not
+ * free either child; they are released when the returned plan is freed or
+ * executed. On failure (NULL return) both children are freed.
+ *
+ * @return Plan node, or NULL on error.
+ */
+rnt_plan_t rnt_plan_join(rnt_plan_t left, rnt_plan_t right);
+
+/**
+ * @brief Creates a TAKE plan node that limits output to at most @p limit tuples.
+ *
+ * Takes ownership of @p source. On failure @p source is freed.
+ *
+ * @return Plan node, or NULL on error.
+ */
+rnt_plan_t rnt_plan_take(rnt_plan_t source, size_t limit);
+
+/**
+ * @brief Releases a plan that was built but not yet executed.
+ *
+ * Closes any open cursors and handles owned by the plan tree, then frees all
+ * nodes. Safe to call with NULL. Do NOT call after rnt_vm_execute_plan —
+ * that function takes ownership.
+ */
+void rnt_plan_free(rnt_plan_t plan);
+
+/**
+ * @brief Executes a plan tree and returns a streaming VM cursor.
+ *
+ * Takes ownership of @p plan; the caller must not call rnt_plan_free after this.
+ * The returned cursor is advanced with rnt_vm_cursor_next() and must be closed
+ * with rnt_vm_cursor_close(), which also releases all plan resources.
+ *
+ * @return VM cursor, or NULL when the plan is NULL.
+ */
+rnt_cursor_t rnt_vm_execute_plan(rnt_plan_t plan);
+
+/**
+ * @brief Advances a VM cursor and returns the next merged tuple as a kv string.
+ *
+ * Same encoding and return-value semantics as rnt_cursor_next.
+ *
+ * @param vm_cursor Cursor returned by rnt_vm_execute_plan.
+ * @param tuple_out Set to a heap-allocated kv string, or NULL when exhausted.
+ * Release with rnt_free_string() when non-NULL.
+ * @return 1 when a tuple was returned, 0 when exhausted, negative on error.
+ */
+int rnt_vm_cursor_next(rnt_cursor_t vm_cursor, char** tuple_out);
+
+/**
+ * @brief Closes a VM cursor, releases all plan nodes, cursors, and handles.
+ * @return 0 on success, negative on error.
+ */
+int rnt_vm_cursor_close(rnt_cursor_t vm_cursor);
+
+/* ------------------------------------------------------------------ */
+/* Memory management */
+/* ------------------------------------------------------------------ */
+
+/** @brief Releases a string allocated by the API. Safe to call with NULL. */
+void rnt_free_string(char* s);
+
+/** @brief Releases a byte buffer allocated by the API. Safe to call with NULL. */
+void rnt_free_bytes(uint8_t* p);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/include/SqliteBackend.h b/include/SqliteBackend.h
index 0b21925..ce4e465 100644
--- a/include/SqliteBackend.h
+++ b/include/SqliteBackend.h
@@ -16,23 +16,13 @@
* kv(hash TEXT PRIMARY KEY, value BLOB NOT NULL)
* — content-addressed store; key is the SHA256 hex digest of value.
*
- * relation_index(path TEXT NOT NULL, tuple_hash TEXT NOT NULL,
- * UNIQUE(path, tuple_hash))
- * — maps a relation's logical path to the ordered set of tuple hashes
- * it contains. Corresponds to Sakura's per-relation B+ tree of
- * tuple hashes stored in the physical layer.
+ * Relation membership is tracked by the Merkle B-tree maintained in-process.
+ * The former relation_index table has been removed; tuple hashes are looked up
+ * via Merkle::Page which reads only the KV store.
*/
namespace nt
{
- /**
- * @class SqliteBackend
- * @brief IStorageBackend that persists data in a SQLite database.
- *
- * Constructed with a file path or ":memory:" for an in-process database.
- * All KV values are opaque blobs; callers are responsible for serialization
- * (see TupleCodec).
- */
class NT_API SqliteBackend : public IStorageBackend
{
public:
@@ -46,15 +36,8 @@ namespace nt
std::string Put(std::vector<uint8_t> value) override;
std::optional<std::vector<uint8_t>> Get(const std::string& hash) override;
- void LinkTuple(const std::vector<std::string>& relation_path,
- const std::string& tuple_hash) override;
- std::vector<std::string> TupleHashes(const std::vector<std::string>& relation_path,
- std::size_t offset,
- std::size_t limit) override;
private:
sqlite3* db_ = nullptr;
-
- static std::string PathKey(const std::vector<std::string>& path);
};
}
diff --git a/include/Types.h b/include/Types.h
index 3e38829..f7eadff 100644
--- a/include/Types.h
+++ b/include/Types.h
@@ -24,7 +24,33 @@ enum OBJECT_TYPE {
* a projected stored relation) or AlephZero (e.g. the eq builtin). The
* object_type for this label must be an ephemeral_object_type.
*/
- EPHEMERAL_RELATION
+ EPHEMERAL_RELATION,
+ /**
+ * A named mutable reference to a multigroup state, analogous to a git
+ * branch. A BRANCH object carries an opaque payload (serialized multigroup
+ * bytes) that the caller — in practice Sakura — deserializes on open.
+ *
+ * Branch objects are the entry point for database connections: a client
+ * opens a handle to /system/branches/<name>, reads the payload bytes to
+ * reconstruct the multigroup in memory, and holds the handle for the
+ * duration of the session.
+ *
+ * The `exclusive` flag on the object_type should be set to true so that
+ * LifecycleManager::Contention serializes concurrent writers.
+ */
+ BRANCH,
+ /**
+ * A named query plan stored in the object registry. A VIEW object carries
+ * the serialized plan definition and the multigroup hash at definition time.
+ * No handles to physical storage are kept open in the VIEW object itself.
+ * Each time the view is opened, fresh relation handles and cursors are
+ * created for the duration of that execution, then released on close.
+ *
+ * @todo Implement ObjectManager::View, rnt_register_view, and the
+ * open-view path in HandlerManager. Views are reserved in the type
+ * system but have no runtime support yet.
+ */
+ VIEW
};
/** @brief Operations that may be supported by an object type. */