diff options
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 Binary files differnew file mode 100644 index 0000000..3f1a558 --- /dev/null +++ b/images/logo-smooth.png 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 @@ -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()); |
