aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-06-05 21:40:44 -0300
committerGitHub <noreply@github.com>2026-06-05 21:40:44 -0300
commit5174fc25698cff33b03b5746bf1742e417988f7d (patch)
tree232a3acdd9b458de481dbd5cbde889505168c8e8
parent85ed897de3676af866cc619dc700025820c02201 (diff)
downloadRNT-5174fc25698cff33b03b5746bf1742e417988f7d.tar.gz
RNT-5174fc25698cff33b03b5746bf1742e417988f7d.zip
Project C interface, reroute FOL plan enums & format
-rw-r--r--.clang-format15
-rw-r--r--images/logo-smooth.pngbin0 -> 811390 bytes
-rw-r--r--include/CursorManager.h24
-rw-r--r--include/HandlerManager.h18
-rw-r--r--include/IStorageBackend.h10
-rw-r--r--include/IdentityManager.h10
-rw-r--r--include/InMemoryBackend.h12
-rw-r--r--include/LifecycleManager.h14
-rw-r--r--include/Merkle.h259
-rw-r--r--include/MultigroupCodec.h23
-rw-r--r--include/NT.h12
-rw-r--r--include/NamespaceReferenceManager.h17
-rw-r--r--include/ObjectManager.h38
-rw-r--r--include/PermissionsManager.h23
-rw-r--r--include/RNT_C_API.h82
-rw-r--r--include/Runtime.h10
-rw-r--r--include/SqliteBackend.h12
-rw-r--r--include/TupleCodec.h5
-rw-r--r--include/Types.h21
-rw-r--r--include/VM.h49
-rw-r--r--src/CursorManager.cpp84
-rw-r--r--src/HandlerManager.cpp50
-rw-r--r--src/IdentityManager.cpp11
-rw-r--r--src/InMemoryBackend.cpp14
-rw-r--r--src/LifecycleManager.cpp127
-rw-r--r--src/Merkle.cpp1018
-rw-r--r--src/MultigroupCodec.cpp67
-rw-r--r--src/NamespaceReferenceManager.cpp112
-rw-r--r--src/ObjectManager.cpp26
-rw-r--r--src/PermissionsManager.cpp22
-rw-r--r--src/RNT_C_API.cpp897
-rw-r--r--src/RelationalNT.cpp52
-rw-r--r--src/SqliteBackend.cpp44
-rw-r--r--src/TupleCodec.cpp39
-rw-r--r--src/VM.cpp121
-rw-r--r--test/C_API.cpp141
-rw-r--r--test/CursorManager.cpp86
-rw-r--r--test/LifecycleManager.cpp96
-rw-r--r--test/MultigroupCodec.cpp67
-rw-r--r--test/NamespaceReferenceManager.cpp162
-rw-r--r--test/ObjectManager.cpp33
-rw-r--r--test/VM.cpp159
42 files changed, 1939 insertions, 2143 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..5325526
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,15 @@
+---
+Language: Cpp
+BasedOnStyle: LLVM
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+ColumnLimit: 100
+BreakBeforeBraces: Attach
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+PointerAlignment: Left
+NamespaceIndentation: All
+SortIncludes: true
+...
diff --git a/images/logo-smooth.png b/images/logo-smooth.png
new file mode 100644
index 0000000..3f1a558
--- /dev/null
+++ b/images/logo-smooth.png
Binary files differ
diff --git a/include/CursorManager.h b/include/CursorManager.h
index 2743516..a4c82d7 100644
--- a/include/CursorManager.h
+++ b/include/CursorManager.h
@@ -16,8 +16,7 @@
* @brief Declares paging and cursor orchestration for query execution.
*/
-namespace nt
-{
+namespace nt {
/**
* @class CursorManager
* @brief Orchestrates paginated data retrieval from a storage backend.
@@ -34,9 +33,8 @@ namespace nt
* Merkle::Page(backend, merkle_root, fetch_offset, PAGE_SIZE) to retrieve
* the next page of tuple hashes without loading sibling subtrees.
*/
- class CursorManager
- {
- public:
+ class CursorManager {
+ public:
static constexpr std::size_t PAGE_SIZE = 1;
/**
@@ -49,8 +47,7 @@ namespace nt
* resolved snapshot for the handle's relation and Pins it for the
* cursor's lifetime, releasing the pin in Close.
*/
- explicit CursorManager(IStorageBackend& backend,
- LifecycleManager* lifecycles = nullptr,
+ explicit CursorManager(IStorageBackend& backend, LifecycleManager* lifecycles = nullptr,
ObjectManager* objects = nullptr);
/**
@@ -88,7 +85,7 @@ namespace nt
std::vector<std::string> args;
std::vector<Tuple> page;
std::size_t page_position = 0;
- std::size_t fetch_offset = 0; // next Merkle::Page() call starts here
+ std::size_t fetch_offset = 0; // next Merkle::Page() call starts here
bool exhausted = false;
/**
* True for EPHEMERAL_RELATION cursors; selects the generator-driven
@@ -124,8 +121,7 @@ namespace nt
* @param merkle_root Hex hash of the relation's Merkle root at open
* time. Ignored for EPHEMERAL_RELATION handles.
*/
- cursor* Open(HandlerManager::handle* handle,
- const std::string& merkle_root = "");
+ cursor* Open(HandlerManager::handle* handle, const std::string& merkle_root = "");
/**
* @brief Pulls the next tuple from the cursor.
@@ -141,10 +137,10 @@ namespace nt
*/
void Close(cursor* cursor);
- private:
- IStorageBackend& backend_;
+ private:
+ IStorageBackend& backend_;
LifecycleManager* lifecycles_ = nullptr;
- ObjectManager* objects_ = nullptr;
+ ObjectManager* objects_ = nullptr;
/**
* Fetches a page of hashes via Merkle::Page, resolves each from the
@@ -154,4 +150,4 @@ namespace nt
*/
void LoadPage(cursor* c);
};
-}
+} // namespace nt
diff --git a/include/HandlerManager.h b/include/HandlerManager.h
index a9ce09d..b2beb5f 100644
--- a/include/HandlerManager.h
+++ b/include/HandlerManager.h
@@ -13,17 +13,15 @@
* @brief Declares handle allocation and release for authorized object access.
*/
-namespace nt
-{
+namespace nt {
class IdentityManager;
class LifecycleManager;
class NamespaceReferenceManager;
class PermissionsManager;
/** @brief Opens and closes authorized handles to registry objects. */
- class HandlerManager
- {
- public:
+ class HandlerManager {
+ public:
/**
* @brief Constructs a HandlerManager with shared runtime dependencies.
*
@@ -31,10 +29,8 @@ namespace nt
* counters, permission policy) persists across calls rather than being
* rebuilt per open/close.
*/
- HandlerManager(ObjectManager& objects,
- PermissionsManager& permissions,
- IdentityManager& identities,
- LifecycleManager& lifecycles,
+ HandlerManager(ObjectManager& objects, PermissionsManager& permissions,
+ IdentityManager& identities, LifecycleManager& lifecycles,
NamespaceReferenceManager& references);
/**
@@ -90,11 +86,11 @@ namespace nt
*/
bool Close(struct handle* handle);
- private:
+ private:
ObjectManager& objects_;
PermissionsManager& permissions_;
IdentityManager& identities_;
LifecycleManager& lifecycles_;
NamespaceReferenceManager& references_;
};
-}
+} // namespace nt
diff --git a/include/IStorageBackend.h b/include/IStorageBackend.h
index e408c95..a7cc782 100644
--- a/include/IStorageBackend.h
+++ b/include/IStorageBackend.h
@@ -15,8 +15,7 @@
* Merkle class, not by this interface. Only Put and Get are required.
*/
-namespace nt
-{
+namespace nt {
/**
* @interface IStorageBackend
* @brief Contract between CursorManager / Merkle and a physical storage engine.
@@ -29,9 +28,8 @@ namespace nt
* maintains insertion-order-independent relation membership on top of these
* two primitives.
*/
- class IStorageBackend
- {
- public:
+ class IStorageBackend {
+ public:
virtual ~IStorageBackend() = default;
/**
@@ -48,4 +46,4 @@ namespace nt
*/
virtual std::optional<std::vector<uint8_t>> Get(const std::string& hash) = 0;
};
-}
+} // namespace nt
diff --git a/include/IdentityManager.h b/include/IdentityManager.h
index 6b487a9..1dd75da 100644
--- a/include/IdentityManager.h
+++ b/include/IdentityManager.h
@@ -8,8 +8,7 @@
* @brief Declares object capability checks.
*/
-namespace nt
-{
+namespace nt {
/**
* @brief Checks whether an object supports identity-level operations.
*
@@ -21,9 +20,8 @@ namespace nt
* so that error logs identify the type without dereferencing the pointer.
* See docs/reactos-ob-comparison.md §2.
*/
- class IdentityManager
- {
- public:
+ class IdentityManager {
+ public:
/**
* @brief Returns true when the object's type supports being opened.
*
@@ -46,4 +44,4 @@ namespace nt
*/
const bool CanClose(ObjectManager::registry* object);
};
-}
+} // namespace nt
diff --git a/include/InMemoryBackend.h b/include/InMemoryBackend.h
index 1a7d3a7..b66d355 100644
--- a/include/InMemoryBackend.h
+++ b/include/InMemoryBackend.h
@@ -19,15 +19,13 @@
* Relation membership is tracked by the Merkle class, not this backend.
*/
-namespace nt
-{
- class InMemoryBackend : public IStorageBackend
- {
- public:
+namespace nt {
+ class 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;
- private:
+ private:
std::unordered_map<std::string, std::vector<uint8_t>> kv_;
};
-}
+} // namespace nt
diff --git a/include/LifecycleManager.h b/include/LifecycleManager.h
index e053cd2..c1e63d8 100644
--- a/include/LifecycleManager.h
+++ b/include/LifecycleManager.h
@@ -9,8 +9,7 @@
* @brief Declares monitoring, pinning, cleanup, and contention operations.
*/
-namespace nt
-{
+namespace nt {
/**
* @brief Manages runtime lifecycle concerns for registry objects.
*
@@ -23,9 +22,8 @@ namespace nt
* Monitor / Unmonitor manage `handle_count`. Pin / Unpin manage
* `reference_count`. See docs/reactos-ob-comparison.md §1.
*/
- class LifecycleManager
- {
- public:
+ class LifecycleManager {
+ public:
/**
* @brief Constructs a LifecycleManager bound to a registry and store.
*
@@ -120,7 +118,7 @@ namespace nt
*/
const bool Contention(ObjectManager::registry* object);
- private:
+ private:
/**
* @brief Joint-counters check + type-specific cascade + Unregister.
*
@@ -158,7 +156,7 @@ namespace nt
*/
void CascadeBranchTree(ObjectManager::registry* branch_tree);
- ObjectManager& objects_;
+ ObjectManager& objects_;
IStorageBackend& storage_;
};
-}
+} // namespace nt
diff --git a/include/Merkle.h b/include/Merkle.h
index ba8018a..6256ddc 100644
--- a/include/Merkle.h
+++ b/include/Merkle.h
@@ -58,143 +58,128 @@
namespace nt {
-/** @brief 32-byte hash, the universal payload type for all merkle levels. */
-using Hash32 = std::array<uint8_t, 32>;
-
-/** @brief Decode a 64-character lowercase-hex SHA256 string into raw bytes.
- * Throws std::invalid_argument when the string is not exactly 64 hex chars. */
-Hash32 hex_to_bin(const std::string& hex);
-
-/** @brief Encode raw 32 bytes back into a 64-character lowercase-hex string. */
-std::string bin_to_hex(const Hash32& bin);
-
-template<typename Key>
-class Merkle {
-public:
- static constexpr size_t B = 64;
-
- /**
- * @brief Insert or overwrite a `(key, payload)` mapping.
- * @param store KV backend for node serialisation.
- * @param root_hex Current root hash (empty string = empty tree).
- * @param key Routing/identity key.
- * @param payload Hash this key maps to (child root one level down, or
- * the key itself at the relation level).
- * @return New root hash. Equal to root_hex when an insertion of an
- * identical (key, payload) pair would be a no-op.
- */
- static std::string Insert(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key,
- const Hash32& payload);
-
- /**
- * @brief Remove a key from the tree.
- * @return New root hash. Empty string when the tree becomes empty.
- * Equal to root_hex when the key is absent.
- */
- static std::string Remove(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key);
-
- /**
- * @brief Look up the payload for a key.
- * @return The payload when present, std::nullopt when absent.
- */
- static std::optional<Hash32> Get(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key);
-
- /** @brief One (key, payload) pair returned by Page(). */
- struct Entry {
- Key key;
- Hash32 payload;
+ /** @brief 32-byte hash, the universal payload type for all merkle levels. */
+ using Hash32 = std::array<uint8_t, 32>;
+
+ /** @brief Decode a 64-character lowercase-hex SHA256 string into raw bytes.
+ * Throws std::invalid_argument when the string is not exactly 64 hex chars. */
+ Hash32 hex_to_bin(const std::string& hex);
+
+ /** @brief Encode raw 32 bytes back into a 64-character lowercase-hex string. */
+ std::string bin_to_hex(const Hash32& bin);
+
+ template <typename Key> class Merkle {
+ public:
+ static constexpr size_t B = 64;
+
+ /**
+ * @brief Insert or overwrite a `(key, payload)` mapping.
+ * @param store KV backend for node serialisation.
+ * @param root_hex Current root hash (empty string = empty tree).
+ * @param key Routing/identity key.
+ * @param payload Hash this key maps to (child root one level down, or
+ * the key itself at the relation level).
+ * @return New root hash. Equal to root_hex when an insertion of an
+ * identical (key, payload) pair would be a no-op.
+ */
+ static std::string Insert(IStorageBackend& store, const std::string& root_hex,
+ const Key& key, const Hash32& payload);
+
+ /**
+ * @brief Remove a key from the tree.
+ * @return New root hash. Empty string when the tree becomes empty.
+ * Equal to root_hex when the key is absent.
+ */
+ static std::string Remove(IStorageBackend& store, const std::string& root_hex,
+ const Key& key);
+
+ /**
+ * @brief Look up the payload for a key.
+ * @return The payload when present, std::nullopt when absent.
+ */
+ static std::optional<Hash32> Get(IStorageBackend& store, const std::string& root_hex,
+ const Key& key);
+
+ /** @brief One (key, payload) pair returned by Page(). */
+ struct Entry {
+ Key key;
+ Hash32 payload;
+ };
+
+ /**
+ * @brief Page through entries in key-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 entries. Only the nodes on the active path are loaded;
+ * sibling subtrees are never touched.
+ */
+ static std::vector<Entry> Page(IStorageBackend& store, const std::string& root_hex,
+ size_t offset, size_t limit);
+
+ private:
+ struct LeafEntry {
+ Key key;
+ Hash32 payload;
+ };
+
+ struct LeafNode {
+ std::vector<LeafEntry> entries;
+ };
+
+ struct ChildEntry {
+ uint64_t leaf_count;
+ Key min_key;
+ Hash32 child_hash;
+ };
+
+ struct InternalNode {
+ std::vector<ChildEntry> entries;
+ };
+
+ // 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);
+
+ // 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);
+
+ /** @brief Returns the smallest key in the subtree rooted at @p node_hex. */
+ static Key subtree_min_key(IStorageBackend& store, const std::string& node_hex);
+
+ /** @brief Routes a key to the rightmost child whose min_key ≤ target. */
+ static size_t route(const InternalNode& node, const Key& target);
+
+ struct InsertResult {
+ std::string new_hash;
+ uint64_t leaf_count = 0;
+ bool did_split = false;
+ uint64_t split_leaf_count = 0;
+ Key split_min_key{};
+ std::string split_hash;
+ };
+
+ static InsertResult insert_into(IStorageBackend& store, const std::string& node_hex,
+ const Key& key, const Hash32& payload);
+
+ struct RemoveResult {
+ std::string new_hash;
+ uint64_t leaf_count = 0;
+ };
+
+ static RemoveResult remove_from(IStorageBackend& store, const std::string& node_hex,
+ const Key& key);
+
+ static void page_from(IStorageBackend& store, const std::string& node_hex, size_t& offset,
+ size_t limit, std::vector<Entry>& out);
+
+ static std::optional<Hash32> get_from(IStorageBackend& store, const std::string& node_hex,
+ const Key& key);
};
- /**
- * @brief Page through entries in key-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 entries. Only the nodes on the active path are loaded;
- * sibling subtrees are never touched.
- */
- static std::vector<Entry> Page(IStorageBackend& store,
- const std::string& root_hex,
- size_t offset,
- size_t limit);
-
-private:
- struct LeafEntry {
- Key key;
- Hash32 payload;
- };
-
- struct LeafNode {
- std::vector<LeafEntry> entries;
- };
-
- struct ChildEntry {
- uint64_t leaf_count;
- Key min_key;
- Hash32 child_hash;
- };
-
- struct InternalNode {
- std::vector<ChildEntry> entries;
- };
-
- // 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);
-
- // 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);
-
- /** @brief Returns the smallest key in the subtree rooted at @p node_hex. */
- static Key subtree_min_key(IStorageBackend& store, const std::string& node_hex);
-
- /** @brief Routes a key to the rightmost child whose min_key ≤ target. */
- static size_t route(const InternalNode& node, const Key& target);
-
- struct InsertResult {
- std::string new_hash;
- uint64_t leaf_count = 0;
- bool did_split = false;
- uint64_t split_leaf_count = 0;
- Key split_min_key{};
- std::string split_hash;
- };
-
- static InsertResult insert_into(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key,
- const Hash32& payload);
-
- struct RemoveResult {
- std::string new_hash;
- uint64_t leaf_count = 0;
- };
-
- static RemoveResult remove_from(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key);
-
- static void page_from(IStorageBackend& store,
- const std::string& node_hex,
- size_t& offset,
- size_t limit,
- std::vector<Entry>& out);
-
- static std::optional<Hash32> get_from(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key);
-};
-
} // namespace nt
diff --git a/include/MultigroupCodec.h b/include/MultigroupCodec.h
index 50f7bb8..9d003ea 100644
--- a/include/MultigroupCodec.h
+++ b/include/MultigroupCodec.h
@@ -29,8 +29,7 @@
* payloads to `Merkle<std::string>`.
*/
-namespace nt::MultigroupCodec
-{
+namespace nt::MultigroupCodec {
/** @brief One entry in a multigroup snapshot: (relation_name, root_hex). */
using RelationEntry = std::pair<std::string, std::string>;
@@ -45,8 +44,7 @@ namespace nt::MultigroupCodec
* @param relations Entries to load. Duplicates collapse to the last value.
* @return Root node hash (empty string when `relations` is empty).
*/
- std::string Build(IStorageBackend& store,
- const std::vector<RelationEntry>& relations);
+ std::string Build(IStorageBackend& store, const std::vector<RelationEntry>& relations);
/**
* @brief Enumerate all (name, root_hex) pairs at a multigroup root.
@@ -54,8 +52,7 @@ namespace nt::MultigroupCodec
* Pages the entire tree (offset 0, no limit). Returned entries are in
* key-sorted order. Empty input root yields an empty list.
*/
- std::vector<RelationEntry> List(IStorageBackend& store,
- const std::string& root_hex);
+ std::vector<RelationEntry> List(IStorageBackend& store, const std::string& root_hex);
/**
* @brief Insert or overwrite one (relation_name → root_hex) entry.
@@ -65,25 +62,21 @@ namespace nt::MultigroupCodec
* mutations. Pass an empty `root_hex` for the prior-state argument to
* start from an empty tree.
*/
- std::string InsertOne(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& relation_name,
- const std::string& relation_root_hex);
+ std::string InsertOne(IStorageBackend& store, const std::string& root_hex,
+ const std::string& relation_name, const std::string& relation_root_hex);
/**
* @brief Remove one relation entry from the multigroup tree.
*
* Returns the new root hash; empty when the tree becomes empty.
*/
- std::string RemoveOne(IStorageBackend& store,
- const std::string& root_hex,
+ std::string RemoveOne(IStorageBackend& store, const std::string& root_hex,
const std::string& relation_name);
/**
* @brief Look up one relation's root hex by name.
* @return Hex root, or empty string when the name is absent.
*/
- std::string Lookup(IStorageBackend& store,
- const std::string& root_hex,
+ std::string Lookup(IStorageBackend& store, const std::string& root_hex,
const std::string& relation_name);
-}
+} // namespace nt::MultigroupCodec
diff --git a/include/NT.h b/include/NT.h
index 0fc5514..041c517 100644
--- a/include/NT.h
+++ b/include/NT.h
@@ -5,13 +5,13 @@
* @brief Includes for the Relational NT public API.
*/
-#include "Types.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "NamespaceReferenceManager.h"
#include "CursorManager.h"
+#include "HandlerManager.h"
#include "IdentityManager.h"
#include "LifecycleManager.h"
-#include "HandlerManager.h"
-#include "VM.h"
+#include "NamespaceReferenceManager.h"
+#include "ObjectManager.h"
+#include "PermissionsManager.h"
#include "Runtime.h"
+#include "Types.h"
+#include "VM.h"
diff --git a/include/NamespaceReferenceManager.h b/include/NamespaceReferenceManager.h
index 3264271..d09f4d4 100644
--- a/include/NamespaceReferenceManager.h
+++ b/include/NamespaceReferenceManager.h
@@ -12,8 +12,7 @@
* @brief Declares logical reference mapping and namespace isolation.
*/
-namespace nt
-{
+namespace nt {
/**
* @class NamespaceReferenceManager
* @brief Handles the logical mapping and isolation of human-readable references.
@@ -67,9 +66,8 @@ namespace nt
* @todo Define the batch-update API with all-or-nothing contention semantics.
* See docs/reactos-ob-comparison.md §8.
*/
- class NamespaceReferenceManager
- {
- public:
+ class NamespaceReferenceManager {
+ public:
/**
* @brief Constructs a NamespaceReferenceManager bound to a registry.
* @param objects Registry used to look up reference objects (branches,
@@ -78,8 +76,7 @@ namespace nt
* nodes when resolving `/system/branches/<n>/<mg>/...`
* paths through to their snapshot.
*/
- NamespaceReferenceManager(ObjectManager& objects,
- IStorageBackend& storage);
+ NamespaceReferenceManager(ObjectManager& objects, IStorageBackend& storage);
/**
* @brief Rewrites a logical path through reference reparse rules.
@@ -103,8 +100,8 @@ namespace nt
*/
std::vector<std::string> Resolve(std::vector<std::string> path) const;
- private:
- ObjectManager& objects_;
+ private:
+ ObjectManager& objects_;
IStorageBackend& storage_;
};
-}
+} // namespace nt
diff --git a/include/ObjectManager.h b/include/ObjectManager.h
index 1391508..9ea49e6 100644
--- a/include/ObjectManager.h
+++ b/include/ObjectManager.h
@@ -16,8 +16,7 @@
* @brief Declares the registry responsible for runtime-managed database objects.
*/
-namespace nt
-{
+namespace nt {
/**
* @brief Owns the registry used to locate and reference database objects.
*
@@ -25,12 +24,11 @@ namespace nt
* the metadata needed to reason about object type, path, references, and
* handles while the concrete storage model is still being designed.
*/
- class ObjectManager
- {
- public:
+ class ObjectManager {
+ public:
/** @brief Base type for objects stored in the registry. */
class IObject {
- public:
+ public:
virtual ~IObject() = default;
};
@@ -52,7 +50,7 @@ namespace nt
* pins it, no pinned cursor still references it).
*/
class Multigroup : public IObject {
- public:
+ public:
std::string merkle_root;
};
/**
@@ -63,7 +61,7 @@ namespace nt
* Updated atomically by rnt_link_tuple / rnt_unlink_tuple / rnt_clear_relation.
*/
class Relation : public IObject {
- public:
+ public:
std::string merkle_root;
};
/**
@@ -96,8 +94,8 @@ namespace nt
* path this remains a documentation-only contract.
*/
class EphemeralRelation : public IObject {
- public:
- std::string merkle_root;
+ public:
+ std::string merkle_root;
std::vector<std::string> dependencies;
};
/** @brief Abstract registry object representing a transaction. */
@@ -125,7 +123,7 @@ namespace nt
* the walk hot.
*/
class Session : public IObject {
- public:
+ public:
void* connection_context = nullptr;
std::map<std::string, std::string> branch_overrides;
};
@@ -147,7 +145,7 @@ namespace nt
* Multigroups and operate on those.
*/
class Branch : public IObject {
- public:
+ public:
std::string name;
std::string target_hash;
};
@@ -165,7 +163,7 @@ namespace nt
* each multigroup it references before unregistering the entry.
*/
class BranchTree : public IObject {
- public:
+ public:
std::string merkle_root;
};
@@ -224,8 +222,7 @@ namespace nt
* maps `offset` to the appropriate pair via a bijective enumeration scheme
* (e.g. Cantor pairing); callers must bound such scans with a TAKE node.
*/
- struct ephemeral_object_type : object_type
- {
+ struct ephemeral_object_type : object_type {
enum class Cardinality { Finite, ConstrainedFinite, AlephZero, Continuum };
/**
@@ -235,12 +232,10 @@ namespace nt
* @param limit Maximum number of tuples to return.
*/
using Generator = std::function<std::vector<Tuple>(
- const std::vector<std::string>& args,
- std::size_t offset,
- std::size_t limit)>;
+ const std::vector<std::string>& args, std::size_t offset, std::size_t limit)>;
Cardinality cardinality;
- Generator generator;
+ Generator generator;
};
/**
@@ -313,8 +308,7 @@ namespace nt
* @param object Object payload. Ownership is transferred to the registry.
* @param type Object type descriptor. Ownership is transferred to the registry.
*/
- void Register(std::vector<std::string> path,
- std::unique_ptr<IObject> object,
+ void Register(std::vector<std::string> path, std::unique_ptr<IObject> object,
std::unique_ptr<object_type> type);
/**
@@ -338,4 +332,4 @@ namespace nt
*/
bool Unregister(const std::vector<std::string>& object_path);
};
-}
+} // namespace nt
diff --git a/include/PermissionsManager.h b/include/PermissionsManager.h
index 3af75f6..af72154 100644
--- a/include/PermissionsManager.h
+++ b/include/PermissionsManager.h
@@ -11,8 +11,7 @@
* @brief Declares authentication and object-access checks.
*/
-namespace nt
-{
+namespace nt {
/**
* @class Capability-based permission manager
* @brief Evaluates connection claims and object permissions.
@@ -37,9 +36,8 @@ namespace nt
* rather than re-evaluating the descriptor on every call.
* See docs/reactos-ob-comparison.md §4.
*/
- class PermissionsManager
- {
- public:
+ class PermissionsManager {
+ public:
/**
* @brief Validates a connection during login.
* @param method Authentication method used by the connection.
@@ -62,7 +60,7 @@ namespace nt
* 3 other stored relations, while you can access the ephemeral relation,
* you cannot directly access the other 3 relations. You need explicit
* permission to create a handle on the others.
- *
+ *
* Here's an example tree:
* /system
* /multigroups
@@ -70,22 +68,21 @@ namespace nt
* /relations
* /user {type : stored}
* /order {type : stored}
- * /user_and_order {type : ephemeral; deps = [/system/multigroups/coffee_shop/relations/users, /system/multigroups/coffee_shop/relations/orders]}
- * /users
- * /peter
- * /permissions
+ * /user_and_order {type : ephemeral; deps =
+ * [/system/multigroups/coffee_shop/relations/users,
+ * /system/multigroups/coffee_shop/relations/orders]} /users /peter /permissions
* /multigroups/coffee_shop/relations/user_and_order {descriptor : [read]}
* /paul
* /permissions
* /multigroups/coffee_shop/relations/user {descriptor : [write, read]}
- *
+ *
* Meaning that `peter` has access to read the `user_and_order` ephemeral
* relation, but cannot open a handle on `user` and `order`.
- *
+ *
* @param object Object being accessed.
* @param connection_context Connection metadata for the caller.
* @return True when access is allowed.
*/
const bool Access(const ObjectManager::registry* object, const void* connection_context);
};
-}
+} // namespace nt
diff --git a/include/RNT_C_API.h b/include/RNT_C_API.h
index 707c04a..1a59785 100644
--- a/include/RNT_C_API.h
+++ b/include/RNT_C_API.h
@@ -36,6 +36,7 @@
#include <stdint.h>
#include "Api.h"
+#include "VM.h"
#ifdef __cplusplus
extern "C" {
@@ -133,8 +134,7 @@ NT_API int rnt_session_close(const char* session_hash);
* @param target_hash Snapshot hash to bind, or "" to clear.
* @return 0 on success, negative on error.
*/
-NT_API int rnt_session_set_branch(const char* session_hash,
- const char* branch_name,
+NT_API int rnt_session_set_branch(const char* session_hash, const char* branch_name,
const char* target_hash);
/* ------------------------------------------------------------------ */
@@ -292,9 +292,7 @@ NT_API int rnt_list_snapshot_relations(const char* snapshot_hash, char** out);
* Release with rnt_free_string(). NULL on error.
* @return 0 on success, negative on error.
*/
-NT_API int rnt_link_tuple(const char* relation_path,
- const char* kv_attrs,
- char** hash_out);
+NT_API 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.
@@ -377,40 +375,78 @@ NT_API int rnt_cursor_close(rnt_cursor_t cursor);
/* VM plan builder */
/* ------------------------------------------------------------------ */
-/** Opaque plan node tree, built incrementally via rnt_plan_* calls. */
+/** Opaque plan node tree, built via rnt_plan_assemble calls. */
typedef void* rnt_plan_t;
/**
- * @brief Creates a SCAN plan node that reads all tuples from a stored relation.
+ * SCAN context: 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.
+ * @param relation_path Absolute slash-separated path, e.g.
* "/system/branches/main/multigroups/warehouse/relations/public:users".
- * @return Plan node, or NULL when the relation does not exist or cannot be opened.
*/
-NT_API rnt_plan_t rnt_plan_scan(const char* relation_path);
+typedef struct {
+ const char* relation_path;
+} PlanArgsScan;
/**
- * @brief Creates a nested-loop JOIN plan node.
+ * JOIN context: nested-loop join of two child plans.
*
- * 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.
+ * Takes ownership of both children. They are released when the assembled plan
+ * is freed or executed; on failure both are freed.
+ */
+typedef struct {
+ rnt_plan_t left;
+ rnt_plan_t right;
+} PlanArgsJoin;
+
+/**
+ * TAKE context: passes at most @p limit tuples from @p source, then stops.
*
- * @return Plan node, or NULL on error.
+ * Takes ownership of @p source; on failure @p source is freed.
+ */
+typedef struct {
+ rnt_plan_t source;
+ size_t limit;
+} PlanArgsTake;
+
+/**
+ * PROJECT context: keeps only the named attributes of @p source.
+ *
+ * @p attrs is a NULL-terminated array of attribute names to retain, e.g.
+ * <tt>{ "name", "profession", NULL }</tt>. Takes ownership of @p source; on
+ * failure @p source is freed.
+ */
+typedef struct {
+ rnt_plan_t source;
+ const char** attrs;
+} PlanArgsProject;
+
+/**
+ * A single plan-construction request. @p operation selects which context member
+ * is read; the others are ignored. The members are laid out side by side rather
+ * than overlapped in a union so the struct maps cleanly onto OCaml ctypes, which
+ * has no union support. Only the member matching @p operation need be populated.
*/
-NT_API rnt_plan_t rnt_plan_join(rnt_plan_t left, rnt_plan_t right);
+typedef struct {
+ nt::Operation operation;
+ PlanArgsScan scan;
+ PlanArgsJoin join;
+ PlanArgsTake take;
+ PlanArgsProject project;
+} PlanAction;
/**
- * @brief Creates a TAKE plan node that limits output to at most @p limit tuples.
+ * @brief Builds one plan node from @p action and returns the resulting subtree.
*
- * Takes ownership of @p source. On failure @p source is freed.
+ * Sole entry point for plan construction. Validates runtime state once, then
+ * dispatches on @p action.op. Child plans for JOIN/TAKE/PROJECT are themselves
+ * results of prior rnt_plan_assemble calls; ownership transfers per the
+ * per-operator rules documented on each PlanArgs* struct.
*
- * @return Plan node, or NULL on error.
+ * @return Plan node, or NULL when the runtime is uninitialised or construction
+ * fails (e.g. relation does not exist).
*/
-NT_API rnt_plan_t rnt_plan_take(rnt_plan_t source, size_t limit);
+NT_API rnt_plan_t rnt_plan_assemble(PlanAction action);
/**
* @brief Releases a plan that was built but not yet executed.
diff --git a/include/Runtime.h b/include/Runtime.h
index 4abe8c7..0fb41a5 100644
--- a/include/Runtime.h
+++ b/include/Runtime.h
@@ -7,12 +7,10 @@
* @brief Declares the top-level Relational NT runtime facade.
*/
-namespace nt
-{
+namespace nt {
/** @brief Top-level runtime object used by applications and the console entry point. */
- class NT
- {
- public:
+ class NT {
+ public:
/**
* @brief Reports whether the runtime is active.
* @return True when the runtime is running.
@@ -22,4 +20,4 @@ namespace nt
/** @brief Simulates an external entry call into the runtime. */
void SimulateEntryCall();
};
-}
+} // namespace nt
diff --git a/include/SqliteBackend.h b/include/SqliteBackend.h
index e4cdf56..037d8e6 100644
--- a/include/SqliteBackend.h
+++ b/include/SqliteBackend.h
@@ -21,11 +21,9 @@
* via Merkle::Page which reads only the KV store.
*/
-namespace nt
-{
- class SqliteBackend : public IStorageBackend
- {
- public:
+namespace nt {
+ class SqliteBackend : public IStorageBackend {
+ public:
/**
* @brief Opens (or creates) a SQLite database at @p path.
* @param path File path, or ":memory:" for an in-process database.
@@ -37,7 +35,7 @@ namespace nt
std::string Put(std::vector<uint8_t> value) override;
std::optional<std::vector<uint8_t>> Get(const std::string& hash) override;
- private:
+ private:
sqlite3* db_ = nullptr;
};
-}
+} // namespace nt
diff --git a/include/TupleCodec.h b/include/TupleCodec.h
index 18294d4..6c74c1e 100644
--- a/include/TupleCodec.h
+++ b/include/TupleCodec.h
@@ -23,8 +23,7 @@
* [uint32_le name_len][name bytes][uint32_le value_len][value bytes] ...
*/
-namespace nt::TupleCodec
-{
+namespace nt::TupleCodec {
/**
* @brief Computes the SHA256 hex digest of the serialized tuple.
*
@@ -49,4 +48,4 @@ namespace nt::TupleCodec
* @return Reconstructed attribute list in serialization order.
*/
std::vector<Attribute> Deserialize(const std::vector<uint8_t>& bytes);
-}
+} // namespace nt::TupleCodec
diff --git a/include/Types.h b/include/Types.h
index edf4297..6956e09 100644
--- a/include/Types.h
+++ b/include/Types.h
@@ -118,8 +118,7 @@ enum AUTH_METHOD {
PLAIN_TEXT
};
-namespace nt
-{
+namespace nt {
/** @brief A single named value in a tuple, produced by the cursor layer. */
struct Attribute {
std::string name;
@@ -133,9 +132,9 @@ namespace nt
* one attribute at a time. Returns nullptr when all attributes are exhausted.
*/
class Tuple {
- public:
- explicit Tuple(std::vector<Attribute> attributes)
- : attributes_(std::move(attributes)) {}
+ public:
+ explicit Tuple(std::vector<Attribute> attributes) : attributes_(std::move(attributes)) {
+ }
/** @brief Returns the next attribute, or nullptr when exhausted. */
const Attribute* Next() {
@@ -144,13 +143,17 @@ namespace nt
return nullptr;
}
- void Reset() { position_ = 0; }
+ void Reset() {
+ position_ = 0;
+ }
/** @brief Direct read-only access to all attributes without affecting position. */
- const std::vector<Attribute>& attrs() const { return attributes_; }
+ const std::vector<Attribute>& attrs() const {
+ return attributes_;
+ }
- private:
+ private:
std::vector<Attribute> attributes_;
size_t position_ = 0;
};
-}
+} // namespace nt
diff --git a/include/VM.h b/include/VM.h
index 3bfe521..3d97779 100644
--- a/include/VM.h
+++ b/include/VM.h
@@ -14,8 +14,7 @@
* @brief Declares the logic execution boundary for relational and higher-order workloads.
*/
-namespace nt
-{
+namespace nt {
/**
* @class VM
* @brief Hosts the first-order logic (FOL) and second-order logic (SOL) cores.
@@ -64,15 +63,25 @@ namespace nt
* (for EPHEMERAL_RELATION cursors) or ignored (for stored RELATION cursors,
* which need no per-probe parameterization).
*/
- struct PathArg
- {
+ struct PathArg {
enum class Kind { Var, Const };
- Kind kind;
+ Kind kind;
std::string name; /**< Attribute name to look up (Var), or literal value (Const). */
- static PathArg Var(std::string attr) { return { Kind::Var, std::move(attr) }; }
- static PathArg Const(std::string val) { return { Kind::Const, std::move(val) }; }
+ static PathArg Var(std::string attr) {
+ return {Kind::Var, std::move(attr)};
+ }
+ static PathArg Const(std::string val) {
+ return {Kind::Const, std::move(val)};
+ }
+ };
+
+ enum Operation {
+ FOL_OPERATION_SCAN = 1,
+ FOL_OPERATION_JOIN,
+ FOL_OPERATION_TAKE,
+ FOL_OPERATION_PROJECT
};
/**
@@ -91,12 +100,9 @@ namespace nt
* this to bound scans over AlephZero ephemeral relations.
* - PROJECT: filters each input tuple to the named attributes in project_attrs.
*/
- struct PlanNode
- {
- enum class Op { SCAN, JOIN, TAKE, PROJECT };
-
- Op op;
- PlanNode* left = nullptr;
+ struct PlanNode {
+ Operation op;
+ PlanNode* left = nullptr;
PlanNode* right = nullptr;
/**
@@ -134,13 +140,12 @@ namespace nt
* successful join. Callers must consume the returned pointer before
* calling Next() again — same contract as a cursor page pointer.
*/
- Tuple* join_left = nullptr;
+ Tuple* join_left = nullptr;
std::optional<Tuple> join_buffer;
};
- class VM
- {
- public:
+ class VM {
+ public:
// TODO: CursorManager should be injected as a shared runtime instance,
// matching the pattern flagged for HandlerManager and LifecycleManager.
//
@@ -167,21 +172,19 @@ namespace nt
*/
Tuple* Next(PlanNode* node);
- private:
+ private:
CursorManager& cursors_;
/**
* Resolves a scan_args template against an outer tuple, producing the
* concrete args vector to write into the inner cursor before probing.
*/
- static std::vector<std::string> ResolveArgs(const std::vector<PathArg>& tmpl,
- Tuple* from);
+ static std::vector<std::string> ResolveArgs(const std::vector<PathArg>& tmpl, Tuple* from);
/** Resets the inner cursor state and writes resolved args into it. */
- static void ResetInner(CursorManager::cursor* c,
- std::vector<std::string> args);
+ static void ResetInner(CursorManager::cursor* c, std::vector<std::string> args);
/** Merges left and right tuple attributes into node->join_buffer. */
static Tuple* MergeInto(PlanNode* node, Tuple* left, Tuple* right);
};
-}
+} // namespace nt
diff --git a/src/CursorManager.cpp b/src/CursorManager.cpp
index d96909b..0d71b1c 100644
--- a/src/CursorManager.cpp
+++ b/src/CursorManager.cpp
@@ -3,46 +3,38 @@
#include "ObjectManager.h"
#include "TupleCodec.h"
-namespace nt
-{
- CursorManager::CursorManager(IStorageBackend& backend,
- LifecycleManager* lifecycles,
+namespace nt {
+ CursorManager::CursorManager(IStorageBackend& backend, LifecycleManager* lifecycles,
ObjectManager* objects)
- : backend_(backend)
- , lifecycles_(lifecycles)
- , objects_(objects)
- {}
+ : backend_(backend), lifecycles_(lifecycles), objects_(objects) {
+ }
// Finds the /system/snapshots/<H> entry that owns a handle's relation, or
// nullptr when the relation lives outside the snapshot namespace (tests use
// synthetic paths like {"relations", "villager"}).
- static ObjectManager::registry* find_parent_snapshot(
- ObjectManager& objects,
- HandlerManager::handle* handle)
- {
- if (handle == nullptr || handle->object == nullptr
- || handle->object->head == nullptr)
+ static ObjectManager::registry* find_parent_snapshot(ObjectManager& objects,
+ HandlerManager::handle* handle) {
+ if (handle == nullptr || handle->object == nullptr || handle->object->head == nullptr)
return nullptr;
const auto& p = handle->object->head->path;
- if (p.size() != 5) return nullptr;
+ if (p.size() != 5)
+ return nullptr;
if (p[0] != "system" || p[1] != "snapshots" || p[3] != "relations")
return nullptr;
- return objects.Find({ p[0], p[1], p[2] });
+ return objects.Find({p[0], p[1], p[2]});
}
- void CursorManager::LoadPage(cursor* c)
- {
- auto entries = Merkle<Hash32>::Page(backend_, c->merkle_root,
- c->fetch_offset, PAGE_SIZE);
+ void CursorManager::LoadPage(cursor* c) {
+ auto entries = Merkle<Hash32>::Page(backend_, c->merkle_root, c->fetch_offset, PAGE_SIZE);
c->page.clear();
c->page_position = 0;
- for (const auto& entry : entries)
- {
+ for (const auto& entry : entries) {
auto bytes = backend_.Get(bin_to_hex(entry.payload));
- if (!bytes) continue;
+ if (!bytes)
+ continue;
c->page.emplace_back(TupleCodec::Deserialize(*bytes));
}
@@ -52,23 +44,23 @@ namespace nt
}
CursorManager::cursor* CursorManager::Open(HandlerManager::handle* handle,
- const std::string& merkle_root)
- {
+ const std::string& merkle_root) {
if (handle == nullptr || handle->object == nullptr)
return nullptr;
const auto label = handle->object->head->type->label;
auto pin_snapshot = [&](cursor* c) {
- if (lifecycles_ == nullptr || objects_ == nullptr) return;
+ if (lifecycles_ == nullptr || objects_ == nullptr)
+ return;
auto* snap = find_parent_snapshot(*objects_, handle);
- if (snap == nullptr) return;
+ if (snap == nullptr)
+ return;
lifecycles_->Pin(snap);
c->pinned_snapshot = snap;
};
- if (label == EPHEMERAL_RELATION)
- {
+ if (label == EPHEMERAL_RELATION) {
// Cursor is created exhausted — the JOIN operator writes args and
// resets the state before the first probe. The generator is copied
// off the registry-owned ephemeral_object_type so Next() never has
@@ -78,8 +70,8 @@ namespace nt
auto* c = new cursor();
c->is_ephemeral = true;
- c->generator = etype->generator;
- c->exhausted = true;
+ c->generator = etype->generator;
+ c->exhausted = true;
pin_snapshot(c);
return c;
}
@@ -91,8 +83,7 @@ namespace nt
c->merkle_root = merkle_root;
pin_snapshot(c);
- if (merkle_root.empty())
- {
+ if (merkle_root.empty()) {
// Empty relation: valid but immediately exhausted.
c->exhausted = true;
return c;
@@ -102,36 +93,39 @@ namespace nt
return c;
}
- Tuple* CursorManager::Next(cursor* c)
- {
- if (c == nullptr) return nullptr;
+ Tuple* CursorManager::Next(cursor* c) {
+ if (c == nullptr)
+ return nullptr;
if (c->page_position < c->page.size())
return &c->page[c->page_position++];
- if (c->exhausted) return nullptr;
+ if (c->exhausted)
+ return nullptr;
- if (c->is_ephemeral)
- {
+ if (c->is_ephemeral) {
c->page = c->generator(c->args, c->fetch_offset, PAGE_SIZE);
c->page_position = 0;
c->fetch_offset += c->page.size();
if (c->page.size() < PAGE_SIZE)
c->exhausted = true;
- if (c->page.empty()) return nullptr;
+ if (c->page.empty())
+ return nullptr;
return &c->page[c->page_position++];
}
LoadPage(c);
- if (c->page.empty()) return nullptr;
+ if (c->page.empty())
+ return nullptr;
return &c->page[c->page_position++];
}
- void CursorManager::Close(cursor* c)
- {
- if (c == nullptr) { return; }
+ void CursorManager::Close(cursor* c) {
+ if (c == nullptr) {
+ return;
+ }
if (lifecycles_ != nullptr && c->pinned_snapshot != nullptr)
lifecycles_->Unpin(c->pinned_snapshot);
delete c;
}
-}
+} // namespace nt
diff --git a/src/HandlerManager.cpp b/src/HandlerManager.cpp
index cb3b723..505b393 100644
--- a/src/HandlerManager.cpp
+++ b/src/HandlerManager.cpp
@@ -5,44 +5,34 @@
#include "NamespaceReferenceManager.h"
#include "PermissionsManager.h"
-namespace nt
-{
- void HandlerManager::DeallocateHandle(struct handle* handle)
- {
+namespace nt {
+ void HandlerManager::DeallocateHandle(struct handle* handle) {
delete handle;
}
- HandlerManager::HandlerManager(ObjectManager& objects,
- PermissionsManager& permissions,
- IdentityManager& identities,
- LifecycleManager& lifecycles,
- NamespaceReferenceManager& references)
- : objects_(objects)
- , permissions_(permissions)
- , identities_(identities)
- , lifecycles_(lifecycles)
- , references_(references)
- {}
+ HandlerManager::HandlerManager(ObjectManager& objects, PermissionsManager& permissions,
+ IdentityManager& identities, LifecycleManager& lifecycles,
+ NamespaceReferenceManager& references)
+ : objects_(objects), permissions_(permissions), identities_(identities),
+ lifecycles_(lifecycles), references_(references) {
+ }
- struct HandlerManager::handle* HandlerManager::Open(std::vector<std::string> object_path, void* connection_context)
- {
+ struct HandlerManager::handle* HandlerManager::Open(std::vector<std::string> object_path,
+ void* connection_context) {
const auto resolved = references_.Resolve(std::move(object_path));
ObjectManager::registry* retrieved_object = objects_.Find(resolved);
- if (permissions_.Access(retrieved_object, (const void*)connection_context)
- && identities_.CanOpen(retrieved_object)
- && lifecycles_.Contention(retrieved_object)) {
+ if (permissions_.Access(retrieved_object, (const void*)connection_context) &&
+ identities_.CanOpen(retrieved_object) && lifecycles_.Contention(retrieved_object)) {
// Starts monitoring this guy
lifecycles_.Monitor(retrieved_object);
- return new handle{ retrieved_object, connection_context }; // Return a successful handle
- }
- else {
+ return new handle{retrieved_object, connection_context}; // Return a successful handle
+ } else {
// Return something more algebraic instead of null or an int
return nullptr;
};
}
- bool HandlerManager::Close(struct handle* handle)
- {
+ bool HandlerManager::Close(struct handle* handle) {
if (handle == nullptr) {
return false;
}
@@ -51,21 +41,19 @@ namespace nt
void* connection_context = handle->connection_context;
// Checking for permissions here may be relevant.
// What if we have a handle but no permission? Spooky
- if (permissions_.Access(object, connection_context)
- && identities_.CanClose(object)
+ if (permissions_.Access(object, connection_context) && identities_.CanClose(object)
// TODO: I think that in this case we do not need to worry about contention.
// But be careful to evaluate this scenario later.
//&& lifecycles_.Contention(object)
- ) {
+ ) {
// Stops monitoring this guy
lifecycles_.Unmonitor(object);
DeallocateHandle(handle);
return true; // Return a successful handle
- }
- else {
+ } else {
// Return something more algebraic instead of null or an int.
// It's kinda bad to not even report what went wrong.
return false;
};
}
-}
+} // namespace nt
diff --git a/src/IdentityManager.cpp b/src/IdentityManager.cpp
index 93ad705..9b131a9 100644
--- a/src/IdentityManager.cpp
+++ b/src/IdentityManager.cpp
@@ -1,14 +1,11 @@
#include "IdentityManager.h"
-namespace nt
-{
- const bool IdentityManager::CanOpen(ObjectManager::registry* object)
- {
+namespace nt {
+ const bool IdentityManager::CanOpen(ObjectManager::registry* object) {
return object != nullptr;
}
- const bool IdentityManager::CanClose(ObjectManager::registry* object)
- {
+ const bool IdentityManager::CanClose(ObjectManager::registry* object) {
return object != nullptr;
}
-}
+} // namespace nt
diff --git a/src/InMemoryBackend.cpp b/src/InMemoryBackend.cpp
index 4649da0..8aebc65 100644
--- a/src/InMemoryBackend.cpp
+++ b/src/InMemoryBackend.cpp
@@ -2,19 +2,17 @@
#include <picosha2.h>
-namespace nt
-{
- std::string InMemoryBackend::Put(std::vector<uint8_t> value)
- {
+namespace nt {
+ std::string InMemoryBackend::Put(std::vector<uint8_t> value) {
const std::string hash = picosha2::hash256_hex_string(value.begin(), value.end());
kv_.emplace(hash, std::move(value));
return hash;
}
- std::optional<std::vector<uint8_t>> InMemoryBackend::Get(const std::string& hash)
- {
+ std::optional<std::vector<uint8_t>> InMemoryBackend::Get(const std::string& hash) {
auto it = kv_.find(hash);
- if (it == kv_.end()) return std::nullopt;
+ if (it == kv_.end())
+ return std::nullopt;
return it->second;
}
-}
+} // namespace nt
diff --git a/src/LifecycleManager.cpp b/src/LifecycleManager.cpp
index 78e112d..4a12e09 100644
--- a/src/LifecycleManager.cpp
+++ b/src/LifecycleManager.cpp
@@ -5,55 +5,50 @@
#include <vector>
-namespace nt
-{
- LifecycleManager::LifecycleManager(ObjectManager& objects,
- IStorageBackend& storage)
- : objects_(objects), storage_(storage)
- {}
-
- void LifecycleManager::Monitor(ObjectManager::registry* object)
- {
+namespace nt {
+ LifecycleManager::LifecycleManager(ObjectManager& objects, IStorageBackend& storage)
+ : objects_(objects), storage_(storage) {
+ }
+
+ void LifecycleManager::Monitor(ObjectManager::registry* object) {
if (object != nullptr)
++object->head->handle_count;
}
- void LifecycleManager::Unmonitor(ObjectManager::registry* object)
- {
- if (object == nullptr || object->head->handle_count == 0) return;
+ void LifecycleManager::Unmonitor(ObjectManager::registry* object) {
+ if (object == nullptr || object->head->handle_count == 0)
+ return;
--object->head->handle_count;
TryCollect(object);
}
- void LifecycleManager::Pin(ObjectManager::registry* object)
- {
+ void LifecycleManager::Pin(ObjectManager::registry* object) {
if (object != nullptr)
++object->head->reference_count;
}
- void LifecycleManager::Unpin(ObjectManager::registry* object)
- {
- if (object == nullptr || object->head->reference_count == 0) return;
+ void LifecycleManager::Unpin(ObjectManager::registry* object) {
+ if (object == nullptr || object->head->reference_count == 0)
+ return;
--object->head->reference_count;
TryCollect(object);
}
- bool LifecycleManager::IsEligibleForGC(ObjectManager::registry* object) const
- {
- if (object == nullptr || object->head == nullptr) return false;
- return object->head->handle_count == 0
- && object->head->reference_count == 0;
+ bool LifecycleManager::IsEligibleForGC(ObjectManager::registry* object) const {
+ if (object == nullptr || object->head == nullptr)
+ return false;
+ return object->head->handle_count == 0 && object->head->reference_count == 0;
}
- const bool LifecycleManager::Contention(ObjectManager::registry* object)
- {
+ const bool LifecycleManager::Contention(ObjectManager::registry* object) {
return object != nullptr;
}
- void LifecycleManager::TryCollect(ObjectManager::registry* object)
- {
- if (!IsEligibleForGC(object)) return;
- if (object->head == nullptr || object->head->type == nullptr) return;
+ void LifecycleManager::TryCollect(ObjectManager::registry* object) {
+ if (!IsEligibleForGC(object))
+ return;
+ if (object->head == nullptr || object->head->type == nullptr)
+ return;
// Named-lifetime types are managed explicitly and must not auto-collect:
// BRANCH — named branches persist until a future explicit-delete API
@@ -64,48 +59,59 @@ namespace nt
// EPHEMERAL_RELATION, TRANSACTION) is purely structurally referenced
// and is GC-eligible by counters alone.
const auto label = object->head->type->label;
- if (label == BRANCH || label == SESSION) return;
+ if (label == BRANCH || label == SESSION)
+ return;
- if (label == MULTIGROUP) CascadeMultigroup(object);
- if (label == BRANCH_TREE) CascadeBranchTree(object);
+ if (label == MULTIGROUP)
+ CascadeMultigroup(object);
+ if (label == BRANCH_TREE)
+ CascadeBranchTree(object);
objects_.Unregister(object->head->path);
}
- void LifecycleManager::CascadeMultigroup(ObjectManager::registry* multigroup)
- {
- if (multigroup == nullptr || multigroup->head == nullptr) return;
+ void LifecycleManager::CascadeMultigroup(ObjectManager::registry* multigroup) {
+ if (multigroup == nullptr || multigroup->head == nullptr)
+ return;
const auto& mg_path = multigroup->head->path;
- if (mg_path.size() < 3) return;
+ if (mg_path.size() < 3)
+ return;
// /system/snapshots/<hash>/relations/<n> entries are the relation
// children pinned by this multigroup. Collect first, mutate after —
// Unpin can Unregister the entry we're standing on if its counters
// were already zero, and iteration must outlive that mutation.
std::vector<ObjectManager::registry*> children;
- for (auto* cur = objects_.entries.get(); cur != nullptr; cur = cur->next.get())
- {
+ for (auto* cur = objects_.entries.get(); cur != nullptr; cur = cur->next.get()) {
const auto& p = cur->head->path;
- if (p.size() != mg_path.size() + 2) continue;
+ if (p.size() != mg_path.size() + 2)
+ continue;
bool prefix_match = true;
for (size_t i = 0; i < mg_path.size(); ++i)
- if (p[i] != mg_path[i]) { prefix_match = false; break; }
- if (!prefix_match) continue;
- if (p[mg_path.size()] != "relations") continue;
+ if (p[i] != mg_path[i]) {
+ prefix_match = false;
+ break;
+ }
+ if (!prefix_match)
+ continue;
+ if (p[mg_path.size()] != "relations")
+ continue;
children.push_back(cur);
}
- for (auto* child : children) Unpin(child);
+ for (auto* child : children)
+ Unpin(child);
}
- void LifecycleManager::CascadeBranchTree(ObjectManager::registry* branch_tree)
- {
- if (branch_tree == nullptr || branch_tree->head == nullptr) return;
+ void LifecycleManager::CascadeBranchTree(ObjectManager::registry* branch_tree) {
+ if (branch_tree == nullptr || branch_tree->head == nullptr)
+ return;
auto* bt = dynamic_cast<ObjectManager::BranchTree*>(branch_tree->object.get());
- if (bt == nullptr || bt->merkle_root.empty()) return;
+ if (bt == nullptr || bt->merkle_root.empty())
+ return;
// Page through the (mg_name, mg_hash) tree and collect each referenced
// multigroup before unpinning. Decoupling iteration from mutation
@@ -113,24 +119,25 @@ namespace nt
std::vector<ObjectManager::registry*> mgs;
constexpr size_t kPageSize = 1024;
size_t offset = 0;
- while (true)
- {
- auto page = Merkle<std::string>::Page(storage_, bt->merkle_root,
- offset, kPageSize);
- if (page.empty()) break;
- for (const auto& entry : page)
- {
+ while (true) {
+ auto page = Merkle<std::string>::Page(storage_, bt->merkle_root, offset, kPageSize);
+ if (page.empty())
+ break;
+ for (const auto& entry : page) {
static const Hash32 zero{};
- if (entry.payload == zero) continue;
+ if (entry.payload == zero)
+ continue;
const std::string mg_hash = bin_to_hex(entry.payload);
- auto* mg_entry = objects_.Find(
- {"system", "snapshots", mg_hash});
- if (mg_entry) mgs.push_back(mg_entry);
+ auto* mg_entry = objects_.Find({"system", "snapshots", mg_hash});
+ if (mg_entry)
+ mgs.push_back(mg_entry);
}
- if (page.size() < kPageSize) break;
+ if (page.size() < kPageSize)
+ break;
offset += page.size();
}
- for (auto* mg : mgs) Unpin(mg);
+ for (auto* mg : mgs)
+ Unpin(mg);
}
-}
+} // namespace nt
diff --git a/src/Merkle.cpp b/src/Merkle.cpp
index 760cfce..09329f9 100644
--- a/src/Merkle.cpp
+++ b/src/Merkle.cpp
@@ -7,604 +7,546 @@
namespace nt {
-// ── Wire format constants ────────────────────────────────────────────────────
-
-static constexpr uint8_t TAG_LEAF = 0x4C; // 'L'
-static constexpr uint8_t TAG_INTERNAL = 0x49; // 'I'
-
-// ── Byte-order helpers ───────────────────────────────────────────────────────
-
-static void write_u32(std::vector<uint8_t>& buf, uint32_t v)
-{
- buf.push_back(static_cast<uint8_t>(v >> 24));
- buf.push_back(static_cast<uint8_t>(v >> 16));
- buf.push_back(static_cast<uint8_t>(v >> 8));
- buf.push_back(static_cast<uint8_t>(v ));
-}
-
-static void write_u64(std::vector<uint8_t>& buf, uint64_t v)
-{
- buf.push_back(static_cast<uint8_t>(v >> 56));
- buf.push_back(static_cast<uint8_t>(v >> 48));
- buf.push_back(static_cast<uint8_t>(v >> 40));
- buf.push_back(static_cast<uint8_t>(v >> 32));
- buf.push_back(static_cast<uint8_t>(v >> 24));
- buf.push_back(static_cast<uint8_t>(v >> 16));
- buf.push_back(static_cast<uint8_t>(v >> 8));
- buf.push_back(static_cast<uint8_t>(v ));
-}
-
-static uint32_t read_u32(const uint8_t* p)
-{
- return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16)
- | (uint32_t(p[2]) << 8) | uint32_t(p[3]);
-}
-
-static uint64_t read_u64(const uint8_t* p)
-{
- return (uint64_t(p[0]) << 56) | (uint64_t(p[1]) << 48)
- | (uint64_t(p[2]) << 40) | (uint64_t(p[3]) << 32)
- | (uint64_t(p[4]) << 24) | (uint64_t(p[5]) << 16)
- | (uint64_t(p[6]) << 8) | uint64_t(p[7]);
-}
-
-// ── Hex / binary conversion (free, key-agnostic) ─────────────────────────────
-
-Hash32 hex_to_bin(const std::string& hex)
-{
- if (hex.size() != 64)
- throw std::invalid_argument(
- "Merkle: hash must be exactly 64 hex characters, got " +
- std::to_string(hex.size()));
-
- Hash32 out{};
- auto nibble = [](char c) -> uint8_t {
- if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
- if (c >= 'a' && c <= 'f') return static_cast<uint8_t>(c - 'a' + 10);
- if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
- throw std::invalid_argument(std::string("Merkle: invalid hex character '") + c + "'");
- };
- for (size_t i = 0; i < 32; ++i)
- out[i] = static_cast<uint8_t>((nibble(hex[2*i]) << 4) | nibble(hex[2*i+1]));
- return out;
-}
-
-std::string bin_to_hex(const Hash32& bin)
-{
- static const char HEX[] = "0123456789abcdef";
- std::string out(64, '0');
- for (size_t i = 0; i < 32; ++i)
- {
- out[2*i] = HEX[(bin[i] >> 4) & 0xF];
- out[2*i+1] = HEX[bin[i] & 0xF];
+ // ── Wire format constants ────────────────────────────────────────────────────
+
+ static constexpr uint8_t TAG_LEAF = 0x4C; // 'L'
+ static constexpr uint8_t TAG_INTERNAL = 0x49; // 'I'
+
+ // ── Byte-order helpers ───────────────────────────────────────────────────────
+
+ static void write_u32(std::vector<uint8_t>& buf, uint32_t v) {
+ buf.push_back(static_cast<uint8_t>(v >> 24));
+ buf.push_back(static_cast<uint8_t>(v >> 16));
+ buf.push_back(static_cast<uint8_t>(v >> 8));
+ buf.push_back(static_cast<uint8_t>(v));
}
- return out;
-}
-// ── Key codec: per-Key serialisation primitives ──────────────────────────────
+ static void write_u64(std::vector<uint8_t>& buf, uint64_t v) {
+ buf.push_back(static_cast<uint8_t>(v >> 56));
+ buf.push_back(static_cast<uint8_t>(v >> 48));
+ buf.push_back(static_cast<uint8_t>(v >> 40));
+ buf.push_back(static_cast<uint8_t>(v >> 32));
+ buf.push_back(static_cast<uint8_t>(v >> 24));
+ buf.push_back(static_cast<uint8_t>(v >> 16));
+ buf.push_back(static_cast<uint8_t>(v >> 8));
+ buf.push_back(static_cast<uint8_t>(v));
+ }
-namespace {
+ static uint32_t read_u32(const uint8_t* p) {
+ return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) |
+ uint32_t(p[3]);
+ }
-template<typename Key>
-struct KeyCodec; // primary template intentionally undefined
+ static uint64_t read_u64(const uint8_t* p) {
+ return (uint64_t(p[0]) << 56) | (uint64_t(p[1]) << 48) | (uint64_t(p[2]) << 40) |
+ (uint64_t(p[3]) << 32) | (uint64_t(p[4]) << 24) | (uint64_t(p[5]) << 16) |
+ (uint64_t(p[6]) << 8) | uint64_t(p[7]);
+ }
-template<>
-struct KeyCodec<Hash32> {
- static void write(std::vector<uint8_t>& buf, const Hash32& k) {
- buf.insert(buf.end(), k.begin(), k.end());
+ // ── Hex / binary conversion (free, key-agnostic) ─────────────────────────────
+
+ Hash32 hex_to_bin(const std::string& hex) {
+ if (hex.size() != 64)
+ throw std::invalid_argument("Merkle: hash must be exactly 64 hex characters, got " +
+ std::to_string(hex.size()));
+
+ Hash32 out{};
+ auto nibble = [](char c) -> uint8_t {
+ if (c >= '0' && c <= '9')
+ return static_cast<uint8_t>(c - '0');
+ if (c >= 'a' && c <= 'f')
+ return static_cast<uint8_t>(c - 'a' + 10);
+ if (c >= 'A' && c <= 'F')
+ return static_cast<uint8_t>(c - 'A' + 10);
+ throw std::invalid_argument(std::string("Merkle: invalid hex character '") + c + "'");
+ };
+ for (size_t i = 0; i < 32; ++i)
+ out[i] = static_cast<uint8_t>((nibble(hex[2 * i]) << 4) | nibble(hex[2 * i + 1]));
+ return out;
}
- // Returns key and advances *p_inout by the number of consumed bytes.
- static Hash32 read(const uint8_t*& p) {
- Hash32 k;
- std::memcpy(k.data(), p, 32);
- p += 32;
- return k;
+
+ std::string bin_to_hex(const Hash32& bin) {
+ static const char HEX[] = "0123456789abcdef";
+ std::string out(64, '0');
+ for (size_t i = 0; i < 32; ++i) {
+ out[2 * i] = HEX[(bin[i] >> 4) & 0xF];
+ out[2 * i + 1] = HEX[bin[i] & 0xF];
+ }
+ return out;
+ }
+
+ // ── Key codec: per-Key serialisation primitives ──────────────────────────────
+
+ namespace {
+
+ template <typename Key> struct KeyCodec; // primary template intentionally undefined
+
+ template <> struct KeyCodec<Hash32> {
+ static void write(std::vector<uint8_t>& buf, const Hash32& k) {
+ buf.insert(buf.end(), k.begin(), k.end());
+ }
+ // Returns key and advances *p_inout by the number of consumed bytes.
+ static Hash32 read(const uint8_t*& p) {
+ Hash32 k;
+ std::memcpy(k.data(), p, 32);
+ p += 32;
+ return k;
+ }
+ static size_t encoded_size(const Hash32&) {
+ return 32;
+ }
+ };
+
+ template <> struct KeyCodec<std::string> {
+ static void write(std::vector<uint8_t>& buf, const std::string& k) {
+ write_u32(buf, static_cast<uint32_t>(k.size()));
+ buf.insert(buf.end(), k.begin(), k.end());
+ }
+ static std::string read(const uint8_t*& p) {
+ uint32_t len = read_u32(p);
+ p += 4;
+ std::string k(reinterpret_cast<const char*>(p), len);
+ p += len;
+ return k;
+ }
+ static size_t encoded_size(const std::string& k) {
+ return 4 + k.size();
+ }
+ };
+
+ } // anonymous namespace
+
+ // ── Node serialisation ───────────────────────────────────────────────────────
+
+ template <typename Key> std::vector<uint8_t> Merkle<Key>::encode_leaf(const LeafNode& n) {
+ std::vector<uint8_t> buf;
+ size_t payload_size = 0;
+ for (const auto& e : n.entries)
+ payload_size += KeyCodec<Key>::encoded_size(e.key) + 32;
+ buf.reserve(1 + 4 + payload_size);
+
+ buf.push_back(TAG_LEAF);
+ write_u32(buf, static_cast<uint32_t>(n.entries.size()));
+ for (const auto& e : n.entries) {
+ KeyCodec<Key>::write(buf, e.key);
+ buf.insert(buf.end(), e.payload.begin(), e.payload.end());
+ }
+ return buf;
}
- static size_t encoded_size(const Hash32&) { return 32; }
-};
-
-template<>
-struct KeyCodec<std::string> {
- static void write(std::vector<uint8_t>& buf, const std::string& k) {
- write_u32(buf, static_cast<uint32_t>(k.size()));
- buf.insert(buf.end(), k.begin(), k.end());
+
+ template <typename Key>
+ std::vector<uint8_t> Merkle<Key>::encode_internal(const InternalNode& n) {
+ std::vector<uint8_t> buf;
+ size_t payload_size = 0;
+ for (const auto& e : n.entries)
+ payload_size += 8 + KeyCodec<Key>::encoded_size(e.min_key) + 32;
+ buf.reserve(1 + 4 + payload_size);
+
+ buf.push_back(TAG_INTERNAL);
+ write_u32(buf, static_cast<uint32_t>(n.entries.size()));
+ for (const auto& e : n.entries) {
+ write_u64(buf, e.leaf_count);
+ KeyCodec<Key>::write(buf, e.min_key);
+ buf.insert(buf.end(), e.child_hash.begin(), e.child_hash.end());
+ }
+ return buf;
}
- static std::string read(const uint8_t*& p) {
- uint32_t len = read_u32(p);
- p += 4;
- std::string k(reinterpret_cast<const char*>(p), len);
- p += len;
- return k;
+
+ template <typename Key>
+ typename Merkle<Key>::LeafNode Merkle<Key>::decode_leaf(const std::vector<uint8_t>& bytes) {
+ uint32_t count = read_u32(bytes.data() + 1);
+ LeafNode n;
+ n.entries.reserve(count);
+ const uint8_t* p = bytes.data() + 5;
+ for (uint32_t i = 0; i < count; ++i) {
+ LeafEntry e;
+ e.key = KeyCodec<Key>::read(p);
+ std::memcpy(e.payload.data(), p, 32);
+ p += 32;
+ n.entries.push_back(std::move(e));
+ }
+ return n;
}
- static size_t encoded_size(const std::string& k) { return 4 + k.size(); }
-};
-
-} // anonymous namespace
-
-// ── Node serialisation ───────────────────────────────────────────────────────
-
-template<typename Key>
-std::vector<uint8_t> Merkle<Key>::encode_leaf(const LeafNode& n)
-{
- std::vector<uint8_t> buf;
- size_t payload_size = 0;
- for (const auto& e : n.entries)
- payload_size += KeyCodec<Key>::encoded_size(e.key) + 32;
- buf.reserve(1 + 4 + payload_size);
-
- buf.push_back(TAG_LEAF);
- write_u32(buf, static_cast<uint32_t>(n.entries.size()));
- for (const auto& e : n.entries)
- {
- KeyCodec<Key>::write(buf, e.key);
- buf.insert(buf.end(), e.payload.begin(), e.payload.end());
+
+ template <typename Key>
+ typename Merkle<Key>::InternalNode
+ Merkle<Key>::decode_internal(const std::vector<uint8_t>& bytes) {
+ uint32_t count = read_u32(bytes.data() + 1);
+ InternalNode n;
+ n.entries.reserve(count);
+ const uint8_t* p = bytes.data() + 5;
+ for (uint32_t i = 0; i < count; ++i) {
+ ChildEntry e;
+ e.leaf_count = read_u64(p);
+ p += 8;
+ e.min_key = KeyCodec<Key>::read(p);
+ std::memcpy(e.child_hash.data(), p, 32);
+ p += 32;
+ n.entries.push_back(std::move(e));
+ }
+ return n;
}
- return buf;
-}
-
-template<typename Key>
-std::vector<uint8_t> Merkle<Key>::encode_internal(const InternalNode& n)
-{
- std::vector<uint8_t> buf;
- size_t payload_size = 0;
- for (const auto& e : n.entries)
- payload_size += 8 + KeyCodec<Key>::encoded_size(e.min_key) + 32;
- buf.reserve(1 + 4 + payload_size);
-
- buf.push_back(TAG_INTERNAL);
- write_u32(buf, static_cast<uint32_t>(n.entries.size()));
- for (const auto& e : n.entries)
- {
- write_u64(buf, e.leaf_count);
- KeyCodec<Key>::write(buf, e.min_key);
- buf.insert(buf.end(), e.child_hash.begin(), e.child_hash.end());
+
+ template <typename Key> bool Merkle<Key>::is_leaf_bytes(const std::vector<uint8_t>& bytes) {
+ return !bytes.empty() && bytes[0] == TAG_LEAF;
}
- return buf;
-}
-
-template<typename Key>
-typename Merkle<Key>::LeafNode Merkle<Key>::decode_leaf(const std::vector<uint8_t>& bytes)
-{
- uint32_t count = read_u32(bytes.data() + 1);
- LeafNode n;
- n.entries.reserve(count);
- const uint8_t* p = bytes.data() + 5;
- for (uint32_t i = 0; i < count; ++i)
- {
- LeafEntry e;
- e.key = KeyCodec<Key>::read(p);
- std::memcpy(e.payload.data(), p, 32);
- p += 32;
- n.entries.push_back(std::move(e));
+
+ // ── Storage helpers ──────────────────────────────────────────────────────────
+
+ template <typename Key>
+ std::vector<uint8_t> Merkle<Key>::load_node(IStorageBackend& store,
+ const std::string& hash_hex) {
+ auto opt = store.Get(hash_hex);
+ if (!opt)
+ throw std::runtime_error("Merkle: node not found: " + hash_hex);
+ return std::move(*opt);
}
- return n;
-}
-
-template<typename Key>
-typename Merkle<Key>::InternalNode Merkle<Key>::decode_internal(const std::vector<uint8_t>& bytes)
-{
- uint32_t count = read_u32(bytes.data() + 1);
- InternalNode n;
- n.entries.reserve(count);
- const uint8_t* p = bytes.data() + 5;
- for (uint32_t i = 0; i < count; ++i)
- {
- ChildEntry e;
- e.leaf_count = read_u64(p);
- p += 8;
- e.min_key = KeyCodec<Key>::read(p);
- std::memcpy(e.child_hash.data(), p, 32);
- p += 32;
- n.entries.push_back(std::move(e));
+
+ template <typename Key>
+ std::string Merkle<Key>::store_leaf(IStorageBackend& store, const LeafNode& n) {
+ return store.Put(encode_leaf(n));
}
- return n;
-}
-
-template<typename Key>
-bool Merkle<Key>::is_leaf_bytes(const std::vector<uint8_t>& bytes)
-{
- return !bytes.empty() && bytes[0] == TAG_LEAF;
-}
-
-// ── Storage helpers ──────────────────────────────────────────────────────────
-
-template<typename Key>
-std::vector<uint8_t> Merkle<Key>::load_node(IStorageBackend& store,
- const std::string& hash_hex)
-{
- auto opt = store.Get(hash_hex);
- if (!opt)
- throw std::runtime_error("Merkle: node not found: " + hash_hex);
- return std::move(*opt);
-}
-
-template<typename Key>
-std::string Merkle<Key>::store_leaf(IStorageBackend& store, const LeafNode& n)
-{
- return store.Put(encode_leaf(n));
-}
-
-template<typename Key>
-std::string Merkle<Key>::store_internal(IStorageBackend& store, const InternalNode& n)
-{
- return store.Put(encode_internal(n));
-}
-
-template<typename Key>
-Key Merkle<Key>::subtree_min_key(IStorageBackend& store, const std::string& node_hex)
-{
- auto bytes = load_node(store, node_hex);
- if (is_leaf_bytes(bytes))
- {
- auto leaf = decode_leaf(bytes);
- assert(!leaf.entries.empty() && "Merkle invariant: leaf node must never be empty");
- return leaf.entries.front().key;
+
+ template <typename Key>
+ std::string Merkle<Key>::store_internal(IStorageBackend& store, const InternalNode& n) {
+ return store.Put(encode_internal(n));
}
- auto node = decode_internal(bytes);
- assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
- return node.entries.front().min_key;
-}
-
-// ── Routing helper ───────────────────────────────────────────────────────────
-
-template<typename Key>
-size_t Merkle<Key>::route(const InternalNode& node, const Key& target)
-{
- assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
- assert(std::is_sorted(node.entries.begin(), node.entries.end(),
- [](const ChildEntry& a, const ChildEntry& b) {
- return a.min_key < b.min_key;
- }) && "Merkle invariant: internal node entries must be sorted by min_key");
-
- // Rightmost child whose min_key ≤ target.
- // Defaults to 0 (leftmost) when target < all min_keys.
- size_t idx = 0;
- for (size_t i = 1; i < node.entries.size(); ++i)
- {
- if (!(target < node.entries[i].min_key))
- idx = i;
- else
- break;
+
+ template <typename Key>
+ Key Merkle<Key>::subtree_min_key(IStorageBackend& store, const std::string& node_hex) {
+ auto bytes = load_node(store, node_hex);
+ if (is_leaf_bytes(bytes)) {
+ auto leaf = decode_leaf(bytes);
+ assert(!leaf.entries.empty() && "Merkle invariant: leaf node must never be empty");
+ return leaf.entries.front().key;
+ }
+ auto node = decode_internal(bytes);
+ assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
+ return node.entries.front().min_key;
}
- return idx;
-}
-
-// ── Insert ───────────────────────────────────────────────────────────────────
-
-template<typename Key>
-std::string Merkle<Key>::Insert(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key,
- const Hash32& payload)
-{
- if (root_hex.empty())
- {
- LeafNode leaf;
- leaf.entries.push_back({ key, payload });
- return store_leaf(store, leaf);
+
+ // ── Routing helper ───────────────────────────────────────────────────────────
+
+ template <typename Key> size_t Merkle<Key>::route(const InternalNode& node, const Key& target) {
+ assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
+ assert(std::is_sorted(node.entries.begin(), node.entries.end(),
+ [](const ChildEntry& a, const ChildEntry& b) {
+ return a.min_key < b.min_key;
+ }) &&
+ "Merkle invariant: internal node entries must be sorted by min_key");
+
+ // Rightmost child whose min_key ≤ target.
+ // Defaults to 0 (leftmost) when target < all min_keys.
+ size_t idx = 0;
+ for (size_t i = 1; i < node.entries.size(); ++i) {
+ if (!(target < node.entries[i].min_key))
+ idx = i;
+ else
+ break;
+ }
+ return idx;
}
- auto result = insert_into(store, root_hex, key, payload);
- if (!result.did_split)
- return result.new_hash;
+ // ── Insert ───────────────────────────────────────────────────────────────────
+
+ template <typename Key>
+ std::string Merkle<Key>::Insert(IStorageBackend& store, const std::string& root_hex,
+ const Key& key, const Hash32& payload) {
+ if (root_hex.empty()) {
+ LeafNode leaf;
+ leaf.entries.push_back({key, payload});
+ return store_leaf(store, leaf);
+ }
+
+ auto result = insert_into(store, root_hex, key, payload);
+ if (!result.did_split)
+ return result.new_hash;
+
+ // Root split: wrap both halves in a new internal root.
+ Key left_min = subtree_min_key(store, result.new_hash);
+
+ InternalNode new_root;
+
+ ChildEntry left;
+ left.leaf_count = result.leaf_count;
+ left.min_key = left_min;
+ left.child_hash = hex_to_bin(result.new_hash);
+ new_root.entries.push_back(std::move(left));
+
+ ChildEntry right;
+ right.leaf_count = result.split_leaf_count;
+ right.min_key = result.split_min_key;
+ right.child_hash = hex_to_bin(result.split_hash);
+ new_root.entries.push_back(std::move(right));
+
+ return store_internal(store, new_root);
+ }
+
+ template <typename Key>
+ typename Merkle<Key>::InsertResult
+ Merkle<Key>::insert_into(IStorageBackend& store, const std::string& node_hex, const Key& key,
+ const Hash32& payload) {
+ auto bytes = load_node(store, node_hex);
+
+ // ── Leaf node ───────────────────────────────────────────────────────────
+ if (is_leaf_bytes(bytes)) {
+ auto leaf = decode_leaf(bytes);
+
+ auto pos = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
+ [](const LeafEntry& e, const Key& k) { return e.key < k; });
+ if (pos != leaf.entries.end() && !(key < pos->key)) {
+ // Key already present — update payload if it changed, else no-op.
+ if (pos->payload == payload) {
+ InsertResult r;
+ r.new_hash = node_hex;
+ r.leaf_count = leaf.entries.size();
+ return r;
+ }
+ pos->payload = payload;
+ InsertResult r;
+ r.new_hash = store_leaf(store, leaf);
+ r.leaf_count = leaf.entries.size();
+ return r;
+ }
+
+ leaf.entries.insert(pos, LeafEntry{key, payload});
- // Root split: wrap both halves in a new internal root.
- Key left_min = subtree_min_key(store, result.new_hash);
-
- InternalNode new_root;
-
- ChildEntry left;
- left.leaf_count = result.leaf_count;
- left.min_key = left_min;
- left.child_hash = hex_to_bin(result.new_hash);
- new_root.entries.push_back(std::move(left));
-
- ChildEntry right;
- right.leaf_count = result.split_leaf_count;
- right.min_key = result.split_min_key;
- right.child_hash = hex_to_bin(result.split_hash);
- new_root.entries.push_back(std::move(right));
-
- return store_internal(store, new_root);
-}
-
-template<typename Key>
-typename Merkle<Key>::InsertResult Merkle<Key>::insert_into(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key,
- const Hash32& payload)
-{
- auto bytes = load_node(store, node_hex);
-
- // ── Leaf node ───────────────────────────────────────────────────────────
- if (is_leaf_bytes(bytes))
- {
- auto leaf = decode_leaf(bytes);
-
- auto pos = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
- [](const LeafEntry& e, const Key& k) {
- return e.key < k;
- });
- if (pos != leaf.entries.end() && !(key < pos->key))
- {
- // Key already present — update payload if it changed, else no-op.
- if (pos->payload == payload)
- {
+ if (leaf.entries.size() <= B) {
InsertResult r;
- r.new_hash = node_hex;
+ r.new_hash = store_leaf(store, leaf);
r.leaf_count = leaf.entries.size();
return r;
}
- pos->payload = payload;
+
+ // Split leaf at midpoint.
+ size_t mid = leaf.entries.size() / 2;
+ LeafNode left_leaf, right_leaf;
+ left_leaf.entries = {leaf.entries.begin(),
+ leaf.entries.begin() + static_cast<ptrdiff_t>(mid)};
+ right_leaf.entries = {leaf.entries.begin() + static_cast<ptrdiff_t>(mid),
+ leaf.entries.end()};
+
InsertResult r;
- r.new_hash = store_leaf(store, leaf);
- r.leaf_count = leaf.entries.size();
+ r.new_hash = store_leaf(store, left_leaf);
+ r.leaf_count = left_leaf.entries.size();
+ r.did_split = true;
+ r.split_leaf_count = right_leaf.entries.size();
+ r.split_min_key = right_leaf.entries.front().key;
+ r.split_hash = store_leaf(store, right_leaf);
return r;
}
- leaf.entries.insert(pos, LeafEntry{ key, payload });
+ // ── Internal node ───────────────────────────────────────────────────────
+ auto node = decode_internal(bytes);
+
+ size_t child_idx = route(node, key);
+ auto child_result =
+ insert_into(store, bin_to_hex(node.entries[child_idx].child_hash), key, payload);
- if (leaf.entries.size() <= B)
- {
+ node.entries[child_idx].leaf_count = child_result.leaf_count;
+ node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
+
+ // Update min_key for the modified child — only child 0 can have its min
+ // decrease (routing guarantees min_key[i>0] ≤ inserted key, so inserting
+ // below min_key[i>0] is impossible).
+ if (child_idx == 0)
+ node.entries[0].min_key = subtree_min_key(store, child_result.new_hash);
+
+ uint64_t total = 0;
+ for (const auto& e : node.entries)
+ total += e.leaf_count;
+
+ if (!child_result.did_split) {
+ InsertResult r;
+ r.new_hash = store_internal(store, node);
+ r.leaf_count = total;
+ return r;
+ }
+
+ // Insert the right sibling immediately after child_idx.
+ ChildEntry sibling;
+ sibling.leaf_count = child_result.split_leaf_count;
+ sibling.min_key = child_result.split_min_key;
+ sibling.child_hash = hex_to_bin(child_result.split_hash);
+ node.entries.insert(node.entries.begin() + static_cast<ptrdiff_t>(child_idx) + 1,
+ std::move(sibling));
+ total += child_result.split_leaf_count;
+
+ if (node.entries.size() <= B) {
InsertResult r;
- r.new_hash = store_leaf(store, leaf);
- r.leaf_count = leaf.entries.size();
+ r.new_hash = store_internal(store, node);
+ r.leaf_count = total;
return r;
}
- // Split leaf at midpoint.
- size_t mid = leaf.entries.size() / 2;
- LeafNode left_leaf, right_leaf;
- left_leaf.entries = { leaf.entries.begin(),
- leaf.entries.begin() + static_cast<ptrdiff_t>(mid) };
- right_leaf.entries = { leaf.entries.begin() + static_cast<ptrdiff_t>(mid),
- leaf.entries.end() };
+ // Split this internal node.
+ size_t mid = node.entries.size() / 2;
+ InternalNode left_node, right_node;
+ left_node.entries = {node.entries.begin(),
+ node.entries.begin() + static_cast<ptrdiff_t>(mid)};
+ right_node.entries = {node.entries.begin() + static_cast<ptrdiff_t>(mid),
+ node.entries.end()};
+
+ uint64_t left_count = 0, right_count = 0;
+ for (const auto& e : left_node.entries)
+ left_count += e.leaf_count;
+ for (const auto& e : right_node.entries)
+ right_count += e.leaf_count;
InsertResult r;
- r.new_hash = store_leaf(store, left_leaf);
- r.leaf_count = left_leaf.entries.size();
- r.did_split = true;
- r.split_leaf_count = right_leaf.entries.size();
- r.split_min_key = right_leaf.entries.front().key;
- r.split_hash = store_leaf(store, right_leaf);
+ r.new_hash = store_internal(store, left_node);
+ r.leaf_count = left_count;
+ r.did_split = true;
+ r.split_leaf_count = right_count;
+ r.split_min_key = right_node.entries.front().min_key;
+ r.split_hash = store_internal(store, right_node);
return r;
}
- // ── Internal node ───────────────────────────────────────────────────────
- auto node = decode_internal(bytes);
-
- size_t child_idx = route(node, key);
- auto child_result = insert_into(store,
- bin_to_hex(node.entries[child_idx].child_hash),
- key,
- payload);
+ // ── Remove ───────────────────────────────────────────────────────────────────
+
+ template <typename Key>
+ std::string Merkle<Key>::Remove(IStorageBackend& store, const std::string& root_hex,
+ const Key& key) {
+ if (root_hex.empty())
+ return "";
+ auto result = remove_from(store, root_hex, key);
+ if (result.new_hash.empty())
+ return "";
+
+ // Collapse: if the root is an internal node with a single child, skip it.
+ auto bytes = load_node(store, result.new_hash);
+ if (!is_leaf_bytes(bytes)) {
+ auto node = decode_internal(bytes);
+ if (node.entries.size() == 1)
+ return bin_to_hex(node.entries[0].child_hash);
+ }
+ return result.new_hash;
+ }
- node.entries[child_idx].leaf_count = child_result.leaf_count;
- node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
+ template <typename Key>
+ typename Merkle<Key>::RemoveResult
+ Merkle<Key>::remove_from(IStorageBackend& store, const std::string& node_hex, const Key& key) {
+ auto bytes = load_node(store, node_hex);
+
+ // ── Leaf node ───────────────────────────────────────────────────────────
+ if (is_leaf_bytes(bytes)) {
+ auto leaf = decode_leaf(bytes);
+ auto it = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
+ [](const LeafEntry& e, const Key& k) { return e.key < k; });
+ if (it == leaf.entries.end() || key < it->key)
+ return {node_hex, leaf.entries.size()}; // not found — no change
+
+ leaf.entries.erase(it);
+ if (leaf.entries.empty())
+ return {"", 0};
+ return {store_leaf(store, leaf), leaf.entries.size()};
+ }
- // Update min_key for the modified child — only child 0 can have its min
- // decrease (routing guarantees min_key[i>0] ≤ inserted key, so inserting
- // below min_key[i>0] is impossible).
- if (child_idx == 0)
- node.entries[0].min_key = subtree_min_key(store, child_result.new_hash);
+ // ── Internal node ───────────────────────────────────────────────────────
+ auto node = decode_internal(bytes);
- uint64_t total = 0;
- for (const auto& e : node.entries) total += e.leaf_count;
+ size_t child_idx = route(node, key);
+ auto child_result = remove_from(store, bin_to_hex(node.entries[child_idx].child_hash), key);
+
+ if (child_result.new_hash.empty()) {
+ node.entries.erase(node.entries.begin() + static_cast<ptrdiff_t>(child_idx));
+ if (node.entries.empty())
+ return {"", 0};
+ } else {
+ node.entries[child_idx].leaf_count = child_result.leaf_count;
+ node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
+ // The min of any child might have changed if we removed its minimum entry.
+ node.entries[child_idx].min_key = subtree_min_key(store, child_result.new_hash);
+ }
- if (!child_result.did_split)
- {
- InsertResult r;
- r.new_hash = store_internal(store, node);
- r.leaf_count = total;
- return r;
+ uint64_t total = 0;
+ for (const auto& e : node.entries)
+ total += e.leaf_count;
+ return {store_internal(store, node), total};
}
- // Insert the right sibling immediately after child_idx.
- ChildEntry sibling;
- sibling.leaf_count = child_result.split_leaf_count;
- sibling.min_key = child_result.split_min_key;
- sibling.child_hash = hex_to_bin(child_result.split_hash);
- node.entries.insert(node.entries.begin() + static_cast<ptrdiff_t>(child_idx) + 1,
- std::move(sibling));
- total += child_result.split_leaf_count;
-
- if (node.entries.size() <= B)
- {
- InsertResult r;
- r.new_hash = store_internal(store, node);
- r.leaf_count = total;
- return r;
+ // ── Get ──────────────────────────────────────────────────────────────────────
+
+ template <typename Key>
+ std::optional<Hash32> Merkle<Key>::Get(IStorageBackend& store, const std::string& root_hex,
+ const Key& key) {
+ if (root_hex.empty())
+ return std::nullopt;
+ return get_from(store, root_hex, key);
}
- // Split this internal node.
- size_t mid = node.entries.size() / 2;
- InternalNode left_node, right_node;
- left_node.entries = { node.entries.begin(),
- node.entries.begin() + static_cast<ptrdiff_t>(mid) };
- right_node.entries = { node.entries.begin() + static_cast<ptrdiff_t>(mid),
- node.entries.end() };
-
- uint64_t left_count = 0, right_count = 0;
- for (const auto& e : left_node.entries) left_count += e.leaf_count;
- for (const auto& e : right_node.entries) right_count += e.leaf_count;
-
- InsertResult r;
- r.new_hash = store_internal(store, left_node);
- r.leaf_count = left_count;
- r.did_split = true;
- r.split_leaf_count = right_count;
- r.split_min_key = right_node.entries.front().min_key;
- r.split_hash = store_internal(store, right_node);
- return r;
-}
-
-// ── Remove ───────────────────────────────────────────────────────────────────
-
-template<typename Key>
-std::string Merkle<Key>::Remove(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key)
-{
- if (root_hex.empty()) return "";
- auto result = remove_from(store, root_hex, key);
- if (result.new_hash.empty()) return "";
-
- // Collapse: if the root is an internal node with a single child, skip it.
- auto bytes = load_node(store, result.new_hash);
- if (!is_leaf_bytes(bytes))
- {
+ template <typename Key>
+ std::optional<Hash32> Merkle<Key>::get_from(IStorageBackend& store, const std::string& node_hex,
+ const Key& key) {
+ auto bytes = load_node(store, node_hex);
+ if (is_leaf_bytes(bytes)) {
+ auto leaf = decode_leaf(bytes);
+ auto it = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
+ [](const LeafEntry& e, const Key& k) { return e.key < k; });
+ if (it == leaf.entries.end() || key < it->key)
+ return std::nullopt;
+ return it->payload;
+ }
auto node = decode_internal(bytes);
- if (node.entries.size() == 1)
- return bin_to_hex(node.entries[0].child_hash);
+ size_t child_idx = route(node, key);
+ return get_from(store, bin_to_hex(node.entries[child_idx].child_hash), key);
}
- return result.new_hash;
-}
-
-template<typename Key>
-typename Merkle<Key>::RemoveResult Merkle<Key>::remove_from(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key)
-{
- auto bytes = load_node(store, node_hex);
-
- // ── Leaf node ───────────────────────────────────────────────────────────
- if (is_leaf_bytes(bytes))
- {
- auto leaf = decode_leaf(bytes);
- auto it = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
- [](const LeafEntry& e, const Key& k) {
- return e.key < k;
- });
- if (it == leaf.entries.end() || key < it->key)
- return { node_hex, leaf.entries.size() }; // not found — no change
-
- leaf.entries.erase(it);
- if (leaf.entries.empty())
- return { "", 0 };
- return { store_leaf(store, leaf), leaf.entries.size() };
+
+ // ── Page ─────────────────────────────────────────────────────────────────────
+
+ template <typename Key>
+ std::vector<typename Merkle<Key>::Entry> Merkle<Key>::Page(IStorageBackend& store,
+ const std::string& root_hex,
+ size_t offset, size_t limit) {
+ if (root_hex.empty() || limit == 0)
+ return {};
+ std::vector<Entry> out;
+ out.reserve(limit);
+ size_t skip = offset;
+ page_from(store, root_hex, skip, limit, out);
+ return out;
}
- // ── Internal node ───────────────────────────────────────────────────────
- auto node = decode_internal(bytes);
+ template <typename Key>
+ void Merkle<Key>::page_from(IStorageBackend& store, const std::string& node_hex, size_t& offset,
+ size_t limit, std::vector<Entry>& out) {
+ auto bytes = load_node(store, node_hex);
- size_t child_idx = route(node, key);
- auto child_result = remove_from(store,
- bin_to_hex(node.entries[child_idx].child_hash),
- key);
+ if (is_leaf_bytes(bytes)) {
+ auto leaf = decode_leaf(bytes);
+ const size_t available = leaf.entries.size();
- if (child_result.new_hash.empty())
- {
- node.entries.erase(node.entries.begin() + static_cast<ptrdiff_t>(child_idx));
- if (node.entries.empty()) return { "", 0 };
- }
- else
- {
- node.entries[child_idx].leaf_count = child_result.leaf_count;
- node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
- // The min of any child might have changed if we removed its minimum entry.
- node.entries[child_idx].min_key = subtree_min_key(store, child_result.new_hash);
- }
+ if (offset >= available) {
+ offset -= available;
+ return;
+ }
- uint64_t total = 0;
- for (const auto& e : node.entries) total += e.leaf_count;
- return { store_internal(store, node), total };
-}
-
-// ── Get ──────────────────────────────────────────────────────────────────────
-
-template<typename Key>
-std::optional<Hash32> Merkle<Key>::Get(IStorageBackend& store,
- const std::string& root_hex,
- const Key& key)
-{
- if (root_hex.empty()) return std::nullopt;
- return get_from(store, root_hex, key);
-}
-
-template<typename Key>
-std::optional<Hash32> Merkle<Key>::get_from(IStorageBackend& store,
- const std::string& node_hex,
- const Key& key)
-{
- auto bytes = load_node(store, node_hex);
- if (is_leaf_bytes(bytes))
- {
- auto leaf = decode_leaf(bytes);
- auto it = std::lower_bound(leaf.entries.begin(), leaf.entries.end(), key,
- [](const LeafEntry& e, const Key& k) {
- return e.key < k;
- });
- if (it == leaf.entries.end() || key < it->key)
- return std::nullopt;
- return it->payload;
- }
- auto node = decode_internal(bytes);
- size_t child_idx = route(node, key);
- return get_from(store, bin_to_hex(node.entries[child_idx].child_hash), key);
-}
-
-// ── Page ─────────────────────────────────────────────────────────────────────
-
-template<typename Key>
-std::vector<typename Merkle<Key>::Entry> Merkle<Key>::Page(IStorageBackend& store,
- const std::string& root_hex,
- size_t offset,
- size_t limit)
-{
- if (root_hex.empty() || limit == 0) return {};
- std::vector<Entry> out;
- out.reserve(limit);
- size_t skip = offset;
- page_from(store, root_hex, skip, limit, out);
- return out;
-}
-
-template<typename Key>
-void Merkle<Key>::page_from(IStorageBackend& store,
- const std::string& node_hex,
- size_t& offset,
- size_t limit,
- std::vector<Entry>& out)
-{
- auto bytes = load_node(store, node_hex);
-
- if (is_leaf_bytes(bytes))
- {
- auto leaf = decode_leaf(bytes);
- const size_t available = leaf.entries.size();
-
- if (offset >= available)
- {
- offset -= available;
+ for (size_t i = offset; i < available && out.size() < limit; ++i)
+ out.push_back(Entry{leaf.entries[i].key, leaf.entries[i].payload});
+
+ offset = 0;
return;
}
- for (size_t i = offset; i < available && out.size() < limit; ++i)
- out.push_back(Entry{ leaf.entries[i].key, leaf.entries[i].payload });
-
- offset = 0;
- return;
- }
+ auto node = decode_internal(bytes);
- auto node = decode_internal(bytes);
+ for (const auto& entry : node.entries) {
+ if (out.size() >= limit)
+ break;
- for (const auto& entry : node.entries)
- {
- if (out.size() >= limit) break;
+ if (offset >= entry.leaf_count) {
+ offset -= entry.leaf_count;
+ continue;
+ }
- if (offset >= entry.leaf_count)
- {
- offset -= entry.leaf_count;
- continue;
+ page_from(store, bin_to_hex(entry.child_hash), offset, limit, out);
}
-
- page_from(store, bin_to_hex(entry.child_hash), offset, limit, out);
}
-}
-// ── Explicit instantiations ──────────────────────────────────────────────────
+ // ── Explicit instantiations ──────────────────────────────────────────────────
-template class Merkle<Hash32>;
-template class Merkle<std::string>;
+ template class Merkle<Hash32>;
+ template class Merkle<std::string>;
} // namespace nt
diff --git a/src/MultigroupCodec.cpp b/src/MultigroupCodec.cpp
index 3deaa23..0d0cf14 100644
--- a/src/MultigroupCodec.cpp
+++ b/src/MultigroupCodec.cpp
@@ -4,81 +4,68 @@
#include <map>
-namespace nt::MultigroupCodec
-{
+namespace nt::MultigroupCodec {
// A relation with zero tuples reports an empty merkle_root externally.
// The merkle tree's payload type is fixed-width Hash32, so the codec
// maps "" ↔ the all-zero hash. The collision probability with a real
// SHA256 hitting all-zero (2^-256) is treated as unreachable.
- static nt::Hash32 encode_payload(const std::string& hex)
- {
- if (hex.empty()) return nt::Hash32{};
+ static nt::Hash32 encode_payload(const std::string& hex) {
+ if (hex.empty())
+ return nt::Hash32{};
return nt::hex_to_bin(hex);
}
- static std::string decode_payload(const nt::Hash32& bin)
- {
+ static std::string decode_payload(const nt::Hash32& bin) {
static const nt::Hash32 zero{};
- if (bin == zero) return "";
+ if (bin == zero)
+ return "";
return nt::bin_to_hex(bin);
}
- std::string Build(IStorageBackend& store,
- const std::vector<RelationEntry>& relations)
- {
+ std::string Build(IStorageBackend& store, const std::vector<RelationEntry>& relations) {
std::map<std::string, std::string> canonical;
for (const auto& [name, root_hex] : relations)
canonical[name] = root_hex;
std::string root;
for (const auto& [name, root_hex] : canonical)
- root = nt::Merkle<std::string>::Insert(store, root, name,
- encode_payload(root_hex));
+ root = nt::Merkle<std::string>::Insert(store, root, name, encode_payload(root_hex));
return root;
}
- std::vector<RelationEntry> List(IStorageBackend& store,
- const std::string& root_hex)
- {
+ std::vector<RelationEntry> List(IStorageBackend& store, const std::string& root_hex) {
std::vector<RelationEntry> out;
constexpr size_t kPageSize = 1024;
size_t offset = 0;
- while (true)
- {
- auto page = nt::Merkle<std::string>::Page(store, root_hex,
- offset, kPageSize);
- if (page.empty()) break;
+ while (true) {
+ auto page = nt::Merkle<std::string>::Page(store, root_hex, offset, kPageSize);
+ if (page.empty())
+ break;
for (auto& entry : page)
- out.emplace_back(std::move(entry.key),
- decode_payload(entry.payload));
- if (page.size() < kPageSize) break;
+ out.emplace_back(std::move(entry.key), decode_payload(entry.payload));
+ if (page.size() < kPageSize)
+ break;
offset += page.size();
}
return out;
}
- std::string InsertOne(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& relation_name,
- const std::string& relation_root_hex)
- {
+ std::string InsertOne(IStorageBackend& store, const std::string& root_hex,
+ const std::string& relation_name, const std::string& relation_root_hex) {
return nt::Merkle<std::string>::Insert(store, root_hex, relation_name,
- encode_payload(relation_root_hex));
+ encode_payload(relation_root_hex));
}
- std::string RemoveOne(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& relation_name)
- {
+ std::string RemoveOne(IStorageBackend& store, const std::string& root_hex,
+ const std::string& relation_name) {
return nt::Merkle<std::string>::Remove(store, root_hex, relation_name);
}
- std::string Lookup(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& relation_name)
- {
+ std::string Lookup(IStorageBackend& store, const std::string& root_hex,
+ const std::string& relation_name) {
auto found = nt::Merkle<std::string>::Get(store, root_hex, relation_name);
- if (!found) return "";
+ if (!found)
+ return "";
return decode_payload(*found);
}
-}
+} // namespace nt::MultigroupCodec
diff --git a/src/NamespaceReferenceManager.cpp b/src/NamespaceReferenceManager.cpp
index 455d077..0d99b73 100644
--- a/src/NamespaceReferenceManager.cpp
+++ b/src/NamespaceReferenceManager.cpp
@@ -2,58 +2,48 @@
#include "Merkle.h"
-namespace nt
-{
+namespace nt {
NamespaceReferenceManager::NamespaceReferenceManager(ObjectManager& objects,
- IStorageBackend& storage)
- : objects_(objects), storage_(storage)
- {}
+ IStorageBackend& storage)
+ : objects_(objects), storage_(storage) {
+ }
// Resolves the multigroup hash that `mg_name` currently maps to inside the
// branch-tree rooted at `branch_tree_root`. Returns the empty string when
// the branch is unborn (empty root) or the multigroup is absent.
- static std::string lookup_mg_hash(IStorageBackend& storage,
- const std::string& branch_tree_root,
- const std::string& mg_name)
- {
- if (branch_tree_root.empty()) return "";
+ static std::string lookup_mg_hash(IStorageBackend& storage, const std::string& branch_tree_root,
+ const std::string& mg_name) {
+ if (branch_tree_root.empty())
+ return "";
auto found = Merkle<std::string>::Get(storage, branch_tree_root, mg_name);
- if (!found) return "";
+ if (!found)
+ return "";
// All-zero payload = "no snapshot bound" sentinel matching MultigroupCodec.
static const Hash32 zero{};
- if (*found == zero) return "";
+ if (*found == zero)
+ return "";
return bin_to_hex(*found);
}
- std::vector<std::string> NamespaceReferenceManager::Resolve(
- std::vector<std::string> path) const
- {
+ std::vector<std::string>
+ NamespaceReferenceManager::Resolve(std::vector<std::string> path) const {
constexpr int kMaxReparseDepth = 30;
- for (int depth = 0; depth < kMaxReparseDepth; ++depth)
- {
+ for (int depth = 0; depth < kMaxReparseDepth; ++depth) {
// /system/sessions/<X>/branches/<name>/multigroups/<mg>/<sub>...
// — per-session override takes precedence; absent override falls
// back to the global branch. Requires at minimum the literal
// "multigroups" qualifier and an mg name.
- if (path.size() > 6
- && path[0] == "system"
- && path[1] == "sessions"
- && path[3] == "branches"
- && path[5] == "multigroups")
- {
- const std::vector<std::string> session_path{
- "system", "sessions", path[2]
- };
+ if (path.size() > 6 && path[0] == "system" && path[1] == "sessions" &&
+ path[3] == "branches" && path[5] == "multigroups") {
+ const std::vector<std::string> session_path{"system", "sessions", path[2]};
auto* session_entry = objects_.Find(session_path);
std::string branch_tree_root;
- if (session_entry && session_entry->object)
- {
- auto* session = dynamic_cast<ObjectManager::Session*>(
- session_entry->object.get());
- if (session)
- {
+ if (session_entry && session_entry->object) {
+ auto* session =
+ dynamic_cast<ObjectManager::Session*>(session_entry->object.get());
+ if (session) {
auto it = session->branch_overrides.find(path[4]);
if (it != session->branch_overrides.end())
branch_tree_root = it->second;
@@ -61,27 +51,21 @@ namespace nt
}
// No session-level override — defer to the global branch's HEAD.
- if (branch_tree_root.empty())
- {
- const std::vector<std::string> branch_path{
- "system", "branches", path[4]
- };
+ if (branch_tree_root.empty()) {
+ const std::vector<std::string> branch_path{"system", "branches", path[4]};
auto* bentry = objects_.Find(branch_path);
- if (bentry && bentry->object)
- {
- auto* branch = dynamic_cast<ObjectManager::Branch*>(
- bentry->object.get());
- if (branch) branch_tree_root = branch->target_hash;
+ if (bentry && bentry->object) {
+ auto* branch = dynamic_cast<ObjectManager::Branch*>(bentry->object.get());
+ if (branch)
+ branch_tree_root = branch->target_hash;
}
}
- const std::string mg_hash =
- lookup_mg_hash(storage_, branch_tree_root, path[6]);
- if (mg_hash.empty()) return path;
+ const std::string mg_hash = lookup_mg_hash(storage_, branch_tree_root, path[6]);
+ if (mg_hash.empty())
+ return path;
- std::vector<std::string> rewritten{
- "system", "snapshots", mg_hash
- };
+ std::vector<std::string> rewritten{"system", "snapshots", mg_hash};
for (size_t i = 7; i < path.size(); ++i)
rewritten.push_back(path[i]);
@@ -94,28 +78,22 @@ namespace nt
// Requires the literal "multigroups" qualifier and an mg name. A
// bare /system/branches/<name> path opens the branch object itself
// and is returned unchanged.
- if (path.size() > 4
- && path[0] == "system"
- && path[1] == "branches"
- && path[3] == "multigroups")
- {
- const std::vector<std::string> branch_path{
- "system", "branches", path[2]
- };
+ if (path.size() > 4 && path[0] == "system" && path[1] == "branches" &&
+ path[3] == "multigroups") {
+ const std::vector<std::string> branch_path{"system", "branches", path[2]};
auto* entry = objects_.Find(branch_path);
- if (!entry || !entry->object) return path;
+ if (!entry || !entry->object)
+ return path;
- auto* branch =
- dynamic_cast<ObjectManager::Branch*>(entry->object.get());
- if (!branch) return path;
+ auto* branch = dynamic_cast<ObjectManager::Branch*>(entry->object.get());
+ if (!branch)
+ return path;
- const std::string mg_hash =
- lookup_mg_hash(storage_, branch->target_hash, path[4]);
- if (mg_hash.empty()) return path;
+ const std::string mg_hash = lookup_mg_hash(storage_, branch->target_hash, path[4]);
+ if (mg_hash.empty())
+ return path;
- std::vector<std::string> rewritten{
- "system", "snapshots", mg_hash
- };
+ std::vector<std::string> rewritten{"system", "snapshots", mg_hash};
for (size_t i = 5; i < path.size(); ++i)
rewritten.push_back(path[i]);
@@ -129,4 +107,4 @@ namespace nt
// Cycle guard tripped — surface as a lookup miss.
return {};
}
-}
+} // namespace nt
diff --git a/src/ObjectManager.cpp b/src/ObjectManager.cpp
index 67504c3..70e622b 100644
--- a/src/ObjectManager.cpp
+++ b/src/ObjectManager.cpp
@@ -1,11 +1,8 @@
#include "ObjectManager.h"
-namespace nt
-{
- void ObjectManager::Register(std::vector<std::string> path,
- std::unique_ptr<IObject> object,
- std::unique_ptr<object_type> type)
- {
+namespace nt {
+ void ObjectManager::Register(std::vector<std::string> path, std::unique_ptr<IObject> object,
+ std::unique_ptr<object_type> type) {
auto head = std::make_unique<registry_head>();
head->reference_count = 0;
head->handle_count = 0;
@@ -19,11 +16,9 @@ namespace nt
entries = std::move(entry);
}
- ObjectManager::registry* ObjectManager::Find(const std::vector<std::string> object_path)
- {
+ ObjectManager::registry* ObjectManager::Find(const std::vector<std::string> object_path) {
registry* current = entries.get();
- while (current != nullptr)
- {
+ while (current != nullptr) {
if (current->head->path == object_path)
return current;
current = current->next.get();
@@ -31,13 +26,10 @@ namespace nt
return nullptr;
}
- bool ObjectManager::Unregister(const std::vector<std::string>& object_path)
- {
+ bool ObjectManager::Unregister(const std::vector<std::string>& object_path) {
std::unique_ptr<registry>* slot = &entries;
- while (slot->get() != nullptr)
- {
- if ((*slot)->head->path == object_path)
- {
+ while (slot->get() != nullptr) {
+ if ((*slot)->head->path == object_path) {
*slot = std::move((*slot)->next);
return true;
}
@@ -45,4 +37,4 @@ namespace nt
}
return false;
}
-}
+} // namespace nt
diff --git a/src/PermissionsManager.cpp b/src/PermissionsManager.cpp
index ddd49eb..bcdfdd7 100644
--- a/src/PermissionsManager.cpp
+++ b/src/PermissionsManager.cpp
@@ -1,19 +1,17 @@
#include "PermissionsManager.h"
-namespace nt
-{
- std::set<AUTH_CLAIM> PermissionsManager::Firewall(AUTH_METHOD method)
- {
+namespace nt {
+ std::set<AUTH_CLAIM> PermissionsManager::Firewall(AUTH_METHOD method) {
return {};
}
- const bool PermissionsManager::Access(const ObjectManager::registry* object, const void* connection_context)
- {
- // Checks the security descriptor on the head of the object we find in the ObjectManager::registry
- // Then additionally check other metadata yet to be defined in the connection_context,
- // like user group, policy, etc.
- // Yet to decide if this should be summoned at every attempt to get ahold of an object,
- // that might produce significant overhead. How can we solve this?
+ const bool PermissionsManager::Access(const ObjectManager::registry* object,
+ const void* connection_context) {
+ // Checks the security descriptor on the head of the object we find in the
+ // ObjectManager::registry Then additionally check other metadata yet to be defined in the
+ // connection_context, like user group, policy, etc. Yet to decide if this should be
+ // summoned at every attempt to get ahold of an object, that might produce significant
+ // overhead. How can we solve this?
return object != nullptr;
}
-}
+} // namespace nt
diff --git a/src/RNT_C_API.cpp b/src/RNT_C_API.cpp
index c92d03b..abd683c 100644
--- a/src/RNT_C_API.cpp
+++ b/src/RNT_C_API.cpp
@@ -3,13 +3,13 @@
#include "CursorManager.h"
#include "HandlerManager.h"
#include "IdentityManager.h"
+#include "InMemoryBackend.h"
#include "LifecycleManager.h"
#include "Merkle.h"
#include "MultigroupCodec.h"
#include "NamespaceReferenceManager.h"
#include "ObjectManager.h"
#include "PermissionsManager.h"
-#include "InMemoryBackend.h"
#include "SqliteBackend.h"
#include "TupleCodec.h"
#include "VM.h"
@@ -34,129 +34,119 @@
// contended again.
// ---------------------------------------------------------------------------
-namespace
-{
- struct Runtime
- {
+namespace {
+ struct Runtime {
std::unique_ptr<nt::IStorageBackend> storage;
- nt::ObjectManager objects;
+ nt::ObjectManager objects;
nt::PermissionsManager permissions;
- nt::IdentityManager identities;
+ nt::IdentityManager identities;
std::unique_ptr<nt::LifecycleManager> lifecycles;
std::unique_ptr<nt::NamespaceReferenceManager> references;
std::unique_ptr<nt::HandlerManager> handler;
- std::unique_ptr<nt::CursorManager> cursors;
+ std::unique_ptr<nt::CursorManager> cursors;
};
static std::unique_ptr<Runtime> g_rt;
- static bool g_init_done = false;
- bool is_initialized() { return g_init_done; }
-
- static std::vector<std::string> split_path(const char* path)
- {
+ static std::vector<std::string> split_path(const char* path) {
std::vector<std::string> parts;
- if (!path) return parts;
+ if (!path)
+ return parts;
std::string s(path);
- if (!s.empty() && s[0] == '/') s.erase(0, 1);
+ if (!s.empty() && s[0] == '/')
+ s.erase(0, 1);
std::stringstream ss(s);
std::string part;
while (std::getline(ss, part, '/'))
- if (!part.empty()) parts.push_back(part);
+ if (!part.empty())
+ parts.push_back(part);
return parts;
}
- static std::vector<nt::Attribute> parse_kv(const char* kv)
- {
+ static std::vector<nt::Attribute> parse_kv(const char* kv) {
std::vector<nt::Attribute> attrs;
- if (!kv) return attrs;
+ if (!kv)
+ return attrs;
std::stringstream ss(kv);
std::string line;
- while (std::getline(ss, line))
- {
- if (line.empty()) continue;
+ while (std::getline(ss, line)) {
+ if (line.empty())
+ continue;
const auto pos = line.find('=');
- if (pos == std::string::npos) continue;
- attrs.push_back({ line.substr(0, pos), line.substr(pos + 1) });
+ if (pos == std::string::npos)
+ continue;
+ attrs.push_back({line.substr(0, pos), line.substr(pos + 1)});
}
return attrs;
}
- static std::string tuple_to_kv(nt::Tuple* t)
- {
+ static std::string tuple_to_kv(nt::Tuple* t) {
std::string out;
t->Reset();
const nt::Attribute* a = t->Next();
- while (a)
- {
+ while (a) {
out += a->name + "=" + a->value + "\n";
a = t->Next();
}
return out;
}
- static char* heap_str(const std::string& s)
- {
+ static char* heap_str(const std::string& s) {
char* p = new char[s.size() + 1];
std::memcpy(p, s.data(), s.size());
p[s.size()] = '\0';
return p;
}
- static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = RELATION;
+ static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = RELATION;
t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
t->exclusive = false;
return t;
}
- static std::unique_ptr<nt::ObjectManager::object_type> make_branch_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = BRANCH;
+ static std::unique_ptr<nt::ObjectManager::object_type> make_branch_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = BRANCH;
t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
// Branches are mutable reference heads: serialize writers through
// LifecycleManager::Contention.
t->exclusive = true;
return t;
}
- static std::unique_ptr<nt::ObjectManager::object_type> make_multigroup_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = MULTIGROUP;
+ static std::unique_ptr<nt::ObjectManager::object_type> make_multigroup_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = MULTIGROUP;
t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
// Multigroup snapshots are immutable and content-addressed; concurrent
// readers never contend. GC is gated by reference_count only.
t->exclusive = false;
return t;
}
- static std::unique_ptr<nt::ObjectManager::object_type> make_branch_tree_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = BRANCH_TREE;
+ static std::unique_ptr<nt::ObjectManager::object_type> make_branch_tree_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = BRANCH_TREE;
t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
// Branch-tree blobs are immutable content-addressed merkle roots;
// GC is gated by reference_count only.
t->exclusive = false;
return t;
}
- static std::unique_ptr<nt::ObjectManager::object_type> make_session_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = SESSION;
+ static std::unique_ptr<nt::ObjectManager::object_type> make_session_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = SESSION;
// Sessions live only as long as their connection; rnt_session_close
// removes them via ObjectManager::Unregister.
t->disposable = true;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
// Multiple readers/writers may consult a session concurrently to read
// overrides; contention is only enforced when an exclusive mutator
// (set_branch) is wired up. Default to non-exclusive for now.
@@ -168,18 +158,15 @@ namespace
// drawn from std::random_device-seeded mt19937_64; we do not need
// cryptographic strength here, just enough entropy that two concurrent
// rnt_session_open calls cannot collide.
- static std::string random_session_hash()
- {
- static thread_local std::mt19937_64 rng{ std::random_device{}() };
+ static std::string random_session_hash() {
+ static thread_local std::mt19937_64 rng{std::random_device{}()};
static const char hex[] = "0123456789abcdef";
std::string out;
out.reserve(64);
- for (int i = 0; i < 4; ++i)
- {
+ for (int i = 0; i < 4; ++i) {
uint64_t v = rng();
- for (int b = 0; b < 8; ++b)
- {
+ for (int b = 0; b < 8; ++b) {
uint8_t byte = static_cast<uint8_t>(v & 0xFF);
out.push_back(hex[(byte >> 4) & 0xF]);
out.push_back(hex[byte & 0xF]);
@@ -198,26 +185,28 @@ namespace
// Each child Relation is Pinned by its parent Multigroup at creation time.
// LifecycleManager::CascadeMultigroup releases these pins when the
// multigroup itself becomes GC-eligible.
- static std::string register_snapshot(
- const std::vector<nt::MultigroupCodec::RelationEntry>& relations)
- {
- if (!g_rt) return {};
+ static std::string
+ register_snapshot(const std::vector<nt::MultigroupCodec::RelationEntry>& relations) {
+ if (!g_rt)
+ return {};
const std::string hash = nt::MultigroupCodec::Build(*g_rt->storage, relations);
- if (hash.empty()) return {};
+ if (hash.empty())
+ return {};
const auto snap_path = split_path(("/system/snapshots/" + hash).c_str());
- if (g_rt->objects.Find(snap_path) != nullptr) return hash;
+ if (g_rt->objects.Find(snap_path) != nullptr)
+ return hash;
auto mg = std::make_unique<nt::ObjectManager::Multigroup>();
mg->merkle_root = hash;
g_rt->objects.Register(snap_path, std::move(mg), make_multigroup_type());
- for (const auto& [name, root] : relations)
- {
- const auto rel_path = split_path(
- ("/system/snapshots/" + hash + "/relations/" + name).c_str());
- if (g_rt->objects.Find(rel_path) != nullptr) continue;
+ for (const auto& [name, root] : relations) {
+ const auto rel_path =
+ split_path(("/system/snapshots/" + hash + "/relations/" + name).c_str());
+ if (g_rt->objects.Find(rel_path) != nullptr)
+ continue;
auto rel = std::make_unique<nt::ObjectManager::Relation>();
rel->merkle_root = root;
@@ -232,22 +221,21 @@ namespace
// Enumerates (mg_name, mg_hash) pairs in a branch-tree.
static std::vector<std::pair<std::string, std::string>>
- list_branch_tree(const std::string& branch_tree_root)
- {
+ list_branch_tree(const std::string& branch_tree_root) {
std::vector<std::pair<std::string, std::string>> out;
- if (branch_tree_root.empty()) return out;
+ if (branch_tree_root.empty())
+ return out;
constexpr size_t kPageSize = 1024;
size_t offset = 0;
- while (true)
- {
- auto page = nt::Merkle<std::string>::Page(*g_rt->storage,
- branch_tree_root,
- offset, kPageSize);
- if (page.empty()) break;
+ while (true) {
+ auto page =
+ nt::Merkle<std::string>::Page(*g_rt->storage, branch_tree_root, offset, kPageSize);
+ if (page.empty())
+ break;
for (auto& entry : page)
- out.emplace_back(std::move(entry.key),
- nt::bin_to_hex(entry.payload));
- if (page.size() < kPageSize) break;
+ out.emplace_back(std::move(entry.key), nt::bin_to_hex(entry.payload));
+ if (page.size() < kPageSize)
+ break;
offset += page.size();
}
return out;
@@ -257,20 +245,20 @@ namespace
// takes one structural pin on each multigroup the tree references. The
// pins are mirrored by LifecycleManager::CascadeBranchTree at GC time.
// Idempotent: a second call with the same hash returns immediately.
- static void register_branch_tree(const std::string& tree_root)
- {
- if (tree_root.empty()) return;
- const auto bt_path = split_path(
- ("/system/branch_trees/" + tree_root).c_str());
- if (g_rt->objects.Find(bt_path) != nullptr) return;
+ static void register_branch_tree(const std::string& tree_root) {
+ if (tree_root.empty())
+ return;
+ const auto bt_path = split_path(("/system/branch_trees/" + tree_root).c_str());
+ if (g_rt->objects.Find(bt_path) != nullptr)
+ return;
auto bt = std::make_unique<nt::ObjectManager::BranchTree>();
bt->merkle_root = tree_root;
g_rt->objects.Register(bt_path, std::move(bt), make_branch_tree_type());
- for (const auto& [_, mg_hash] : list_branch_tree(tree_root))
- {
- if (mg_hash.empty()) continue;
+ for (const auto& [_, mg_hash] : list_branch_tree(tree_root)) {
+ if (mg_hash.empty())
+ continue;
const auto p = split_path(("/system/snapshots/" + mg_hash).c_str());
g_rt->lifecycles->Pin(g_rt->objects.Find(p));
}
@@ -281,20 +269,16 @@ namespace
// a no-op advance never sees a pinned BranchTree's ref_count briefly hit
// zero and trigger cascade collection of every mg it references.
static void rebind_branch_tree_pin(const std::string& old_tree_root,
- const std::string& new_tree_root)
- {
- if (old_tree_root == new_tree_root) return;
- if (!new_tree_root.empty())
- {
+ const std::string& new_tree_root) {
+ if (old_tree_root == new_tree_root)
+ return;
+ if (!new_tree_root.empty()) {
register_branch_tree(new_tree_root);
- const auto p = split_path(
- ("/system/branch_trees/" + new_tree_root).c_str());
+ const auto p = split_path(("/system/branch_trees/" + new_tree_root).c_str());
g_rt->lifecycles->Pin(g_rt->objects.Find(p));
}
- if (!old_tree_root.empty())
- {
- const auto p = split_path(
- ("/system/branch_trees/" + old_tree_root).c_str());
+ if (!old_tree_root.empty()) {
+ const auto p = split_path(("/system/branch_trees/" + old_tree_root).c_str());
g_rt->lifecycles->Unpin(g_rt->objects.Find(p));
}
}
@@ -302,36 +286,36 @@ namespace
// Looks up the mg_hash currently bound to `mg_name` in the branch's tree.
// Returns empty string when the branch is unborn or the mg is absent.
static std::string lookup_branch_mg(const nt::ObjectManager::Branch& branch,
- const std::string& mg_name)
- {
- if (branch.target_hash.empty()) return "";
- auto found = nt::Merkle<std::string>::Get(*g_rt->storage,
- branch.target_hash, mg_name);
- if (!found) return "";
+ const std::string& mg_name) {
+ if (branch.target_hash.empty())
+ return "";
+ auto found = nt::Merkle<std::string>::Get(*g_rt->storage, branch.target_hash, mg_name);
+ if (!found)
+ return "";
static const nt::Hash32 zero{};
- if (*found == zero) return "";
+ if (*found == zero)
+ return "";
return nt::bin_to_hex(*found);
}
// Reads the (relation_name, root) list of a specific multigroup under a
// branch. Returns empty for unborn branches or absent multigroups.
- static std::vector<nt::MultigroupCodec::RelationEntry> read_mg_relations(
- const nt::ObjectManager::Branch& branch,
- const std::string& mg_name)
- {
+ static std::vector<nt::MultigroupCodec::RelationEntry>
+ read_mg_relations(const nt::ObjectManager::Branch& branch, const std::string& mg_name) {
const std::string mg_hash = lookup_branch_mg(branch, mg_name);
- if (mg_hash.empty()) return {};
+ if (mg_hash.empty())
+ return {};
return nt::MultigroupCodec::List(*g_rt->storage, mg_hash);
}
// Looks up a branch object by path. Returns nullptr when the path does not
// resolve to a BRANCH.
- static nt::ObjectManager::Branch* find_branch(
- const std::vector<std::string>& branch_path)
- {
+ static nt::ObjectManager::Branch* find_branch(const std::vector<std::string>& branch_path) {
auto* entry = g_rt->objects.Find(branch_path);
- if (!entry || !entry->object || !entry->head) return nullptr;
- if (entry->head->type->label != BRANCH) return nullptr;
+ if (!entry || !entry->object || !entry->head)
+ return nullptr;
+ if (entry->head->type->label != BRANCH)
+ return nullptr;
return dynamic_cast<nt::ObjectManager::Branch*>(entry->object.get());
}
@@ -341,15 +325,15 @@ namespace
// path, the multigroup name, and the relation name. Returns false for
// any other path shape.
static bool split_branch_mg_relation(const std::vector<std::string>& parts,
- std::vector<std::string>& branch_path_out,
- std::string& mg_name_out,
- std::string& relation_name_out)
- {
- if (parts.size() != 7) return false;
- if (parts[0] != "system" || parts[1] != "branches"
- || parts[3] != "multigroups" || parts[5] != "relations") return false;
- branch_path_out = { "system", "branches", parts[2] };
- mg_name_out = parts[4];
+ std::vector<std::string>& branch_path_out,
+ std::string& mg_name_out, std::string& relation_name_out) {
+ if (parts.size() != 7)
+ return false;
+ if (parts[0] != "system" || parts[1] != "branches" || parts[3] != "multigroups" ||
+ parts[5] != "relations")
+ return false;
+ branch_path_out = {"system", "branches", parts[2]};
+ mg_name_out = parts[4];
relation_name_out = parts[6];
return true;
}
@@ -367,20 +351,21 @@ namespace
// Pin/unpin transitions are applied via rebind_branch_tree_pin: new mgs
// are pinned before old mgs are unpinned, so an unchanged advance never
// briefly drops a multigroup's ref_count to zero.
- static std::string commit_relation_update(
- nt::ObjectManager::Branch& branch,
- const std::string& mg_name,
- const std::string& relation_name,
- const std::string& new_root)
- {
+ static std::string commit_relation_update(nt::ObjectManager::Branch& branch,
+ const std::string& mg_name,
+ const std::string& relation_name,
+ const std::string& new_root) {
auto relations = read_mg_relations(branch, mg_name);
auto it = std::find_if(relations.begin(), relations.end(),
[&](const auto& e) { return e.first == relation_name; });
- if (it != relations.end()) it->second = new_root;
- else relations.emplace_back(relation_name, new_root);
+ if (it != relations.end())
+ it->second = new_root;
+ else
+ relations.emplace_back(relation_name, new_root);
const std::string new_mg_hash = register_snapshot(relations);
- if (new_mg_hash.empty()) return {};
+ if (new_mg_hash.empty())
+ return {};
const nt::Hash32 mg_payload = nt::hex_to_bin(new_mg_hash);
const std::string new_tree_root = nt::Merkle<std::string>::Insert(
@@ -397,8 +382,7 @@ namespace
// doesn't exist, or the relation is absent.
static std::string read_relation_root(const nt::ObjectManager::Branch& branch,
const std::string& mg_name,
- const std::string& relation_name)
- {
+ const std::string& relation_name) {
const auto relations = read_mg_relations(branch, mg_name);
auto it = std::find_if(relations.begin(), relations.end(),
[&](const auto& e) { return e.first == relation_name; });
@@ -415,19 +399,20 @@ namespace
* are collected here so they can be released atomically when the plan is
* freed or transferred to a VmCursor.
*/
- struct PlanWrapper
- {
- nt::PlanNode* root = nullptr;
- std::vector<std::unique_ptr<nt::PlanNode>> nodes; // owns every node in subtree
- std::vector<nt::CursorManager::cursor*> cursors; // one per SCAN leaf
- std::vector<nt::HandlerManager::handle*> handles; // one per SCAN leaf
+ struct PlanWrapper {
+ nt::PlanNode* root = nullptr;
+ std::vector<std::unique_ptr<nt::PlanNode>> nodes; // owns every node in subtree
+ std::vector<nt::CursorManager::cursor*> cursors; // one per SCAN leaf
+ std::vector<nt::HandlerManager::handle*> handles; // one per SCAN leaf
};
- static void free_plan_wrapper(PlanWrapper* pw)
- {
- if (!pw) return;
- for (auto* c : pw->cursors) g_rt->cursors->Close(c);
- for (auto* h : pw->handles) g_rt->handler->Close(h);
+ static void free_plan_wrapper(PlanWrapper* pw) {
+ if (!pw)
+ return;
+ for (auto* c : pw->cursors)
+ g_rt->cursors->Close(c);
+ for (auto* h : pw->handles)
+ g_rt->handler->Close(h);
delete pw;
}
@@ -436,30 +421,163 @@ namespace
* during plan construction. After rnt_vm_execute_plan the caller drives
* it with rnt_vm_cursor_next / rnt_vm_cursor_close.
*/
- struct VmCursor
- {
- nt::VM vm;
- nt::PlanNode* root;
- std::vector<std::unique_ptr<nt::PlanNode>> nodes;
- std::vector<nt::CursorManager::cursor*> cursors;
- std::vector<nt::HandlerManager::handle*> handles;
-
- explicit VmCursor(nt::CursorManager& cm, PlanWrapper* pw)
- : vm(cm), root(pw->root)
- {
- nodes = std::move(pw->nodes);
+ struct VmCursor {
+ nt::VM vm;
+ nt::PlanNode* root;
+ std::vector<std::unique_ptr<nt::PlanNode>> nodes;
+ std::vector<nt::CursorManager::cursor*> cursors;
+ std::vector<nt::HandlerManager::handle*> handles;
+
+ explicit VmCursor(nt::CursorManager& cm, PlanWrapper* pw) : vm(cm), root(pw->root) {
+ nodes = std::move(pw->nodes);
cursors = std::move(pw->cursors);
handles = std::move(pw->handles);
- delete pw; // struct itself; resources transferred above
+ delete pw; // struct itself; resources transferred above
}
- ~VmCursor()
- {
- for (auto* c : cursors) g_rt->cursors->Close(c);
- for (auto* h : handles) g_rt->handler->Close(h);
+ ~VmCursor() {
+ for (auto* c : cursors)
+ g_rt->cursors->Close(c);
+ for (auto* h : handles)
+ g_rt->handler->Close(h);
}
};
-}
+
+ // ---- Plan builders --------------------------------------------------
+ // Pure construction: each builder receives only its operator's context and
+ // assumes the runtime is already initialised. rnt_plan_assemble is the sole
+ // caller and performs that check once before dispatching here.
+
+ PlanWrapper* build_scan(const PlanArgsScan& a) {
+ if (!a.relation_path)
+ return nullptr;
+
+ // Open a handle to the stored relation through the full manager pipeline.
+ // Open() runs NamespaceReferenceManager::Resolve internally, so the handle
+ // lands on the resolved /system/snapshots/<hash>/relations/<n> entry when
+ // the caller passed a branch-relative path.
+ const auto parts = split_path(a.relation_path);
+ auto* h = g_rt->handler->Open(parts, nullptr);
+ if (!h)
+ return nullptr;
+
+ // Read the Merkle root straight from the handle's object — Resolve has
+ // already pointed it at the snapshot-bound Relation entry.
+ std::string merkle_root;
+ if (h->object && h->object->object) {
+ auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(h->object->object.get());
+ if (rel)
+ merkle_root = rel->merkle_root;
+ }
+
+ auto* c = g_rt->cursors->Open(h, merkle_root);
+ if (!c) {
+ g_rt->handler->Close(h);
+ return nullptr;
+ }
+
+ auto node = std::make_unique<nt::PlanNode>();
+ node->op = nt::FOL_OPERATION_SCAN;
+ node->scan_cursor = c;
+
+ auto* pw = new PlanWrapper();
+ pw->root = node.get();
+ pw->nodes.push_back(std::move(node));
+ pw->cursors.push_back(c);
+ pw->handles.push_back(h);
+ return pw;
+ }
+
+ PlanWrapper* build_join(const PlanArgsJoin& a) {
+ auto* l = static_cast<PlanWrapper*>(a.left);
+ auto* r = static_cast<PlanWrapper*>(a.right);
+
+ if (!l || !r) {
+ free_plan_wrapper(l);
+ free_plan_wrapper(r);
+ return nullptr;
+ }
+
+ auto node = std::make_unique<nt::PlanNode>();
+ node->op = nt::FOL_OPERATION_JOIN;
+ node->left = l->root;
+ node->right = r->root;
+
+ auto* pw = new PlanWrapper();
+ pw->root = node.get();
+ pw->nodes.push_back(std::move(node));
+
+ // Absorb child resources into the new wrapper.
+ for (auto& n : l->nodes)
+ pw->nodes.push_back(std::move(n));
+ for (auto& n : r->nodes)
+ pw->nodes.push_back(std::move(n));
+ for (auto* c : l->cursors)
+ pw->cursors.push_back(c);
+ for (auto* c : r->cursors)
+ pw->cursors.push_back(c);
+ for (auto* h : l->handles)
+ pw->handles.push_back(h);
+ for (auto* h : r->handles)
+ pw->handles.push_back(h);
+
+ delete l;
+ delete r;
+ return pw;
+ }
+
+ PlanWrapper* build_take(const PlanArgsTake& a) {
+ auto* s = static_cast<PlanWrapper*>(a.source);
+ if (!s)
+ return nullptr;
+
+ auto node = std::make_unique<nt::PlanNode>();
+ node->op = nt::FOL_OPERATION_TAKE;
+ node->left = s->root;
+ node->take_limit = a.limit;
+
+ auto* pw = new PlanWrapper();
+ pw->root = node.get();
+ pw->nodes.push_back(std::move(node));
+
+ for (auto& n : s->nodes)
+ pw->nodes.push_back(std::move(n));
+ for (auto* c : s->cursors)
+ pw->cursors.push_back(c);
+ for (auto* h : s->handles)
+ pw->handles.push_back(h);
+
+ delete s;
+ return pw;
+ }
+
+ PlanWrapper* build_project(const PlanArgsProject& a) {
+ auto* s = static_cast<PlanWrapper*>(a.source);
+ if (!s)
+ return nullptr;
+
+ auto node = std::make_unique<nt::PlanNode>();
+ node->op = nt::FOL_OPERATION_PROJECT;
+ node->left = s->root;
+
+ if (a.attrs)
+ for (const char** p = a.attrs; *p; ++p)
+ node->project_attrs.emplace(*p);
+
+ auto* pw = new PlanWrapper();
+ pw->root = node.get();
+ pw->nodes.push_back(std::move(node));
+ for (auto& n : s->nodes)
+ pw->nodes.push_back(std::move(n));
+ for (auto* c : s->cursors)
+ pw->cursors.push_back(c);
+ for (auto* h : s->handles)
+ pw->handles.push_back(h);
+
+ delete s;
+ return pw;
+ }
+} // namespace
// ---------------------------------------------------------------------------
// Public C API
@@ -469,71 +587,59 @@ namespace
// Object registration → Tuple storage → Cursor / VM plan builder
// ---------------------------------------------------------------------------
-int rnt_init(const char* driver, const char* storage_path)
-{
- try
- {
+int rnt_init(const char* driver, const char* storage_path) {
+ try {
auto rt = std::make_unique<Runtime>();
const std::string drv = driver ? driver : "sqlite";
if (drv == "memory")
rt->storage = std::make_unique<nt::InMemoryBackend>();
else
- rt->storage = std::make_unique<nt::SqliteBackend>(
- storage_path ? storage_path : ":memory:");
+ rt->storage =
+ std::make_unique<nt::SqliteBackend>(storage_path ? storage_path : ":memory:");
- rt->lifecycles = std::make_unique<nt::LifecycleManager>(
- rt->objects, *rt->storage);
- rt->references = std::make_unique<nt::NamespaceReferenceManager>(
- rt->objects, *rt->storage);
+ rt->lifecycles = std::make_unique<nt::LifecycleManager>(rt->objects, *rt->storage);
+ rt->references = std::make_unique<nt::NamespaceReferenceManager>(rt->objects, *rt->storage);
rt->handler = std::make_unique<nt::HandlerManager>(
- rt->objects, rt->permissions, rt->identities, *rt->lifecycles,
- *rt->references);
- rt->cursors = std::make_unique<nt::CursorManager>(
- *rt->storage, rt->lifecycles.get(), &rt->objects);
+ rt->objects, rt->permissions, rt->identities, *rt->lifecycles, *rt->references);
+ rt->cursors =
+ std::make_unique<nt::CursorManager>(*rt->storage, rt->lifecycles.get(), &rt->objects);
g_rt = std::move(rt);
// Register the default branch on first boot so that
// rnt_open_handle("/system/branches/master") always succeeds.
const auto master_path = split_path("/system/branches/master");
- if (g_rt->objects.Find(master_path) == nullptr)
- {
- auto branch = std::make_unique<nt::ObjectManager::Branch>();
+ if (g_rt->objects.Find(master_path) == nullptr) {
+ auto branch = std::make_unique<nt::ObjectManager::Branch>();
branch->name = "master";
- g_rt->objects.Register(master_path, std::move(branch),
- make_branch_type());
+ g_rt->objects.Register(master_path, std::move(branch), make_branch_type());
}
- g_init_done = true;
return 0;
- }
- catch (...)
- {
+ } catch (...) {
g_rt.reset();
return -1;
}
}
-int rnt_firewall(const char* /*auth_method*/, char** claims_out)
-{
- if (!is_initialized() || !claims_out) return -1;
+int rnt_firewall(const char* /*auth_method*/, char** claims_out) {
// PermissionsManager::Firewall is stubbed; grant all claims for now.
*claims_out = heap_str("READ WRITE");
return 0;
}
-int rnt_session_open(void* connection_context, char** session_hash_out)
-{
- if (!is_initialized() || !session_hash_out) return -1;
+int rnt_session_open(void* connection_context, char** session_hash_out) {
+ if (!session_hash_out)
+ return -1;
// Retry on the astronomically unlikely event of a hash collision against
// an already-registered session.
std::string hash;
- for (int attempts = 0; attempts < 8; ++attempts)
- {
+ for (int attempts = 0; attempts < 8; ++attempts) {
hash = random_session_hash();
const auto session_path = split_path(("/system/sessions/" + hash).c_str());
- if (g_rt->objects.Find(session_path) != nullptr) continue;
+ if (g_rt->objects.Find(session_path) != nullptr)
+ continue;
auto session = std::make_unique<nt::ObjectManager::Session>();
session->connection_context = connection_context;
@@ -544,16 +650,17 @@ int rnt_session_open(void* connection_context, char** session_hash_out)
return -1;
}
-int rnt_session_close(const char* session_hash)
-{
- if (!is_initialized() || !session_hash) return -1;
- const auto session_path = split_path(
- ("/system/sessions/" + std::string(session_hash)).c_str());
+int rnt_session_close(const char* session_hash) {
+ if (!session_hash)
+ return -1;
+ const auto session_path = split_path(("/system/sessions/" + std::string(session_hash)).c_str());
auto* entry = g_rt->objects.Find(session_path);
- if (!entry || !entry->object) return -1;
+ if (!entry || !entry->object)
+ return -1;
auto* session = dynamic_cast<nt::ObjectManager::Session*>(entry->object.get());
- if (!session) return -1;
+ if (!session)
+ return -1;
// Release every pin this session was holding before the entry disappears,
// so the dependent multigroups become GC-eligible.
@@ -564,29 +671,29 @@ int rnt_session_close(const char* session_hash)
return g_rt->objects.Unregister(session_path) ? 0 : -1;
}
-int rnt_session_set_branch(const char* session_hash,
- const char* branch_name,
- const char* target_hash)
-{
- if (!is_initialized() || !session_hash || !branch_name || !target_hash)
+int rnt_session_set_branch(const char* session_hash, const char* branch_name,
+ const char* target_hash) {
+ if (!session_hash || !branch_name || !target_hash)
return -1;
- const auto session_path = split_path(
- ("/system/sessions/" + std::string(session_hash)).c_str());
+ const auto session_path = split_path(("/system/sessions/" + std::string(session_hash)).c_str());
auto* entry = g_rt->objects.Find(session_path);
- if (!entry || !entry->object || !entry->head) return -1;
- if (entry->head->type->label != SESSION) return -1;
+ if (!entry || !entry->object || !entry->head)
+ return -1;
+ if (entry->head->type->label != SESSION)
+ return -1;
auto* session = dynamic_cast<nt::ObjectManager::Session*>(entry->object.get());
- if (!session) return -1;
+ if (!session)
+ return -1;
const std::string hash(target_hash);
std::string old_hash;
auto it = session->branch_overrides.find(branch_name);
- if (it != session->branch_overrides.end()) old_hash = it->second;
+ if (it != session->branch_overrides.end())
+ old_hash = it->second;
- if (hash.empty())
- {
+ if (hash.empty()) {
session->branch_overrides.erase(branch_name);
rebind_branch_tree_pin(old_hash, "");
return 0;
@@ -594,58 +701,66 @@ int rnt_session_set_branch(const char* session_hash,
// target_hash is a branch-tree root — a content-addressed blob produced
// by an earlier commit. Reject if unknown.
- if (!g_rt->storage->Get(hash)) return -1;
+ if (!g_rt->storage->Get(hash))
+ return -1;
session->branch_overrides[branch_name] = hash;
rebind_branch_tree_pin(old_hash, hash);
return 0;
}
-rnt_handle_t rnt_open_handle(const char* path, const char* /*claims*/)
-{
- if (!is_initialized() || !path) return nullptr;
+rnt_handle_t rnt_open_handle(const char* path, const char* /*claims*/) {
+ if (!path)
+ return nullptr;
return g_rt->handler->Open(split_path(path), nullptr);
}
-int rnt_close_handle(rnt_handle_t handle)
-{
- if (!is_initialized() || !handle) return -1;
+int rnt_close_handle(rnt_handle_t handle) {
+ if (!handle)
+ return -1;
auto* h = static_cast<nt::HandlerManager::handle*>(handle);
return g_rt->handler->Close(h) ? 0 : -1;
}
-int rnt_branch_target(rnt_handle_t handle, char** target_hash_out)
-{
- if (!is_initialized() || !handle || !target_hash_out) return -1;
+int rnt_branch_target(rnt_handle_t handle, char** target_hash_out) {
+ if (!handle || !target_hash_out)
+ return -1;
auto* h = static_cast<nt::HandlerManager::handle*>(handle);
- if (!h->object || !h->object->head) return -1;
- if (h->object->head->type->label != BRANCH) return -1;
+ if (!h->object || !h->object->head)
+ return -1;
+ if (h->object->head->type->label != BRANCH)
+ return -1;
auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(h->object->object.get());
- if (!branch) return -1;
+ if (!branch)
+ return -1;
*target_hash_out = heap_str(branch->target_hash);
return 0;
}
-int rnt_branch_advance(const char* branch_path, const char* new_hash)
-{
- if (!is_initialized() || !branch_path || !new_hash) return -1;
+int rnt_branch_advance(const char* branch_path, const char* new_hash) {
+ if (!branch_path || !new_hash)
+ return -1;
auto* entry = g_rt->objects.Find(split_path(branch_path));
- if (!entry || !entry->object || !entry->head) return -1;
- if (entry->head->type->label != BRANCH) return -1;
+ if (!entry || !entry->object || !entry->head)
+ return -1;
+ if (entry->head->type->label != BRANCH)
+ return -1;
auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(entry->object.get());
- if (!branch) return -1;
+ if (!branch)
+ return -1;
const std::string hash(new_hash);
// Non-empty branch-tree roots must already exist in the KV store. The
// branch tree is a content-addressed blob produced by an earlier mutation
// through commit_relation_update; advancing to an unknown root is treated
// as a caller bug.
- if (!hash.empty() && !g_rt->storage->Get(hash)) return -1;
+ if (!hash.empty() && !g_rt->storage->Get(hash))
+ return -1;
const std::string old_hash = branch->target_hash;
branch->target_hash = hash;
@@ -653,9 +768,9 @@ int rnt_branch_advance(const char* branch_path, const char* new_hash)
return 0;
}
-int rnt_register_relation(const char* path)
-{
- if (!is_initialized() || !path) return -1;
+int rnt_register_relation(const char* path) {
+ if (!path)
+ return -1;
const auto parts = split_path(path);
std::vector<std::string> branch_path;
@@ -664,34 +779,36 @@ int rnt_register_relation(const char* path)
return -1;
auto* branch = find_branch(branch_path);
- if (!branch) return -1;
+ if (!branch)
+ return -1;
// Idempotent: if the relation is already present in the named mg's
// current snapshot, the call succeeds without producing a new snapshot.
const auto current = read_mg_relations(*branch, mg_name);
const bool already_present = std::any_of(
- current.begin(), current.end(),
- [&](const auto& e) { return e.first == relation_name; });
- if (already_present) return 0;
+ current.begin(), current.end(), [&](const auto& e) { return e.first == relation_name; });
+ if (already_present)
+ return 0;
// Empty merkle_root signals a relation with no tuples.
- return commit_relation_update(*branch, mg_name, relation_name, "").empty()
- ? -1 : 0;
+ return commit_relation_update(*branch, mg_name, relation_name, "").empty() ? -1 : 0;
}
-int rnt_register_branch(const char* path, const char* target_hash)
-{
- if (!is_initialized() || !path) return -1;
+int rnt_register_branch(const char* path, const char* target_hash) {
+ if (!path)
+ return -1;
const auto parts = split_path(path);
- if (g_rt->objects.Find(parts) != nullptr) return 0;
+ if (g_rt->objects.Find(parts) != nullptr)
+ return 0;
const std::string hash = target_hash ? target_hash : "";
// Non-empty target_hash is a branch-tree root produced by an earlier
// commit; it must already exist as a KV blob.
- if (!hash.empty() && !g_rt->storage->Get(hash)) return -1;
+ if (!hash.empty() && !g_rt->storage->Get(hash))
+ return -1;
- auto branch = std::make_unique<nt::ObjectManager::Branch>();
- branch->name = parts.empty() ? "" : parts.back();
+ auto branch = std::make_unique<nt::ObjectManager::Branch>();
+ branch->name = parts.empty() ? "" : parts.back();
branch->target_hash = hash;
g_rt->objects.Register(parts, std::move(branch), make_branch_type());
@@ -699,16 +816,18 @@ int rnt_register_branch(const char* path, const char* target_hash)
return 0;
}
-int rnt_list_relations(const char* branch_mg_path, char** out)
-{
- if (!is_initialized() || !branch_mg_path || !out) return -1;
+int rnt_list_relations(const char* branch_mg_path, char** out) {
+ if (!branch_mg_path || !out)
+ return -1;
const auto parts = split_path(branch_mg_path);
- if (parts.size() != 5) return -1;
- if (parts[0] != "system" || parts[1] != "branches"
- || parts[3] != "multigroups") return -1;
+ if (parts.size() != 5)
+ return -1;
+ if (parts[0] != "system" || parts[1] != "branches" || parts[3] != "multigroups")
+ return -1;
- auto* branch = find_branch({ "system", "branches", parts[2] });
- if (!branch) return -1;
+ auto* branch = find_branch({"system", "branches", parts[2]});
+ if (!branch)
+ return -1;
const auto relations = read_mg_relations(*branch, parts[4]);
std::string result;
for (const auto& [name, root] : relations)
@@ -717,11 +836,12 @@ int rnt_list_relations(const char* branch_mg_path, char** out)
return 0;
}
-int rnt_list_branch_multigroups(const char* branch_path, char** out)
-{
- if (!is_initialized() || !branch_path || !out) return -1;
+int rnt_list_branch_multigroups(const char* branch_path, char** out) {
+ if (!branch_path || !out)
+ return -1;
auto* branch = find_branch(split_path(branch_path));
- if (!branch) return -1;
+ if (!branch)
+ return -1;
const auto mgs = list_branch_tree(branch->target_hash);
std::string result;
for (const auto& [name, mg_hash] : mgs)
@@ -730,15 +850,17 @@ int rnt_list_branch_multigroups(const char* branch_path, char** out)
return 0;
}
-int rnt_list_snapshot_relations(const char* snapshot_hash, char** out)
-{
- if (!is_initialized() || !snapshot_hash || !out) return -1;
+int rnt_list_snapshot_relations(const char* snapshot_hash, char** out) {
+ if (!snapshot_hash || !out)
+ return -1;
const std::string hash(snapshot_hash);
const auto snap_path = split_path(("/system/snapshots/" + hash).c_str());
auto* entry = g_rt->objects.Find(snap_path);
- if (!entry || !entry->object) return -1;
+ if (!entry || !entry->object)
+ return -1;
auto* mg = dynamic_cast<nt::ObjectManager::Multigroup*>(entry->object.get());
- if (!mg) return -1;
+ if (!mg)
+ return -1;
const auto relations = nt::MultigroupCodec::List(*g_rt->storage, mg->merkle_root);
std::string result;
for (const auto& [name, root] : relations)
@@ -747,9 +869,9 @@ int rnt_list_snapshot_relations(const char* snapshot_hash, char** out)
return 0;
}
-int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_out)
-{
- if (!is_initialized() || !relation_path || !kv_attrs || !hash_out) return -1;
+int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_out) {
+ if (!relation_path || !kv_attrs || !hash_out)
+ return -1;
const auto parts = split_path(relation_path);
std::vector<std::string> branch_path;
@@ -758,16 +880,16 @@ int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_
return -1;
auto* branch = find_branch(branch_path);
- if (!branch) return -1;
+ if (!branch)
+ return -1;
const std::string old_root = read_relation_root(*branch, mg_name, relation_name);
- auto bytes = nt::TupleCodec::Serialize(parse_kv(kv_attrs));
+ auto bytes = nt::TupleCodec::Serialize(parse_kv(kv_attrs));
const auto tuple_hash = g_rt->storage->Put(std::move(bytes));
- const auto tuple_bin = nt::hex_to_bin(tuple_hash);
+ const auto tuple_bin = nt::hex_to_bin(tuple_hash);
const std::string new_root =
- nt::Merkle<nt::Hash32>::Insert(*g_rt->storage, old_root,
- tuple_bin, tuple_bin);
+ nt::Merkle<nt::Hash32>::Insert(*g_rt->storage, old_root, tuple_bin, tuple_bin);
if (commit_relation_update(*branch, mg_name, relation_name, new_root).empty())
return -1;
@@ -776,9 +898,9 @@ int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_
return 0;
}
-int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash)
-{
- if (!is_initialized() || !relation_path || !tuple_hash) return -1;
+int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash) {
+ if (!relation_path || !tuple_hash)
+ return -1;
const auto parts = split_path(relation_path);
std::vector<std::string> branch_path;
@@ -787,20 +909,19 @@ int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash)
return -1;
auto* branch = find_branch(branch_path);
- if (!branch) return -1;
+ if (!branch)
+ return -1;
const std::string old_root = read_relation_root(*branch, mg_name, relation_name);
const std::string new_root =
- nt::Merkle<nt::Hash32>::Remove(*g_rt->storage, old_root,
- nt::hex_to_bin(tuple_hash));
+ nt::Merkle<nt::Hash32>::Remove(*g_rt->storage, old_root, nt::hex_to_bin(tuple_hash));
- return commit_relation_update(*branch, mg_name, relation_name, new_root).empty()
- ? -1 : 0;
+ return commit_relation_update(*branch, mg_name, relation_name, new_root).empty() ? -1 : 0;
}
-int rnt_clear_relation(const char* relation_path)
-{
- if (!is_initialized() || !relation_path) return -1;
+int rnt_clear_relation(const char* relation_path) {
+ if (!relation_path)
+ return -1;
const auto parts = split_path(relation_path);
std::vector<std::string> branch_path;
@@ -809,15 +930,15 @@ int rnt_clear_relation(const char* relation_path)
return -1;
auto* branch = find_branch(branch_path);
- if (!branch) return -1;
+ if (!branch)
+ return -1;
- return commit_relation_update(*branch, mg_name, relation_name, "").empty()
- ? -1 : 0;
+ return commit_relation_update(*branch, mg_name, relation_name, "").empty() ? -1 : 0;
}
-int rnt_relation_root(const char* relation_path, char** root_hash_out)
-{
- if (!is_initialized() || !relation_path || !root_hash_out) return -1;
+int rnt_relation_root(const char* relation_path, char** root_hash_out) {
+ if (!relation_path || !root_hash_out)
+ return -1;
const auto parts = split_path(relation_path);
std::vector<std::string> branch_path;
@@ -826,7 +947,8 @@ int rnt_relation_root(const char* relation_path, char** root_hash_out)
return -1;
auto* branch = find_branch(branch_path);
- if (!branch) return -1;
+ if (!branch)
+ return -1;
// Distinguish "relation registered but empty" (returns 0 with empty hash)
// from "relation not in this multigroup's snapshot" (returns -1). Without
@@ -834,174 +956,101 @@ int rnt_relation_root(const char* relation_path, char** root_hash_out)
const auto relations = read_mg_relations(*branch, mg_name);
auto it = std::find_if(relations.begin(), relations.end(),
[&](const auto& e) { return e.first == relation_name; });
- if (it == relations.end()) return -1;
+ if (it == relations.end())
+ return -1;
*root_hash_out = heap_str(it->second);
return 0;
}
-rnt_cursor_t rnt_cursor_open(rnt_handle_t handle)
-{
- if (!is_initialized() || !handle) return nullptr;
+rnt_cursor_t rnt_cursor_open(rnt_handle_t handle) {
+ if (!handle)
+ return nullptr;
auto* h = static_cast<nt::HandlerManager::handle*>(handle);
std::string merkle_root;
- if (h->object && h->object->head &&
- h->object->head->type->label == RELATION)
- {
+ if (h->object && h->object->head && h->object->head->type->label == RELATION) {
auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(h->object->object.get());
- if (rel) merkle_root = rel->merkle_root;
+ if (rel)
+ merkle_root = rel->merkle_root;
}
return g_rt->cursors->Open(h, merkle_root);
}
-int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out)
-{
- if (!is_initialized() || !cursor || !tuple_out) return -1;
+int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out) {
+ if (!cursor || !tuple_out)
+ return -1;
auto* c = static_cast<nt::CursorManager::cursor*>(cursor);
nt::Tuple* t = g_rt->cursors->Next(c);
- if (!t) { *tuple_out = nullptr; return 0; }
+ if (!t) {
+ *tuple_out = nullptr;
+ return 0;
+ }
*tuple_out = heap_str(tuple_to_kv(t));
return 1;
}
-int rnt_cursor_close(rnt_cursor_t cursor)
-{
- if (!is_initialized() || !cursor) return -1;
+int rnt_cursor_close(rnt_cursor_t cursor) {
+ if (!cursor)
+ return -1;
g_rt->cursors->Close(static_cast<nt::CursorManager::cursor*>(cursor));
return 0;
}
-void rnt_free_string(char* s) { delete[] s; }
-void rnt_free_bytes(uint8_t* p) { delete[] p; }
+void rnt_free_string(char* s) {
+ delete[] s;
+}
+void rnt_free_bytes(uint8_t* p) {
+ delete[] p;
+}
// ---------------------------------------------------------------------------
// VM plan builder
// ---------------------------------------------------------------------------
-rnt_plan_t rnt_plan_scan(const char* relation_path)
-{
- if (!is_initialized() || !relation_path) return nullptr;
-
- // Open a handle to the stored relation through the full manager pipeline.
- // Open() runs NamespaceReferenceManager::Resolve internally, so the handle
- // lands on the resolved /system/snapshots/<hash>/relations/<n> entry when
- // the caller passed a branch-relative path.
- const auto parts = split_path(relation_path);
- auto* h = g_rt->handler->Open(parts, nullptr);
- if (!h) return nullptr;
-
- // Read the Merkle root straight from the handle's object — Resolve has
- // already pointed it at the snapshot-bound Relation entry.
- std::string merkle_root;
- if (h->object && h->object->object)
- {
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(h->object->object.get());
- if (rel) merkle_root = rel->merkle_root;
+rnt_plan_t rnt_plan_assemble(PlanAction action) {
+ switch (action.operation) {
+ case nt::FOL_OPERATION_SCAN:
+ return build_scan(action.scan);
+ case nt::FOL_OPERATION_JOIN:
+ return build_join(action.join);
+ case nt::FOL_OPERATION_TAKE:
+ return build_take(action.take);
+ case nt::FOL_OPERATION_PROJECT:
+ return build_project(action.project);
}
- auto* c = g_rt->cursors->Open(h, merkle_root);
- if (!c)
- {
- g_rt->handler->Close(h);
- return nullptr;
- }
-
- auto node = std::make_unique<nt::PlanNode>();
- node->op = nt::PlanNode::Op::SCAN;
- node->scan_cursor = c;
-
- auto* pw = new PlanWrapper();
- pw->root = node.get();
- pw->nodes.push_back(std::move(node));
- pw->cursors.push_back(c);
- pw->handles.push_back(h);
- return pw;
-}
-
-rnt_plan_t rnt_plan_join(rnt_plan_t left, rnt_plan_t right)
-{
- if (!left || !right)
- {
- free_plan_wrapper(static_cast<PlanWrapper*>(left));
- free_plan_wrapper(static_cast<PlanWrapper*>(right));
- return nullptr;
- }
-
- auto* l = static_cast<PlanWrapper*>(left);
- auto* r = static_cast<PlanWrapper*>(right);
-
- auto node = std::make_unique<nt::PlanNode>();
- node->op = nt::PlanNode::Op::JOIN;
- node->left = l->root;
- node->right = r->root;
-
- auto* pw = new PlanWrapper();
- pw->root = node.get();
- pw->nodes.push_back(std::move(node));
-
- // Absorb child resources into the new wrapper.
- for (auto& n : l->nodes) pw->nodes.push_back(std::move(n));
- for (auto& n : r->nodes) pw->nodes.push_back(std::move(n));
- for (auto* c : l->cursors) pw->cursors.push_back(c);
- for (auto* c : r->cursors) pw->cursors.push_back(c);
- for (auto* h : l->handles) pw->handles.push_back(h);
- for (auto* h : r->handles) pw->handles.push_back(h);
-
- delete l;
- delete r;
- return pw;
+ return nullptr;
}
-rnt_plan_t rnt_plan_take(rnt_plan_t source, size_t limit)
-{
- if (!source) return nullptr;
-
- auto* s = static_cast<PlanWrapper*>(source);
-
- auto node = std::make_unique<nt::PlanNode>();
- node->op = nt::PlanNode::Op::TAKE;
- node->left = s->root;
- node->take_limit = limit;
-
- auto* pw = new PlanWrapper();
- pw->root = node.get();
- pw->nodes.push_back(std::move(node));
-
- for (auto& n : s->nodes) pw->nodes.push_back(std::move(n));
- for (auto* c : s->cursors) pw->cursors.push_back(c);
- for (auto* h : s->handles) pw->handles.push_back(h);
-
- delete s;
- return pw;
-}
-
-void rnt_plan_free(rnt_plan_t plan)
-{
+void rnt_plan_free(rnt_plan_t plan) {
free_plan_wrapper(static_cast<PlanWrapper*>(plan));
}
-rnt_cursor_t rnt_vm_execute_plan(rnt_plan_t plan)
-{
- if (!is_initialized() || !plan) return nullptr;
+rnt_cursor_t rnt_vm_execute_plan(rnt_plan_t plan) {
+ if (!plan)
+ return nullptr;
// VmCursor constructor transfers ownership from PlanWrapper and deletes it.
return new VmCursor(*g_rt->cursors, static_cast<PlanWrapper*>(plan));
}
-int rnt_vm_cursor_next(rnt_cursor_t vm_cursor, char** tuple_out)
-{
- if (!is_initialized() || !vm_cursor || !tuple_out) return -1;
+int rnt_vm_cursor_next(rnt_cursor_t vm_cursor, char** tuple_out) {
+ if (!vm_cursor || !tuple_out)
+ return -1;
auto* vc = static_cast<VmCursor*>(vm_cursor);
nt::Tuple* t = vc->vm.Next(vc->root);
- if (!t) { *tuple_out = nullptr; return 0; }
+ if (!t) {
+ *tuple_out = nullptr;
+ return 0;
+ }
*tuple_out = heap_str(tuple_to_kv(t));
return 1;
}
-int rnt_vm_cursor_close(rnt_cursor_t vm_cursor)
-{
- if (!is_initialized() || !vm_cursor) return -1;
+int rnt_vm_cursor_close(rnt_cursor_t vm_cursor) {
+ if (!vm_cursor)
+ return -1;
// VmCursor destructor closes all cursors and handles.
delete static_cast<VmCursor*>(vm_cursor);
return 0;
diff --git a/src/RelationalNT.cpp b/src/RelationalNT.cpp
index b1302ee..f27179f 100644
--- a/src/RelationalNT.cpp
+++ b/src/RelationalNT.cpp
@@ -16,50 +16,46 @@
#include <iostream>
#endif
-namespace nt
-{
- bool NT::IsRunning() const
- {
+namespace nt {
+ bool NT::IsRunning() const {
return true;
}
- void NT::SimulateEntryCall()
- {
+ void NT::SimulateEntryCall() {
}
-}
+} // namespace nt
#ifdef NT_CONSOLE_APP
-int main()
-{
+int main() {
const nt::NT runtime;
std::cout << "RNT running: " << (runtime.IsRunning() ? "yes" : "no") << '\n';
// --- Mock: register villager in the object registry ---
nt::ObjectManager objects;
- std::unique_ptr<nt::ObjectManager::object_type> type = std::make_unique<nt::ObjectManager::object_type>();
+ std::unique_ptr<nt::ObjectManager::object_type> type =
+ std::make_unique<nt::ObjectManager::object_type>();
type->label = RELATION;
type->disposable = false;
- type->methods = { OPEN, CLOSE };
+ type->methods = {OPEN, CLOSE};
- const std::vector<std::string> path = { "multigroups", "sakura", "relations", "villager" };
+ const std::vector<std::string> path = {"multigroups", "sakura", "relations", "villager"};
objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), std::move(type));
- auto* villager_rel = dynamic_cast<nt::ObjectManager::Relation*>(
- objects.Find(path)->object.get());
+ auto* villager_rel =
+ dynamic_cast<nt::ObjectManager::Relation*>(objects.Find(path)->object.get());
// --- Populate backend with three tuples via the Merkle tree ---
nt::SqliteBackend backend;
auto store = [&](std::vector<nt::Attribute> attrs) {
auto bytes = nt::TupleCodec::Serialize(attrs);
- auto hash = backend.Put(std::move(bytes));
+ auto hash = backend.Put(std::move(bytes));
auto hash_bin = nt::hex_to_bin(hash);
villager_rel->merkle_root =
- nt::Merkle<nt::Hash32>::Insert(backend, villager_rel->merkle_root,
- hash_bin, hash_bin);
+ nt::Merkle<nt::Hash32>::Insert(backend, villager_rel->merkle_root, hash_bin, hash_bin);
};
- store({ { "name", "Blathers" }, { "profession", "Museum Curator" } });
- store({ { "name", "Rover" }, { "profession", "Traveller" } });
- store({ { "name", "K.K." }, { "profession", "Artist" } });
+ store({{"name", "Blathers"}, {"profession", "Museum Curator"}});
+ store({{"name", "Rover"}, {"profession", "Traveller"}});
+ store({{"name", "K.K."}, {"profession", "Artist"}});
nt::CursorManager cursors(backend);
// --- Open a handle on villager through the full manager pipeline ---
@@ -69,18 +65,16 @@ int main()
nt::NamespaceReferenceManager references(objects, backend);
nt::HandlerManager handler(objects, permissions, identities, lifecycles, references);
- int connection = 1; // dummy connection context
+ int connection = 1; // dummy connection context
nt::HandlerManager::handle* handle = handler.Open(path, &connection);
- if (handle == nullptr)
- {
+ if (handle == nullptr) {
std::cout << "Failed to open handle for villager\n";
return 1;
}
// --- Open a cursor on the relation ---
nt::CursorManager::cursor* cursor = cursors.Open(handle, villager_rel->merkle_root);
- if (cursor == nullptr)
- {
+ if (cursor == nullptr) {
std::cout << "Failed to open cursor for villager\n";
handler.Close(handle);
return 1;
@@ -88,17 +82,15 @@ int main()
// --- Build a SCAN plan and pull all tuples lazily through the FOL VM ---
nt::PlanNode plan;
- plan.op = nt::PlanNode::Op::SCAN;
+ plan.op = nt::FOL_OPERATION_SCAN;
plan.scan_cursor = cursor;
nt::VM vm(cursors);
std::cout << "Tuples in villager:\n";
- while (nt::Tuple* t = vm.Next(&plan))
- {
+ while (nt::Tuple* t = vm.Next(&plan)) {
std::cout << " ";
const nt::Attribute* attr = t->Next();
- while (attr != nullptr)
- {
+ while (attr != nullptr) {
std::cout << attr->name << "=" << attr->value;
attr = t->Next();
if (attr != nullptr)
diff --git a/src/SqliteBackend.cpp b/src/SqliteBackend.cpp
index 7e9c9d5..3e01058 100644
--- a/src/SqliteBackend.cpp
+++ b/src/SqliteBackend.cpp
@@ -3,60 +3,48 @@
#include <picosha2.h>
#include <stdexcept>
-namespace nt
-{
- SqliteBackend::SqliteBackend(const std::string& path)
- {
+namespace nt {
+ SqliteBackend::SqliteBackend(const std::string& path) {
if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK)
throw std::runtime_error(sqlite3_errmsg(db_));
- const char* schema =
- "CREATE TABLE IF NOT EXISTS kv ("
- " hash TEXT PRIMARY KEY,"
- " value BLOB NOT NULL"
- ");";
+ const char* schema = "CREATE TABLE IF NOT EXISTS kv ("
+ " hash TEXT PRIMARY KEY,"
+ " value BLOB NOT NULL"
+ ");";
char* err = nullptr;
- if (sqlite3_exec(db_, schema, nullptr, nullptr, &err) != SQLITE_OK)
- {
+ if (sqlite3_exec(db_, schema, nullptr, nullptr, &err) != SQLITE_OK) {
std::string msg(err);
sqlite3_free(err);
throw std::runtime_error(msg);
}
}
- SqliteBackend::~SqliteBackend()
- {
+ SqliteBackend::~SqliteBackend() {
sqlite3_close(db_);
}
- std::string SqliteBackend::Put(std::vector<uint8_t> value)
- {
+ std::string SqliteBackend::Put(std::vector<uint8_t> value) {
const std::string hash = picosha2::hash256_hex_string(value.begin(), value.end());
sqlite3_stmt* stmt = nullptr;
- sqlite3_prepare_v2(db_,
- "INSERT OR IGNORE INTO kv (hash, value) VALUES (?, ?)",
- -1, &stmt, nullptr);
+ sqlite3_prepare_v2(db_, "INSERT OR IGNORE INTO kv (hash, value) VALUES (?, ?)", -1, &stmt,
+ nullptr);
sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_TRANSIENT);
- sqlite3_bind_blob(stmt, 2, value.data(), static_cast<int>(value.size()),
- SQLITE_TRANSIENT);
+ sqlite3_bind_blob(stmt, 2, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
return hash;
}
- std::optional<std::vector<uint8_t>> SqliteBackend::Get(const std::string& hash)
- {
+ std::optional<std::vector<uint8_t>> SqliteBackend::Get(const std::string& hash) {
sqlite3_stmt* stmt = nullptr;
- sqlite3_prepare_v2(db_,
- "SELECT value FROM kv WHERE hash = ?",
- -1, &stmt, nullptr);
+ sqlite3_prepare_v2(db_, "SELECT value FROM kv WHERE hash = ?", -1, &stmt, nullptr);
sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_TRANSIENT);
std::optional<std::vector<uint8_t>> result;
- if (sqlite3_step(stmt) == SQLITE_ROW)
- {
+ if (sqlite3_step(stmt) == SQLITE_ROW) {
const auto* blob = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 0));
int size = sqlite3_column_bytes(stmt, 0);
result = std::vector<uint8_t>(blob, blob + size);
@@ -64,4 +52,4 @@ namespace nt
sqlite3_finalize(stmt);
return result;
}
-}
+} // namespace nt
diff --git a/src/TupleCodec.cpp b/src/TupleCodec.cpp
index 9b76046..a78ed5b 100644
--- a/src/TupleCodec.cpp
+++ b/src/TupleCodec.cpp
@@ -7,23 +7,19 @@
#include <cstring>
#include <numeric>
-namespace nt::TupleCodec
-{
- static std::vector<Attribute> Sorted(std::vector<Attribute> attrs)
- {
+namespace nt::TupleCodec {
+ static std::vector<Attribute> Sorted(std::vector<Attribute> attrs) {
std::sort(attrs.begin(), attrs.end(),
[](const Attribute& a, const Attribute& b) { return a.name < b.name; });
return attrs;
}
- std::string Hash(const std::vector<Attribute>& attrs)
- {
+ std::string Hash(const std::vector<Attribute>& attrs) {
auto bytes = Serialize(attrs);
return picosha2::hash256_hex_string(bytes.begin(), bytes.end());
}
- std::vector<uint8_t> Serialize(const std::vector<Attribute>& attrs)
- {
+ std::vector<uint8_t> Serialize(const std::vector<Attribute>& attrs) {
// Format: [uint32_le name_len][name][uint32_le value_len][value] ...
// Attributes are stored in sorted name order for determinism.
std::vector<uint8_t> out;
@@ -34,8 +30,7 @@ namespace nt::TupleCodec
out.push_back(static_cast<uint8_t>(n >> 24));
};
- for (const auto& a : Sorted({ attrs.begin(), attrs.end() }))
- {
+ for (const auto& a : Sorted({attrs.begin(), attrs.end()})) {
write_u32(static_cast<uint32_t>(a.name.size()));
out.insert(out.end(), a.name.begin(), a.name.end());
write_u32(static_cast<uint32_t>(a.value.size()));
@@ -44,29 +39,29 @@ namespace nt::TupleCodec
return out;
}
- std::vector<Attribute> Deserialize(const std::vector<uint8_t>& bytes)
- {
+ std::vector<Attribute> Deserialize(const std::vector<uint8_t>& bytes) {
auto read_u32 = [&](size_t pos) -> uint32_t {
- return static_cast<uint32_t>(bytes[pos])
- | (static_cast<uint32_t>(bytes[pos + 1]) << 8)
- | (static_cast<uint32_t>(bytes[pos + 2]) << 16)
- | (static_cast<uint32_t>(bytes[pos + 3]) << 24);
+ return static_cast<uint32_t>(bytes[pos]) |
+ (static_cast<uint32_t>(bytes[pos + 1]) << 8) |
+ (static_cast<uint32_t>(bytes[pos + 2]) << 16) |
+ (static_cast<uint32_t>(bytes[pos + 3]) << 24);
};
std::vector<Attribute> result;
size_t pos = 0;
- while (pos + 8 <= bytes.size())
- {
- uint32_t name_len = read_u32(pos); pos += 4;
+ while (pos + 8 <= bytes.size()) {
+ uint32_t name_len = read_u32(pos);
+ pos += 4;
std::string name(reinterpret_cast<const char*>(bytes.data() + pos), name_len);
pos += name_len;
- uint32_t value_len = read_u32(pos); pos += 4;
+ uint32_t value_len = read_u32(pos);
+ pos += 4;
std::string value(reinterpret_cast<const char*>(bytes.data() + pos), value_len);
pos += value_len;
- result.push_back({ std::move(name), std::move(value) });
+ result.push_back({std::move(name), std::move(value)});
}
return result;
}
-}
+} // namespace nt::TupleCodec
diff --git a/src/VM.cpp b/src/VM.cpp
index a042aed..5f66f83 100644
--- a/src/VM.cpp
+++ b/src/VM.cpp
@@ -1,102 +1,99 @@
#include "VM.h"
#include "Types.h"
-namespace nt
-{
- VM::VM(CursorManager& cursors) : cursors_(cursors) {}
+namespace nt {
+ VM::VM(CursorManager& cursors) : cursors_(cursors) {
+ }
- std::vector<std::string> VM::ResolveArgs(const std::vector<PathArg>& tmpl, Tuple* from)
- {
+ std::vector<std::string> VM::ResolveArgs(const std::vector<PathArg>& tmpl, Tuple* from) {
std::vector<std::string> resolved;
resolved.reserve(tmpl.size());
- for (const auto& arg : tmpl)
- {
- if (arg.kind == PathArg::Kind::Const)
- {
+ for (const auto& arg : tmpl) {
+ if (arg.kind == PathArg::Kind::Const) {
resolved.push_back(arg.name);
continue;
}
std::string val;
for (const auto& attr : from->attrs())
- if (attr.name == arg.name) { val = attr.value; break; }
+ if (attr.name == arg.name) {
+ val = attr.value;
+ break;
+ }
resolved.push_back(std::move(val));
}
return resolved;
}
- void VM::ResetInner(CursorManager::cursor* c, std::vector<std::string> args)
- {
- c->args = std::move(args);
+ void VM::ResetInner(CursorManager::cursor* c, std::vector<std::string> args) {
+ c->args = std::move(args);
c->page.clear();
c->page_position = 0;
- c->fetch_offset = 0;
- c->exhausted = false;
+ c->fetch_offset = 0;
+ c->exhausted = false;
}
- Tuple* VM::MergeInto(PlanNode* node, Tuple* left, Tuple* right)
- {
+ Tuple* VM::MergeInto(PlanNode* node, Tuple* left, Tuple* right) {
std::vector<Attribute> merged;
- for (const auto& a : left->attrs()) merged.push_back(a);
- for (const auto& a : right->attrs()) merged.push_back(a);
+ for (const auto& a : left->attrs())
+ merged.push_back(a);
+ for (const auto& a : right->attrs())
+ merged.push_back(a);
node->join_buffer.emplace(std::move(merged));
return &*node->join_buffer;
}
- Tuple* VM::Next(PlanNode* node)
- {
- if (node == nullptr) return nullptr;
-
- switch (node->op)
- {
- case PlanNode::Op::SCAN:
- return cursors_.Next(node->scan_cursor);
+ Tuple* VM::Next(PlanNode* node) {
+ if (node == nullptr)
+ return nullptr;
- case PlanNode::Op::PROJECT:
- {
- Tuple* next = Next(node->left);
- if (next == nullptr) return nullptr;
+ switch (node->op) {
+ case Operation::FOL_OPERATION_SCAN:
+ return cursors_.Next(node->scan_cursor);
- std::vector<Attribute> projected;
- for (const auto& attr : next->attrs())
- {
- if (node->project_attrs.find(attr.name) != node->project_attrs.end())
- projected.push_back(attr);
- }
+ case Operation::FOL_OPERATION_PROJECT: {
+ Tuple* next = Next(node->left);
+ if (next == nullptr)
+ return nullptr;
- node->project_buffer.emplace(std::move(projected));
- return &*node->project_buffer;
+ std::vector<Attribute> projected;
+ for (const auto& attr : next->attrs()) {
+ if (node->project_attrs.find(attr.name) != node->project_attrs.end())
+ projected.push_back(attr);
}
- case PlanNode::Op::JOIN:
- {
- while (true)
- {
+ node->project_buffer.emplace(std::move(projected));
+ return &*node->project_buffer;
+ }
+
+ case Operation::FOL_OPERATION_JOIN: {
+ while (true) {
+ if (node->join_left == nullptr) {
+ node->join_left = Next(node->left);
if (node->join_left == nullptr)
- {
- node->join_left = Next(node->left);
- if (node->join_left == nullptr) return nullptr;
+ return nullptr;
- auto args = ResolveArgs(node->right->scan_args, node->join_left);
- ResetInner(node->right->scan_cursor, std::move(args));
- }
+ auto args = ResolveArgs(node->right->scan_args, node->join_left);
+ ResetInner(node->right->scan_cursor, std::move(args));
+ }
- Tuple* right = Next(node->right);
- if (right != nullptr)
- return MergeInto(node, node->join_left, right);
+ Tuple* right = Next(node->right);
+ if (right != nullptr)
+ return MergeInto(node, node->join_left, right);
- node->join_left = nullptr;
- }
+ node->join_left = nullptr;
}
+ }
- case PlanNode::Op::TAKE:
- {
- if (node->take_count >= node->take_limit) return nullptr;
- Tuple* t = Next(node->left);
- if (t) ++node->take_count;
- return t;
- }
+ case Operation::FOL_OPERATION_TAKE: {
+ if (node->take_count >= node->take_limit)
+ return nullptr;
+ Tuple* t = Next(node->left);
+ if (t)
+ ++node->take_count;
+ return t;
+ }
}
return nullptr;
}
-}
+} // namespace nt
diff --git a/test/C_API.cpp b/test/C_API.cpp
index f186374..892ed24 100644
--- a/test/C_API.cpp
+++ b/test/C_API.cpp
@@ -15,27 +15,26 @@
// test does not affect another.
// ---------------------------------------------------------------------------
-namespace
-{
+namespace {
struct InitGuard {
- InitGuard() { REQUIRE(rnt_init("memory", nullptr) == 0); }
+ InitGuard() {
+ REQUIRE(rnt_init("memory", nullptr) == 0);
+ }
};
- std::string take_string(char* p)
- {
+ std::string take_string(char* p) {
std::string s = p ? p : "";
rnt_free_string(p);
return s;
}
-}
+} // namespace
// ---------------------------------------------------------------------------
// Step 2 — Branch hash-pointer model
// ---------------------------------------------------------------------------
TEST_CASE("rnt_register_branch with empty target creates an unborn branch",
- "[step2][capi][branch]")
-{
+ "[step2][capi][branch]") {
InitGuard _;
const char* path = "/system/branches/test_step2_unborn";
REQUIRE(rnt_register_branch(path, "") == 0);
@@ -50,25 +49,20 @@ TEST_CASE("rnt_register_branch with empty target creates an unborn branch",
REQUIRE(rnt_close_handle(h) == 0);
}
-TEST_CASE("rnt_register_branch rejects an unregistered target hash",
- "[step2][capi][branch]")
-{
+TEST_CASE("rnt_register_branch rejects an unregistered target hash", "[step2][capi][branch]") {
InitGuard _;
- REQUIRE(rnt_register_branch("/system/branches/test_step2_bad_init",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
- != 0);
+ REQUIRE(rnt_register_branch(
+ "/system/branches/test_step2_bad_init",
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
}
-TEST_CASE("rnt_branch_advance rejects an unregistered target hash",
- "[step2][capi][branch]")
-{
+TEST_CASE("rnt_branch_advance rejects an unregistered target hash", "[step2][capi][branch]") {
InitGuard _;
const char* bpath = "/system/branches/test_step2_bad_advance";
REQUIRE(rnt_register_branch(bpath, "") == 0);
REQUIRE(rnt_branch_advance(
- bpath,
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
+ bpath, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
// Target unchanged.
rnt_handle_t h = rnt_open_handle(bpath, nullptr);
@@ -79,9 +73,7 @@ TEST_CASE("rnt_branch_advance rejects an unregistered target hash",
rnt_close_handle(h);
}
-TEST_CASE("Writing a tuple advances the branch to a new snapshot",
- "[step2][step3][capi][branch]")
-{
+TEST_CASE("Writing a tuple advances the branch to a new snapshot", "[step2][step3][capi][branch]") {
InitGuard _;
const char* bpath = "/system/branches/test_step2_advance";
const char* rpath = "/system/branches/test_step2_advance/multigroups/public/relations/items";
@@ -117,9 +109,7 @@ TEST_CASE("Writing a tuple advances the branch to a new snapshot",
// Step 3+4 — Copy-on-write writes and resolver wiring
// ---------------------------------------------------------------------------
-TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it",
- "[step3][capi][cow]")
-{
+TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it", "[step3][capi][cow]") {
InitGuard _;
const char* bpath = "/system/branches/test_step3_link";
const char* rpath = "/system/branches/test_step3_link/multigroups/public/relations/items";
@@ -138,12 +128,10 @@ TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it",
REQUIRE(rnt_relation_root(rpath, &root) == 0);
const std::string new_root = take_string(root);
- REQUIRE(new_root.size() == 64); // a real Merkle root now
+ REQUIRE(new_root.size() == 64); // a real Merkle root now
}
-TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots",
- "[step3][capi][cow]")
-{
+TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots", "[step3][capi][cow]") {
InitGuard _;
const char* bpath = "/system/branches/test_step3_unlink";
const char* rpath = "/system/branches/test_step3_unlink/multigroups/public/relations/items";
@@ -171,8 +159,7 @@ TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots",
}
TEST_CASE("rnt_open_handle resolves branch-relative paths to snapshot relations",
- "[step4][capi][resolver]")
-{
+ "[step4][capi][resolver]") {
InitGuard _;
const char* bpath = "/system/branches/test_step4_resolve";
const char* rpath = "/system/branches/test_step4_resolve/multigroups/public/relations/items";
@@ -203,26 +190,23 @@ TEST_CASE("rnt_open_handle resolves branch-relative paths to snapshot relations"
rnt_close_handle(h);
}
-TEST_CASE("Reading an unborn branch's relation fails cleanly",
- "[step4][capi][resolver]")
-{
+TEST_CASE("Reading an unborn branch's relation fails cleanly", "[step4][capi][resolver]") {
InitGuard _;
const char* bpath = "/system/branches/test_step4_unborn";
REQUIRE(rnt_register_branch(bpath, "") == 0);
// No relation registered: relation_root miss returns nonzero.
char* root = nullptr;
- REQUIRE(rnt_relation_root(
- "/system/branches/test_step4_unborn/multigroups/public/relations/missing", &root) != 0);
+ REQUIRE(
+ rnt_relation_root("/system/branches/test_step4_unborn/multigroups/public/relations/missing",
+ &root) != 0);
}
// ---------------------------------------------------------------------------
// Step 5 — Session lifecycle and overrides
// ---------------------------------------------------------------------------
-TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash",
- "[step5][capi][session]")
-{
+TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash", "[step5][capi][session]") {
InitGuard _;
char* a = nullptr;
char* b = nullptr;
@@ -238,17 +222,14 @@ TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash",
REQUIRE(rnt_session_close(hb.c_str()) == 0);
}
-TEST_CASE("rnt_session_close on an unknown hash fails",
- "[step5][capi][session]")
-{
+TEST_CASE("rnt_session_close on an unknown hash fails", "[step5][capi][session]") {
InitGuard _;
- REQUIRE(rnt_session_close(
- "0000000000000000000000000000000000000000000000000000000000000000") != 0);
+ REQUIRE(rnt_session_close("0000000000000000000000000000000000000000000000000000000000000000") !=
+ 0);
}
TEST_CASE("Session override redirects branch-relative reads to a different snapshot",
- "[step5][capi][session][resolver]")
-{
+ "[step5][capi][session][resolver]") {
InitGuard _;
const char* bpath = "/system/branches/test_step5_override";
const char* rpath = "/system/branches/test_step5_override/multigroups/public/relations/items";
@@ -273,9 +254,7 @@ TEST_CASE("Session override redirects branch-relative reads to a different snaps
char* sess = nullptr;
REQUIRE(rnt_session_open(nullptr, &sess) == 0);
const std::string sid = take_string(sess);
- REQUIRE(rnt_session_set_branch(sid.c_str(),
- "test_step5_override",
- s1.c_str()) == 0);
+ REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_override", s1.c_str()) == 0);
// Advance the branch to S2 — global HEAD moves on; the session override
// keeps S1 resident.
@@ -283,9 +262,8 @@ TEST_CASE("Session override redirects branch-relative reads to a different snaps
rnt_free_string(th);
// Session view: still S1 (one tuple).
- const std::string spath =
- "/system/sessions/" + sid +
- "/branches/test_step5_override/multigroups/public/relations/items";
+ const std::string spath = "/system/sessions/" + sid +
+ "/branches/test_step5_override/multigroups/public/relations/items";
rnt_handle_t h_session = rnt_open_handle(spath.c_str(), nullptr);
REQUIRE(h_session != nullptr);
@@ -294,8 +272,7 @@ TEST_CASE("Session override redirects branch-relative reads to a different snaps
int rows_under_session = 0;
char* row = nullptr;
- while (rnt_cursor_next(c_session, &row) == 1)
- {
+ while (rnt_cursor_next(c_session, &row) == 1) {
++rows_under_session;
rnt_free_string(row);
}
@@ -310,7 +287,10 @@ TEST_CASE("Session override redirects branch-relative reads to a different snaps
rnt_cursor_t c_global = rnt_cursor_open(h_global);
REQUIRE(c_global != nullptr);
int rows_global = 0;
- while (rnt_cursor_next(c_global, &row) == 1) { ++rows_global; rnt_free_string(row); }
+ while (rnt_cursor_next(c_global, &row) == 1) {
+ ++rows_global;
+ rnt_free_string(row);
+ }
REQUIRE(rows_global == 2);
rnt_cursor_close(c_global);
rnt_close_handle(h_global);
@@ -319,8 +299,7 @@ TEST_CASE("Session override redirects branch-relative reads to a different snaps
}
TEST_CASE("Clearing a session override falls back to the global branch HEAD",
- "[step5][capi][session][resolver]")
-{
+ "[step5][capi][session][resolver]") {
InitGuard _;
const char* bpath = "/system/branches/test_step5_clear";
const char* rpath = "/system/branches/test_step5_clear/multigroups/public/relations/items";
@@ -351,15 +330,17 @@ TEST_CASE("Clearing a session override falls back to the global branch HEAD",
// Now the session sees the global view (two tuples).
const std::string spath =
- "/system/sessions/" + sid +
- "/branches/test_step5_clear/multigroups/public/relations/items";
+ "/system/sessions/" + sid + "/branches/test_step5_clear/multigroups/public/relations/items";
rnt_handle_t h = rnt_open_handle(spath.c_str(), nullptr);
REQUIRE(h != nullptr);
rnt_cursor_t c = rnt_cursor_open(h);
REQUIRE(c != nullptr);
int rows = 0;
char* row = nullptr;
- while (rnt_cursor_next(c, &row) == 1) { ++rows; rnt_free_string(row); }
+ while (rnt_cursor_next(c, &row) == 1) {
+ ++rows;
+ rnt_free_string(row);
+ }
REQUIRE(rows == 2);
rnt_cursor_close(c);
rnt_close_handle(h);
@@ -367,18 +348,15 @@ TEST_CASE("Clearing a session override falls back to the global branch HEAD",
REQUIRE(rnt_session_close(sid.c_str()) == 0);
}
-TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot",
- "[step5][capi][session]")
-{
+TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot", "[step5][capi][session]") {
InitGuard _;
char* sess = nullptr;
REQUIRE(rnt_session_open(nullptr, &sess) == 0);
const std::string sid = take_string(sess);
REQUIRE(rnt_session_set_branch(
- sid.c_str(),
- "anything",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
+ sid.c_str(), "anything",
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
REQUIRE(rnt_session_close(sid.c_str()) == 0);
}
@@ -389,39 +367,34 @@ TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot",
// or its enumerable relation set.
// ---------------------------------------------------------------------------
-TEST_CASE("Two multigroups under one branch advance independently",
- "[capi][multi-mg]")
-{
+TEST_CASE("Two multigroups under one branch advance independently", "[capi][multi-mg]") {
InitGuard _;
const std::string bpath = "/system/branches/test_multi_mg";
REQUIRE(rnt_register_branch(bpath.c_str(), "") == 0);
// Register one relation in each of two distinct multigroups.
- REQUIRE(rnt_register_relation(
- (bpath + "/multigroups/warehouse/relations/orders").c_str()) == 0);
- REQUIRE(rnt_register_relation(
- (bpath + "/multigroups/audit/relations/events").c_str()) == 0);
+ REQUIRE(rnt_register_relation((bpath + "/multigroups/warehouse/relations/orders").c_str()) ==
+ 0);
+ REQUIRE(rnt_register_relation((bpath + "/multigroups/audit/relations/events").c_str()) == 0);
char* mgs_raw = nullptr;
REQUIRE(rnt_list_branch_multigroups(bpath.c_str(), &mgs_raw) == 0);
const std::string mgs = take_string(mgs_raw);
REQUIRE(mgs.find("warehouse\t") != std::string::npos);
- REQUIRE(mgs.find("audit\t") != std::string::npos);
+ REQUIRE(mgs.find("audit\t") != std::string::npos);
// Insert into warehouse; record audit's root before/after.
char* audit_before = nullptr;
- REQUIRE(rnt_list_relations(
- (bpath + "/multigroups/audit").c_str(), &audit_before) == 0);
+ REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_before) == 0);
const std::string audit_before_s = take_string(audit_before);
char* th = nullptr;
- REQUIRE(rnt_link_tuple((bpath + "/multigroups/warehouse/relations/orders").c_str(),
- "id=1\n", &th) == 0);
+ REQUIRE(rnt_link_tuple((bpath + "/multigroups/warehouse/relations/orders").c_str(), "id=1\n",
+ &th) == 0);
rnt_free_string(th);
char* audit_after = nullptr;
- REQUIRE(rnt_list_relations(
- (bpath + "/multigroups/audit").c_str(), &audit_after) == 0);
+ REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_after) == 0);
const std::string audit_after_s = take_string(audit_after);
// audit's relation set is byte-identical: the warehouse-side mutation
@@ -430,13 +403,13 @@ TEST_CASE("Two multigroups under one branch advance independently",
// warehouse/orders is now non-empty.
char* root = nullptr;
- REQUIRE(rnt_relation_root(
- (bpath + "/multigroups/warehouse/relations/orders").c_str(), &root) == 0);
+ REQUIRE(rnt_relation_root((bpath + "/multigroups/warehouse/relations/orders").c_str(), &root) ==
+ 0);
REQUIRE(!take_string(root).empty());
// audit/events still empty.
char* root2 = nullptr;
- REQUIRE(rnt_relation_root(
- (bpath + "/multigroups/audit/relations/events").c_str(), &root2) == 0);
+ REQUIRE(rnt_relation_root((bpath + "/multigroups/audit/relations/events").c_str(), &root2) ==
+ 0);
REQUIRE(take_string(root2).empty());
}
diff --git a/test/CursorManager.cpp b/test/CursorManager.cpp
index 8c7de1a..c823ec5 100644
--- a/test/CursorManager.cpp
+++ b/test/CursorManager.cpp
@@ -27,55 +27,48 @@
// the handle's death is degenerate but safe.
// ---------------------------------------------------------------------------
-namespace
-{
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = { OPEN, CLOSE };
+namespace {
+ std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = {OPEN, CLOSE};
return t;
}
std::unique_ptr<nt::ObjectManager::ephemeral_object_type>
- make_constant_eph_type(std::vector<nt::Attribute> attrs)
- {
- auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->methods = { OPEN, CLOSE };
+ make_constant_eph_type(std::vector<nt::Attribute> attrs) {
+ auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
+ t->label = EPHEMERAL_RELATION;
+ t->methods = {OPEN, CLOSE};
t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::Finite;
- t->generator = [attrs](const std::vector<std::string>&,
- std::size_t offset, std::size_t limit)
- -> std::vector<nt::Tuple>
- {
- if (offset > 0 || limit == 0) return {};
- return { nt::Tuple(attrs) };
+ t->generator = [attrs](const std::vector<std::string>&, std::size_t offset,
+ std::size_t limit) -> std::vector<nt::Tuple> {
+ if (offset > 0 || limit == 0)
+ return {};
+ return {nt::Tuple(attrs)};
};
return t;
}
struct Stack {
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles { objects, backend };
- nt::NamespaceReferenceManager references { objects, backend };
- nt::HandlerManager handler { objects, permissions, identities,
- lifecycles, references };
- nt::CursorManager cursors { backend, &lifecycles, &objects };
+ nt::InMemoryBackend backend;
+ nt::ObjectManager objects;
+ nt::PermissionsManager permissions;
+ nt::IdentityManager identities;
+ nt::LifecycleManager lifecycles{objects, backend};
+ nt::NamespaceReferenceManager references{objects, backend};
+ nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
+ nt::CursorManager cursors{backend, &lifecycles, &objects};
};
-}
+} // namespace
-TEST_CASE("Cursor pins the parent snapshot and unpins on close",
- "[step6][cursor][pin]")
-{
+TEST_CASE("Cursor pins the parent snapshot and unpins on close", "[step6][cursor][pin]") {
Stack s;
// Stand up a minimal snapshot with one relation.
auto& om = s.objects;
- om.Register({"system", "snapshots", "Hcur"},
- std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ om.Register({"system", "snapshots", "Hcur"}, std::make_unique<nt::ObjectManager::Multigroup>(),
+ make_type(MULTIGROUP));
om.Register({"system", "snapshots", "Hcur", "relations", "items"},
std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
@@ -93,27 +86,25 @@ TEST_CASE("Cursor pins the parent snapshot and unpins on close",
int conn = 1;
auto* h = s.handler.Open({"system", "snapshots", "Hcur", "relations", "items"}, &conn);
REQUIRE(h != nullptr);
- REQUIRE(mg_entry->head->reference_count == 1); // external pin only
+ REQUIRE(mg_entry->head->reference_count == 1); // external pin only
auto* c = s.cursors.Open(h);
REQUIRE(c != nullptr);
REQUIRE(c->pinned_snapshot == mg_entry);
- REQUIRE(mg_entry->head->reference_count == 2); // external + cursor pin
+ REQUIRE(mg_entry->head->reference_count == 2); // external + cursor pin
s.cursors.Close(c);
- REQUIRE(mg_entry->head->reference_count == 1); // back to external only
+ REQUIRE(mg_entry->head->reference_count == 1); // back to external only
s.handler.Close(h);
- s.lifecycles.Unpin(mg_entry); // releases the external pin; cascade-collects.
+ s.lifecycles.Unpin(mg_entry); // releases the external pin; cascade-collects.
}
TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
- "[step6][cursor][handle-independence]")
-{
+ "[step6][cursor][handle-independence]") {
Stack s;
// Register an ephemeral relation; its generator yields one tuple.
- s.objects.Register({"eph"},
- std::make_unique<nt::ObjectManager::Relation>(),
+ s.objects.Register({"eph"}, std::make_unique<nt::ObjectManager::Relation>(),
make_constant_eph_type({{"k", "v"}}));
int conn = 1;
@@ -124,7 +115,7 @@ TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
// Ephemeral cursors start exhausted; reset to simulate a JOIN probe.
c->exhausted = false;
- c->args = {};
+ c->args = {};
// Now violate the documented contract: close the handle BEFORE Next().
// The cursor must still produce its tuple from the captured generator
@@ -135,18 +126,19 @@ TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
REQUIRE(tuple != nullptr);
bool found = false;
for (const auto& a : tuple->attrs())
- if (a.name == "k" && a.value == "v") { found = true; break; }
+ if (a.name == "k" && a.value == "v") {
+ found = true;
+ break;
+ }
REQUIRE(found);
s.cursors.Close(c);
}
TEST_CASE("Cursor opened against a non-snapshot relation does not pin anything",
- "[step6][cursor][pin]")
-{
+ "[step6][cursor][pin]") {
Stack s;
- s.objects.Register({"plain", "rel"},
- std::make_unique<nt::ObjectManager::Relation>(),
+ s.objects.Register({"plain", "rel"}, std::make_unique<nt::ObjectManager::Relation>(),
make_type(RELATION));
int conn = 1;
auto* h = s.handler.Open({"plain", "rel"}, &conn);
diff --git a/test/LifecycleManager.cpp b/test/LifecycleManager.cpp
index aa75e14..3712522 100644
--- a/test/LifecycleManager.cpp
+++ b/test/LifecycleManager.cpp
@@ -16,22 +16,18 @@
// unpin every Relation registered under their snapshot subtree.
// ---------------------------------------------------------------------------
-namespace
-{
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = { OPEN, CLOSE };
+namespace {
+ std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = {OPEN, CLOSE};
return t;
}
-}
+} // namespace
-TEST_CASE("Pin and Unpin adjust reference_count symmetrically",
- "[step6][lifecycle][pin]")
-{
+TEST_CASE("Pin and Unpin adjust reference_count symmetrically", "[step6][lifecycle][pin]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* entry = om.Find({"r1"});
@@ -48,19 +44,17 @@ TEST_CASE("Pin and Unpin adjust reference_count symmetrically",
REQUIRE(om.Find({"r1"}) != nullptr);
}
-TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]")
-{
+TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
REQUIRE_NOTHROW(lm.Pin(nullptr));
REQUIRE_NOTHROW(lm.Unpin(nullptr));
}
-TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]")
-{
+TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* entry = om.Find({"r1"});
@@ -69,10 +63,9 @@ TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]")
REQUIRE(entry->head->reference_count == 0);
}
-TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]")
-{
+TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* entry = om.Find({"r1"});
@@ -81,22 +74,21 @@ TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]"
lm.Pin(entry);
// handle_count=1, ref_count=1.
- lm.Unpin(entry); // ref_count → 0, but handle_count still 1.
+ lm.Unpin(entry); // ref_count → 0, but handle_count still 1.
REQUIRE(om.Find({"r1"}) != nullptr);
lm.Pin(entry);
- lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1.
+ lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1.
REQUIRE(om.Find({"r1"}) != nullptr);
- lm.Unpin(entry); // both zero → GC.
+ lm.Unpin(entry); // both zero → GC.
REQUIRE(om.Find({"r1"}) == nullptr);
}
TEST_CASE("Unmonitor that brings both counters to zero collects the entry",
- "[step6][lifecycle][gc]")
-{
+ "[step6][lifecycle][gc]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* entry = om.Find({"r1"});
@@ -107,11 +99,9 @@ TEST_CASE("Unmonitor that brings both counters to zero collects the entry",
REQUIRE(om.Find({"r1"}) == nullptr);
}
-TEST_CASE("IsEligibleForGC reflects the joint-zero rule",
- "[step6][lifecycle][gc]")
-{
+TEST_CASE("IsEligibleForGC reflects the joint-zero rule", "[step6][lifecycle][gc]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* entry = om.Find({"r1"});
@@ -122,21 +112,18 @@ TEST_CASE("IsEligibleForGC reflects the joint-zero rule",
}
TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection",
- "[step6][lifecycle][cascade]")
-{
+ "[step6][lifecycle][cascade]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
- om.Register({"system", "snapshots", "H1"},
- std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ om.Register({"system", "snapshots", "H1"}, std::make_unique<nt::ObjectManager::Multigroup>(),
+ make_type(MULTIGROUP));
auto* mg_entry = om.Find({"system", "snapshots", "H1"});
- for (const auto& n : { std::string{"foo"}, std::string{"bar"} })
- {
+ for (const auto& n : {std::string{"foo"}, std::string{"bar"}}) {
om.Register({"system", "snapshots", "H1", "relations", n},
- std::make_unique<nt::ObjectManager::Relation>(),
- make_type(RELATION));
+ std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
// The parent Multigroup's pin on each child, mirroring what
// register_snapshot does in the C API layer.
lm.Pin(om.Find({"system", "snapshots", "H1", "relations", n}));
@@ -148,38 +135,35 @@ TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection",
lm.Pin(mg_entry);
lm.Unpin(mg_entry);
- REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr);
}
-TEST_CASE("Cascade survives a child with an open handle",
- "[step6][lifecycle][cascade]")
-{
+TEST_CASE("Cascade survives a child with an open handle", "[step6][lifecycle][cascade]") {
nt::InMemoryBackend store;
- nt::ObjectManager om;
+ nt::ObjectManager om;
nt::LifecycleManager lm(om, store);
- om.Register({"system", "snapshots", "H2"},
- std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ om.Register({"system", "snapshots", "H2"}, std::make_unique<nt::ObjectManager::Multigroup>(),
+ make_type(MULTIGROUP));
auto* mg_entry = om.Find({"system", "snapshots", "H2"});
om.Register({"system", "snapshots", "H2", "relations", "live"},
- std::make_unique<nt::ObjectManager::Relation>(),
- make_type(RELATION));
+ std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
auto* child = om.Find({"system", "snapshots", "H2", "relations", "live"});
- lm.Pin(child); // parent's pin
- lm.Monitor(child); // simulate an open handle on the child
+ lm.Pin(child); // parent's pin
+ lm.Monitor(child); // simulate an open handle on the child
// Drop the Multigroup's external ref: cascade unpins child (ref → 0),
// but handle_count is still 1 — child must survive.
lm.Pin(mg_entry);
lm.Unpin(mg_entry);
- REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr);
// Closing the handle now releases the child.
lm.Unmonitor(child);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr);
}
diff --git a/test/MultigroupCodec.cpp b/test/MultigroupCodec.cpp
index c1e2509..b04c405 100644
--- a/test/MultigroupCodec.cpp
+++ b/test/MultigroupCodec.cpp
@@ -17,48 +17,42 @@
namespace {
-// Pad a short label into a deterministic 64-char hex string so the codec can
-// decode it as a 32-byte payload.
-std::string mk_root(char c, char d = '0')
-{
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
-}
-
-} // namespace
-
-TEST_CASE("Build is order-independent - tree sorts intrinsically",
- "[multigroup-codec]")
-{
+ // Pad a short label into a deterministic 64-char hex string so the codec can
+ // decode it as a 32-byte payload.
+ std::string mk_root(char c, char d = '0') {
+ std::string h(64, '0');
+ h[0] = c;
+ h[1] = d;
+ return h;
+ }
+
+} // namespace
+
+TEST_CASE("Build is order-independent - tree sorts intrinsically", "[multigroup-codec]") {
nt::InMemoryBackend a, b;
std::vector<nt::MultigroupCodec::RelationEntry> in_order = {
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
+ {"alpha", mk_root('a')},
+ {"bravo", mk_root('b')},
{"charlie", mk_root('c')},
};
std::vector<nt::MultigroupCodec::RelationEntry> shuffled = {
{"charlie", mk_root('c')},
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
+ {"alpha", mk_root('a')},
+ {"bravo", mk_root('b')},
};
- REQUIRE(nt::MultigroupCodec::Build(a, in_order)
- == nt::MultigroupCodec::Build(b, shuffled));
+ REQUIRE(nt::MultigroupCodec::Build(a, in_order) == nt::MultigroupCodec::Build(b, shuffled));
}
-TEST_CASE("Build distinguishes content changes", "[multigroup-codec]")
-{
+TEST_CASE("Build distinguishes content changes", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> base = {{"foo", mk_root('a')}};
std::vector<nt::MultigroupCodec::RelationEntry> different_root = {{"foo", mk_root('b')}};
std::vector<nt::MultigroupCodec::RelationEntry> different_name = {{"bar", mk_root('a')}};
- std::vector<nt::MultigroupCodec::RelationEntry> extra_member = {
- {"foo", mk_root('a')}, {"bar", mk_root('b')}
- };
+ std::vector<nt::MultigroupCodec::RelationEntry> extra_member = {{"foo", mk_root('a')},
+ {"bar", mk_root('b')}};
const auto h_base = nt::MultigroupCodec::Build(backend, base);
REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_root));
@@ -66,9 +60,7 @@ TEST_CASE("Build distinguishes content changes", "[multigroup-codec]")
REQUIRE(h_base != nt::MultigroupCodec::Build(backend, extra_member));
}
-TEST_CASE("Build then List round-trips entries in key-sorted order",
- "[multigroup-codec]")
-{
+TEST_CASE("Build then List round-trips entries in key-sorted order", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> entries = {
@@ -86,8 +78,7 @@ TEST_CASE("Build then List round-trips entries in key-sorted order",
REQUIRE(listed[0].second == mk_root('b', '2'));
}
-TEST_CASE("Lookup returns payload by name", "[multigroup-codec]")
-{
+TEST_CASE("Lookup returns payload by name", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> entries = {
@@ -101,9 +92,7 @@ TEST_CASE("Lookup returns payload by name", "[multigroup-codec]")
REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "missing").empty());
}
-TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched",
- "[multigroup-codec]")
-{
+TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> entries = {
@@ -113,16 +102,13 @@ TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched",
const auto root0 = nt::MultigroupCodec::Build(backend, entries);
// Mutate foo only; bar's payload must round-trip unchanged.
- const auto root1 = nt::MultigroupCodec::InsertOne(
- backend, root0, "foo", mk_root('d'));
+ const auto root1 = nt::MultigroupCodec::InsertOne(backend, root0, "foo", mk_root('d'));
REQUIRE(root1 != root0);
REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo") == mk_root('d'));
REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
}
-TEST_CASE("RemoveOne deletes one entry; sibling preserved",
- "[multigroup-codec]")
-{
+TEST_CASE("RemoveOne deletes one entry; sibling preserved", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> entries = {
@@ -136,8 +122,7 @@ TEST_CASE("RemoveOne deletes one entry; sibling preserved",
REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
}
-TEST_CASE("Empty multigroup builds to an empty root", "[multigroup-codec]")
-{
+TEST_CASE("Empty multigroup builds to an empty root", "[multigroup-codec]") {
nt::InMemoryBackend backend;
std::vector<nt::MultigroupCodec::RelationEntry> empty;
REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty());
diff --git a/test/NamespaceReferenceManager.cpp b/test/NamespaceReferenceManager.cpp
index 27f0dcd..90e65e5 100644
--- a/test/NamespaceReferenceManager.cpp
+++ b/test/NamespaceReferenceManager.cpp
@@ -21,74 +21,62 @@
namespace {
-using Path = std::vector<std::string>;
-
-// Pad a label into a deterministic 64-char hex string so the merkle codec can
-// decode it as a 32-byte payload.
-std::string mk_hash(char c, char d = '0')
-{
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
-}
-
-std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
-{
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = { OPEN, CLOSE };
- return t;
-}
-
-void register_branch(nt::ObjectManager& om,
- const std::string& name,
- const std::string& target_hash)
-{
- auto br = std::make_unique<nt::ObjectManager::Branch>();
- br->name = name;
- br->target_hash = target_hash;
- om.Register({"system", "branches", name}, std::move(br), make_type(BRANCH));
-}
-
-void register_session(nt::ObjectManager& om,
- const std::string& id,
- std::map<std::string, std::string> overrides = {})
-{
- auto s = std::make_unique<nt::ObjectManager::Session>();
- s->branch_overrides = std::move(overrides);
- om.Register({"system", "sessions", id}, std::move(s), make_type(SESSION));
-}
-
-// Builds a branch tree with a single (mg_name → mg_hash) entry. Returns the
-// branch-tree root hex.
-std::string build_branch_tree(nt::InMemoryBackend& store,
- const std::string& mg_name,
- const std::string& mg_hash_hex)
-{
- return nt::Merkle<std::string>::Insert(store, "", mg_name,
- nt::hex_to_bin(mg_hash_hex));
-}
-
-} // namespace
-
-TEST_CASE("Branch sub-path rewrites through the branch tree to the mg snapshot",
- "[resolve]")
-{
+ using Path = std::vector<std::string>;
+
+ // Pad a label into a deterministic 64-char hex string so the merkle codec can
+ // decode it as a 32-byte payload.
+ std::string mk_hash(char c, char d = '0') {
+ std::string h(64, '0');
+ h[0] = c;
+ h[1] = d;
+ return h;
+ }
+
+ std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = {OPEN, CLOSE};
+ return t;
+ }
+
+ void register_branch(nt::ObjectManager& om, const std::string& name,
+ const std::string& target_hash) {
+ auto br = std::make_unique<nt::ObjectManager::Branch>();
+ br->name = name;
+ br->target_hash = target_hash;
+ om.Register({"system", "branches", name}, std::move(br), make_type(BRANCH));
+ }
+
+ void register_session(nt::ObjectManager& om, const std::string& id,
+ std::map<std::string, std::string> overrides = {}) {
+ auto s = std::make_unique<nt::ObjectManager::Session>();
+ s->branch_overrides = std::move(overrides);
+ om.Register({"system", "sessions", id}, std::move(s), make_type(SESSION));
+ }
+
+ // Builds a branch tree with a single (mg_name → mg_hash) entry. Returns the
+ // branch-tree root hex.
+ std::string build_branch_tree(nt::InMemoryBackend& store, const std::string& mg_name,
+ const std::string& mg_hash_hex) {
+ return nt::Merkle<std::string>::Insert(store, "", mg_name, nt::hex_to_bin(mg_hash_hex));
+ }
+
+} // namespace
+
+TEST_CASE("Branch sub-path rewrites through the branch tree to the mg snapshot", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
const std::string mg_hash = mk_hash('a');
- const std::string tree = build_branch_tree(store, "warehouse", mg_hash);
+ const std::string tree = build_branch_tree(store, "warehouse", mg_hash);
register_branch(om, "main", tree);
nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "branches", "main", "multigroups", "warehouse", "relations", "items"})
- == Path{"system", "snapshots", mg_hash, "relations", "items"});
+ REQUIRE(nrm.Resolve({"system", "branches", "main", "multigroups", "warehouse", "relations",
+ "items"}) == Path{"system", "snapshots", mg_hash, "relations", "items"});
}
-TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]")
-{
+TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
@@ -99,19 +87,17 @@ TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]")
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Resolve on an unborn branch leaves the path unchanged", "[resolve]")
-{
+TEST_CASE("Resolve on an unborn branch leaves the path unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
- register_branch(om, "main", ""); // unborn
+ register_branch(om, "main", ""); // unborn
nt::NamespaceReferenceManager nrm(om, store);
const Path p{"system", "branches", "main", "multigroups", "warehouse", "relations", "items"};
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve]")
-{
+TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
@@ -122,8 +108,7 @@ TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]")
-{
+TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
nt::NamespaceReferenceManager nrm(om, store);
@@ -131,8 +116,7 @@ TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]")
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Non-branch paths pass through unchanged", "[resolve]")
-{
+TEST_CASE("Non-branch paths pass through unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
nt::NamespaceReferenceManager nrm(om, store);
@@ -140,62 +124,52 @@ TEST_CASE("Non-branch paths pass through unchanged", "[resolve]")
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Session override takes precedence over the global branch HEAD",
- "[resolve]")
-{
+TEST_CASE("Session override takes precedence over the global branch HEAD", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
- const std::string mg_global = mk_hash('a');
+ const std::string mg_global = mk_hash('a');
const std::string mg_session = mk_hash('b');
- const std::string tree_global =
- build_branch_tree(store, "warehouse", mg_global);
- const std::string tree_session =
- build_branch_tree(store, "warehouse", mg_session);
+ const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
+ const std::string tree_session = build_branch_tree(store, "warehouse", mg_session);
register_branch(om, "main", tree_global);
register_session(om, "sess123", {{"main", tree_session}});
nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main",
- "multigroups", "warehouse", "relations", "items"})
- == Path{"system", "snapshots", mg_session, "relations", "items"});
+ REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
+ "warehouse", "relations", "items"}) ==
+ Path{"system", "snapshots", mg_session, "relations", "items"});
}
-TEST_CASE("Session resolution falls back to global branch when override is absent",
- "[resolve]")
-{
+TEST_CASE("Session resolution falls back to global branch when override is absent", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
const std::string mg_global = mk_hash('a');
- const std::string tree_global =
- build_branch_tree(store, "warehouse", mg_global);
+ const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
register_branch(om, "main", tree_global);
- register_session(om, "sess123"); // no overrides
+ register_session(om, "sess123"); // no overrides
nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main",
- "multigroups", "warehouse", "relations", "items"})
- == Path{"system", "snapshots", mg_global, "relations", "items"});
+ REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
+ "warehouse", "relations", "items"}) ==
+ Path{"system", "snapshots", mg_global, "relations", "items"});
}
-TEST_CASE("Session resolution with neither override nor global branch is unchanged",
- "[resolve]")
-{
+TEST_CASE("Session resolution with neither override nor global branch is unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
register_session(om, "sess123");
nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "sessions", "sess123", "branches", "main",
+ const Path p{"system", "sessions", "sess123", "branches", "main",
"multigroups", "warehouse", "relations", "items"};
REQUIRE(nrm.Resolve(p) == p);
}
-TEST_CASE("Empty input path is returned unchanged", "[resolve]")
-{
+TEST_CASE("Empty input path is returned unchanged", "[resolve]") {
nt::ObjectManager om;
nt::InMemoryBackend store;
nt::NamespaceReferenceManager nrm(om, store);
diff --git a/test/ObjectManager.cpp b/test/ObjectManager.cpp
index e97bcb2..f90de3a 100644
--- a/test/ObjectManager.cpp
+++ b/test/ObjectManager.cpp
@@ -13,23 +13,19 @@
// share.
// ---------------------------------------------------------------------------
-namespace
-{
- std::unique_ptr<nt::ObjectManager::object_type> session_type()
- {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = SESSION;
+namespace {
+ std::unique_ptr<nt::ObjectManager::object_type> session_type() {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = SESSION;
t->disposable = true;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
return t;
}
-}
+} // namespace
-TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][object-manager]")
-{
+TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][object-manager]") {
nt::ObjectManager om;
- om.Register({"system", "sessions", "alpha"},
- std::make_unique<nt::ObjectManager::Session>(),
+ om.Register({"system", "sessions", "alpha"}, std::make_unique<nt::ObjectManager::Session>(),
session_type());
REQUIRE(om.Find({"system", "sessions", "alpha"}) != nullptr);
@@ -37,15 +33,13 @@ TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][obje
REQUIRE(om.Find({"system", "sessions", "alpha"}) == nullptr);
}
-TEST_CASE("Unregister returns false for missing paths", "[step5][object-manager]")
-{
+TEST_CASE("Unregister returns false for missing paths", "[step5][object-manager]") {
nt::ObjectManager om;
REQUIRE(om.Unregister({"never", "registered"}) == false);
}
TEST_CASE("Unregister splices a middle entry without disturbing neighbours",
- "[step5][object-manager]")
-{
+ "[step5][object-manager]") {
nt::ObjectManager om;
om.Register({"a"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
om.Register({"b"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
@@ -58,16 +52,15 @@ TEST_CASE("Unregister splices a middle entry without disturbing neighbours",
REQUIRE(om.Find({"c"}) != nullptr);
}
-TEST_CASE("Unregister at the head of the list works", "[step5][object-manager]")
-{
+TEST_CASE("Unregister at the head of the list works", "[step5][object-manager]") {
nt::ObjectManager om;
- om.Register({"first"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+ om.Register({"first"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
om.Register({"second"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
// The most recently registered entry is the list head (LIFO).
REQUIRE(om.Unregister({"second"}) == true);
REQUIRE(om.Find({"second"}) == nullptr);
- REQUIRE(om.Find({"first"}) != nullptr);
+ REQUIRE(om.Find({"first"}) != nullptr);
// And the new head is removable too.
REQUIRE(om.Unregister({"first"}) == true);
diff --git a/test/VM.cpp b/test/VM.cpp
index f4ce453..7e61da6 100644
--- a/test/VM.cpp
+++ b/test/VM.cpp
@@ -21,48 +21,45 @@
// ---------------------------------------------------------------------------
// Stores a tuple in the backend and updates the Relation object's Merkle root.
-static void store_tuple(nt::InMemoryBackend& backend,
- nt::ObjectManager& objects,
- const std::vector<std::string>& path,
- std::vector<nt::Attribute> attrs)
-{
+static void store_tuple(nt::InMemoryBackend& backend, nt::ObjectManager& objects,
+ const std::vector<std::string>& path, std::vector<nt::Attribute> attrs) {
auto bytes = nt::TupleCodec::Serialize(attrs);
- auto hash = backend.Put(std::move(bytes));
+ auto hash = backend.Put(std::move(bytes));
auto* entry = objects.Find(path);
- if (!entry) return;
+ if (!entry)
+ return;
auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(entry->object.get());
- if (!rel) return;
+ if (!rel)
+ return;
auto hash_bin = nt::hex_to_bin(hash);
- rel->merkle_root = nt::Merkle<nt::Hash32>::Insert(backend, rel->merkle_root,
- hash_bin, hash_bin);
+ rel->merkle_root =
+ nt::Merkle<nt::Hash32>::Insert(backend, rel->merkle_root, hash_bin, hash_bin);
}
-static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type()
-{
+static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type() {
auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = RELATION;
+ t->label = RELATION;
t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->methods = {OPEN, CLOSE};
return t;
}
// Generator for eq[A, B]: returns one tuple when A == B, nothing otherwise.
// Treats both args as integers when parseable, strings otherwise.
-static std::unique_ptr<nt::ObjectManager::ephemeral_object_type> make_eq_type()
-{
+static std::unique_ptr<nt::ObjectManager::ephemeral_object_type> make_eq_type() {
auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->disposable = false;
- t->methods = { OPEN, CLOSE };
+ t->label = EPHEMERAL_RELATION;
+ t->disposable = false;
+ t->methods = {OPEN, CLOSE};
t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::AlephZero;
- t->generator = [](const std::vector<std::string>& args,
- std::size_t offset,
- std::size_t limit) -> std::vector<nt::Tuple>
- {
- if (args.size() < 2 || offset > 0 || limit == 0) return {};
- if (args[0] != args[1]) return {};
- return { nt::Tuple({ { "left", args[0] }, { "right", args[1] } }) };
+ t->generator = [](const std::vector<std::string>& args, std::size_t offset,
+ std::size_t limit) -> std::vector<nt::Tuple> {
+ if (args.size() < 2 || offset > 0 || limit == 0)
+ return {};
+ if (args[0] != args[1])
+ return {};
+ return {nt::Tuple({{"left", args[0]}, {"right", args[1]}})};
};
return t;
}
@@ -71,16 +68,15 @@ static std::unique_ptr<nt::ObjectManager::ephemeral_object_type> make_eq_type()
// Fixture: shared manager stack
// ---------------------------------------------------------------------------
-struct Fixture
-{
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
+struct Fixture {
+ nt::InMemoryBackend backend;
+ nt::ObjectManager objects;
nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles { objects, backend };
- nt::NamespaceReferenceManager references { objects, backend };
- nt::HandlerManager handler { objects, permissions, identities, lifecycles, references };
- nt::CursorManager cursors { backend, &lifecycles, &objects };
+ nt::IdentityManager identities;
+ nt::LifecycleManager lifecycles{objects, backend};
+ nt::NamespaceReferenceManager references{objects, backend};
+ nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
+ nt::CursorManager cursors{backend, &lifecycles, &objects};
int conn = 1;
};
@@ -88,14 +84,13 @@ struct Fixture
// Tests
// ---------------------------------------------------------------------------
-TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]")
-{
+TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]") {
Fixture f;
- const std::vector<std::string> path = { "relations", "villager" };
+ const std::vector<std::string> path = {"relations", "villager"};
f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, { { "name", "John" }, { "age", "30" } });
- store_tuple(f.backend, f.objects, path, { { "name", "Peter" }, { "age", "25" } });
+ store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}});
+ store_tuple(f.backend, f.objects, path, {{"name", "Peter"}, {"age", "25"}});
auto* handle = f.handler.Open(path, &f.conn);
REQUIRE(handle != nullptr);
@@ -104,12 +99,13 @@ TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]")
REQUIRE(cursor != nullptr);
nt::PlanNode plan;
- plan.op = nt::PlanNode::Op::SCAN;
+ plan.op = nt::FOL_OPERATION_SCAN;
plan.scan_cursor = cursor;
nt::VM vm(f.cursors);
int count = 0;
- while (vm.Next(&plan)) ++count;
+ while (vm.Next(&plan))
+ ++count;
REQUIRE(count == 2);
@@ -117,25 +113,21 @@ TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]")
f.handler.Close(handle);
}
-TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]")
-{
+TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]") {
Fixture f;
- const std::vector<std::string> people_path = { "relations", "people" };
- const std::vector<std::string> eq_path = { "builtins", "eq" };
+ const std::vector<std::string> people_path = {"relations", "people"};
+ const std::vector<std::string> eq_path = {"builtins", "eq"};
// Register stored relation: people(name, age)
- f.objects.Register(people_path,
- std::make_unique<nt::ObjectManager::Relation>(),
+ f.objects.Register(people_path, std::make_unique<nt::ObjectManager::Relation>(),
make_relation_type());
- store_tuple(f.backend, f.objects, people_path, { { "name", "John" }, { "age", "30" } });
- store_tuple(f.backend, f.objects, people_path, { { "name", "Peter" }, { "age", "25" } });
- store_tuple(f.backend, f.objects, people_path, { { "name", "Jude" }, { "age", "25" } });
+ store_tuple(f.backend, f.objects, people_path, {{"name", "John"}, {"age", "30"}});
+ store_tuple(f.backend, f.objects, people_path, {{"name", "Peter"}, {"age", "25"}});
+ store_tuple(f.backend, f.objects, people_path, {{"name", "Jude"}, {"age", "25"}});
// Register ephemeral eq relation
- f.objects.Register(eq_path,
- std::make_unique<nt::ObjectManager::Relation>(),
- make_eq_type());
+ f.objects.Register(eq_path, std::make_unique<nt::ObjectManager::Relation>(), make_eq_type());
// Open handles and cursors
auto* people_handle = f.handler.Open(people_path, &f.conn);
@@ -143,8 +135,8 @@ TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]")
auto* eq_handle = f.handler.Open(eq_path, &f.conn);
REQUIRE(eq_handle != nullptr);
- auto* people_rel = dynamic_cast<nt::ObjectManager::Relation*>(
- f.objects.Find(people_path)->object.get());
+ auto* people_rel =
+ dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(people_path)->object.get());
auto* people_cursor = f.cursors.Open(people_handle, people_rel->merkle_root);
REQUIRE(people_cursor != nullptr);
auto* eq_cursor = f.cursors.Open(eq_handle); // EPHEMERAL: starts exhausted, JOIN resets
@@ -152,24 +144,23 @@ TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]")
// Plan: JOIN( SCAN(people), SCAN(eq[ Var("age"), Const("25") ]) )
nt::PlanNode right;
- right.op = nt::PlanNode::Op::SCAN;
+ right.op = nt::FOL_OPERATION_SCAN;
right.scan_cursor = eq_cursor;
- right.scan_args = { nt::PathArg::Var("age"), nt::PathArg::Const("25") };
+ right.scan_args = {nt::PathArg::Var("age"), nt::PathArg::Const("25")};
nt::PlanNode left;
- left.op = nt::PlanNode::Op::SCAN;
+ left.op = nt::FOL_OPERATION_SCAN;
left.scan_cursor = people_cursor;
nt::PlanNode root;
- root.op = nt::PlanNode::Op::JOIN;
- root.left = &left;
+ root.op = nt::FOL_OPERATION_JOIN;
+ root.left = &left;
root.right = &right;
// Execute and collect matched names
nt::VM vm(f.cursors);
std::vector<std::string> matched;
- while (nt::Tuple* t = vm.Next(&root))
- {
+ while (nt::Tuple* t = vm.Next(&root)) {
for (const auto& attr : t->attrs())
if (attr.name == "name")
matched.push_back(attr.value);
@@ -177,9 +168,9 @@ TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]")
// Jude (age=25) and Peter (age=25) should match; Jogn (age=30) should not
REQUIRE(matched.size() == 2);
- REQUIRE(std::find(matched.begin(), matched.end(), "Jude") != matched.end());
+ REQUIRE(std::find(matched.begin(), matched.end(), "Jude") != matched.end());
REQUIRE(std::find(matched.begin(), matched.end(), "Peter") != matched.end());
- REQUIRE(std::find(matched.begin(), matched.end(), "John") == matched.end());
+ REQUIRE(std::find(matched.begin(), matched.end(), "John") == matched.end());
f.cursors.Close(people_cursor);
f.cursors.Close(eq_cursor);
@@ -187,15 +178,14 @@ TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]")
f.handler.Close(eq_handle);
}
-TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]")
-{
+TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]") {
Fixture f;
- const std::vector<std::string> path = { "relations", "items" };
+ const std::vector<std::string> path = {"relations", "items"};
f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, { { "id", "1" } });
- store_tuple(f.backend, f.objects, path, { { "id", "2" } });
- store_tuple(f.backend, f.objects, path, { { "id", "3" } });
+ store_tuple(f.backend, f.objects, path, {{"id", "1"}});
+ store_tuple(f.backend, f.objects, path, {{"id", "2"}});
+ store_tuple(f.backend, f.objects, path, {{"id", "3"}});
auto* handle = f.handler.Open(path, &f.conn);
REQUIRE(handle != nullptr);
@@ -204,17 +194,18 @@ TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]")
REQUIRE(cursor != nullptr);
nt::PlanNode scan;
- scan.op = nt::PlanNode::Op::SCAN;
+ scan.op = nt::FOL_OPERATION_SCAN;
scan.scan_cursor = cursor;
nt::PlanNode take;
- take.op = nt::PlanNode::Op::TAKE;
- take.left = &scan;
+ take.op = nt::FOL_OPERATION_TAKE;
+ take.left = &scan;
take.take_limit = 2;
nt::VM vm(f.cursors);
int count = 0;
- while (vm.Next(&take)) ++count;
+ while (vm.Next(&take))
+ ++count;
REQUIRE(count == 2);
@@ -222,13 +213,12 @@ TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]")
f.handler.Close(handle);
}
-TEST_CASE("PROJECT filters tuple attributes by name", "[project]")
-{
+TEST_CASE("PROJECT filters tuple attributes by name", "[project]") {
Fixture f;
- const std::vector<std::string> path = { "relations", "people" };
+ const std::vector<std::string> path = {"relations", "people"};
f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, { { "name", "John" }, { "age", "30" }, { "city", "Lisbon" } });
+ store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}, {"city", "Lisbon"}});
auto* handle = f.handler.Open(path, &f.conn);
REQUIRE(handle != nullptr);
@@ -237,20 +227,21 @@ TEST_CASE("PROJECT filters tuple attributes by name", "[project]")
REQUIRE(cursor != nullptr);
nt::PlanNode scan;
- scan.op = nt::PlanNode::Op::SCAN;
+ scan.op = nt::FOL_OPERATION_SCAN;
scan.scan_cursor = cursor;
nt::PlanNode project;
- project.op = nt::PlanNode::Op::PROJECT;
- project.left = &scan;
- project.project_attrs = { "name", "city" };
+ project.op = nt::FOL_OPERATION_PROJECT;
+ project.left = &scan;
+ project.project_attrs = {"name", "city"};
nt::VM vm(f.cursors);
nt::Tuple* t = vm.Next(&project);
REQUIRE(t != nullptr);
std::vector<std::string> names;
- for (const auto& attr : t->attrs()) names.push_back(attr.name);
+ for (const auto& attr : t->attrs())
+ names.push_back(attr.name);
REQUIRE(names.size() == 2);
REQUIRE(std::find(names.begin(), names.end(), "name") != names.end());