diff options
Diffstat (limited to '')
| -rw-r--r-- | src/RNT_C_API.cpp | 649 |
1 files changed, 563 insertions, 86 deletions
diff --git a/src/RNT_C_API.cpp b/src/RNT_C_API.cpp index 34bbacc..c92d03b 100644 --- a/src/RNT_C_API.cpp +++ b/src/RNT_C_API.cpp @@ -5,6 +5,8 @@ #include "IdentityManager.h" #include "LifecycleManager.h" #include "Merkle.h" +#include "MultigroupCodec.h" +#include "NamespaceReferenceManager.h" #include "ObjectManager.h" #include "PermissionsManager.h" #include "InMemoryBackend.h" @@ -12,9 +14,11 @@ #include "TupleCodec.h" #include "VM.h" +#include <algorithm> #include <cstring> #include <memory> #include <mutex> +#include <random> #include <sstream> #include <stdexcept> #include <string> @@ -38,16 +42,16 @@ namespace nt::ObjectManager objects; nt::PermissionsManager permissions; nt::IdentityManager identities; - nt::LifecycleManager lifecycles; + std::unique_ptr<nt::LifecycleManager> lifecycles; + std::unique_ptr<nt::NamespaceReferenceManager> references; std::unique_ptr<nt::HandlerManager> handler; std::unique_ptr<nt::CursorManager> cursors; }; static std::unique_ptr<Runtime> g_rt; - static std::mutex g_init_mutex; static bool g_init_done = false; - bool is_init() { return g_init_done; } + bool is_initialized() { return g_init_done; } static std::vector<std::string> split_path(const char* path) { @@ -121,6 +125,286 @@ namespace 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; + t->disposable = false; + 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; + t->disposable = false; + 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; + // Sessions live only as long as their connection; rnt_session_close + // removes them via ObjectManager::Unregister. + t->disposable = true; + 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. + t->exclusive = false; + return t; + } + + // Generates a random 256-bit hex hash for naming a session. Hashes are + // 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 const char hex[] = "0123456789abcdef"; + + std::string out; + out.reserve(64); + for (int i = 0; i < 4; ++i) + { + uint64_t v = rng(); + 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]); + v >>= 8; + } + } + return out; + } + + // Serializes the (name, merkle_root) pairs into the storage backend and + // registers the resulting multigroup snapshot under /system/snapshots/<hash>, + // with one Relation entry per child at /system/snapshots/<hash>/<name>. + // Idempotent: if the snapshot already exists in the registry the existing + // entries are reused. Returns the multigroup hash, or empty string on error. + // + // 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 {}; + + const std::string hash = nt::MultigroupCodec::Build(*g_rt->storage, relations); + 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; + + 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; + + auto rel = std::make_unique<nt::ObjectManager::Relation>(); + rel->merkle_root = root; + g_rt->objects.Register(rel_path, std::move(rel), make_relation_type()); + + // Structural pin from the parent Multigroup; released by + // LifecycleManager::CascadeMultigroup at GC time. + g_rt->lifecycles->Pin(g_rt->objects.Find(rel_path)); + } + return hash; + } + + // 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) + { + std::vector<std::pair<std::string, std::string>> 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; + for (auto& entry : page) + out.emplace_back(std::move(entry.key), + nt::bin_to_hex(entry.payload)); + if (page.size() < kPageSize) break; + offset += page.size(); + } + return out; + } + + // Registers /system/branch_trees/<hash> as a BRANCH_TREE if missing, and + // 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; + + 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; + const auto p = split_path(("/system/snapshots/" + mg_hash).c_str()); + g_rt->lifecycles->Pin(g_rt->objects.Find(p)); + } + } + + // Adjusts pins when a branch (or session override) moves its bound + // branch-tree root. The new tree is pinned before the old is unpinned so + // 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()) + { + register_branch_tree(new_tree_root); + 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()); + g_rt->lifecycles->Unpin(g_rt->objects.Find(p)); + } + } + + // 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 ""; + static const nt::Hash32 zero{}; + 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) + { + const std::string mg_hash = lookup_branch_mg(branch, mg_name); + 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) + { + auto* entry = g_rt->objects.Find(branch_path); + 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()); + } + + // Parses a path of the form + // /system/branches/<name>/multigroups/<mg>/relations/<rel>. + // Returns true on success; out-parameters are populated with the branch + // 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]; + relation_name_out = parts[6]; + return true; + } + + // Applies a per-relation root update with the full two-level cascade: + // 1. Read the (rel, root) list of `mg_name` from the branch's current + // tree (or empty when the mg is absent / branch is unborn). + // 2. Insert/replace (relation_name, new_root) in that list. + // 3. register_snapshot → new_mg_hash. + // 4. Insert (mg_name → new_mg_hash) into the branch tree → new branch + // tree root. + // 5. Advance branch.target_hash to the new root and rebind pins. + // Returns the new branch tree root on success; empty on error. + // + // 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) + { + 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); + + const std::string new_mg_hash = register_snapshot(relations); + 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( + *g_rt->storage, branch.target_hash, mg_name, mg_payload); + + const std::string old_tree_root = branch.target_hash; + branch.target_hash = new_tree_root; + rebind_branch_tree_pin(old_tree_root, new_tree_root); + return new_tree_root; + } + + // Returns the merkle_root of a named relation in the specified mg under + // the branch's current tree. Empty when the branch is unborn, the mg + // 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 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; }); + return (it == relations.end()) ? std::string{} : it->second; + } + // ----------------------------------------------------------------------- // Plan builder internals // ----------------------------------------------------------------------- @@ -187,9 +471,6 @@ namespace int rnt_init(const char* driver, const char* storage_path) { - std::lock_guard<std::mutex> lock(g_init_mutex); - if (g_init_done) return 0; - try { auto rt = std::make_unique<Runtime>(); @@ -201,19 +482,25 @@ int rnt_init(const char* driver, const char* storage_path) 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->handler = std::make_unique<nt::HandlerManager>( - rt->objects, rt->permissions, rt->identities, rt->lifecycles); - rt->cursors = std::make_unique<nt::CursorManager>(*rt->storage); + 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 main_path = split_path("/system/branches/master"); - if (g_rt->objects.Find(main_path) == nullptr) + 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>(); branch->name = "master"; - g_rt->objects.Register(main_path, std::move(branch), + g_rt->objects.Register(master_path, std::move(branch), make_branch_type()); } @@ -229,28 +516,107 @@ int rnt_init(const char* driver, const char* storage_path) int rnt_firewall(const char* /*auth_method*/, char** claims_out) { - if (!is_init() || !claims_out) return -1; + if (!is_initialized() || !claims_out) return -1; // 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; + + // 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) + { + hash = random_session_hash(); + const auto session_path = split_path(("/system/sessions/" + hash).c_str()); + if (g_rt->objects.Find(session_path) != nullptr) continue; + + auto session = std::make_unique<nt::ObjectManager::Session>(); + session->connection_context = connection_context; + g_rt->objects.Register(session_path, std::move(session), make_session_type()); + *session_hash_out = heap_str(hash); + return 0; + } + 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()); + + auto* entry = g_rt->objects.Find(session_path); + if (!entry || !entry->object) return -1; + auto* session = dynamic_cast<nt::ObjectManager::Session*>(entry->object.get()); + if (!session) return -1; + + // Release every pin this session was holding before the entry disappears, + // so the dependent multigroups become GC-eligible. + for (const auto& [name, hash] : session->branch_overrides) + rebind_branch_tree_pin(hash, ""); + session->branch_overrides.clear(); + + 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) + 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 || !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; + + 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 (hash.empty()) + { + session->branch_overrides.erase(branch_name); + rebind_branch_tree_pin(old_hash, ""); + return 0; + } + + // 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; + + 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_init() || !path) return nullptr; + if (!is_initialized() || !path) return nullptr; return g_rt->handler->Open(split_path(path), nullptr); } int rnt_close_handle(rnt_handle_t handle) { - if (!is_init() || !handle) return -1; + if (!is_initialized() || !handle) return -1; auto* h = static_cast<nt::HandlerManager::handle*>(handle); return g_rt->handler->Close(h) ? 0 : -1; } -int rnt_branch_payload(rnt_handle_t handle, uint8_t** payload_out, size_t* len_out) +int rnt_branch_target(rnt_handle_t handle, char** target_hash_out) { - if (!is_init() || !handle || !payload_out || !len_out) return -1; + if (!is_initialized() || !handle || !target_hash_out) return -1; auto* h = static_cast<nt::HandlerManager::handle*>(handle); if (!h->object || !h->object->head) return -1; @@ -259,127 +625,231 @@ int rnt_branch_payload(rnt_handle_t handle, uint8_t** payload_out, size_t* len_o auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(h->object->object.get()); if (!branch) return -1; - const auto& src = branch->payload; - auto* buf = new uint8_t[src.size()]; - std::memcpy(buf, src.data(), src.size()); - *payload_out = buf; - *len_out = src.size(); + *target_hash_out = heap_str(branch->target_hash); return 0; } -int rnt_branch_set_payload(rnt_handle_t handle, const uint8_t* payload, size_t len) +int rnt_branch_advance(const char* branch_path, const char* new_hash) { - if (!is_init() || !handle) return -1; + if (!is_initialized() || !branch_path || !new_hash) 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; + 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; - auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(h->object->object.get()); + auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(entry->object.get()); if (!branch) return -1; - branch->payload.assign(payload, payload + len); + 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; + + const std::string old_hash = branch->target_hash; + branch->target_hash = hash; + rebind_branch_tree_pin(old_hash, hash); return 0; } int rnt_register_relation(const char* path) { - if (!is_init() || !path) return -1; + if (!is_initialized() || !path) return -1; const auto parts = split_path(path); - if (g_rt->objects.Find(parts) != nullptr) return 0; - g_rt->objects.Register( - parts, - std::make_unique<nt::ObjectManager::Relation>(), - make_relation_type()); - return 0; + + std::vector<std::string> branch_path; + std::string mg_name, relation_name; + if (!split_branch_mg_relation(parts, branch_path, mg_name, relation_name)) + return -1; + + auto* branch = find_branch(branch_path); + 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; + + // Empty merkle_root signals a relation with no tuples. + return commit_relation_update(*branch, mg_name, relation_name, "").empty() + ? -1 : 0; } -int rnt_register_branch(const char* path, const uint8_t* payload, size_t payload_len) +int rnt_register_branch(const char* path, const char* target_hash) { - if (!is_init() || !path) return -1; + if (!is_initialized() || !path) return -1; const auto parts = split_path(path); if (g_rt->objects.Find(parts) != nullptr) return 0; - auto branch = std::make_unique<nt::ObjectManager::Branch>(); - branch->name = parts.empty() ? "" : parts.back(); - if (payload && payload_len > 0) - branch->payload.assign(payload, payload + payload_len); + 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; + + 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()); + rebind_branch_tree_pin("", hash); return 0; } -// Looks up the Relation object for a path; returns nullptr when not found or wrong type. -static nt::ObjectManager::Relation* find_relation(const std::vector<std::string>& parts) +int rnt_list_relations(const char* branch_mg_path, char** out) { - auto* entry = g_rt->objects.Find(parts); - if (!entry || !entry->object) return nullptr; - return dynamic_cast<nt::ObjectManager::Relation*>(entry->object.get()); + if (!is_initialized() || !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; + + 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) + result += name + "\t" + root + "\n"; + *out = heap_str(result); + return 0; +} + +int rnt_list_branch_multigroups(const char* branch_path, char** out) +{ + if (!is_initialized() || !branch_path || !out) return -1; + auto* branch = find_branch(split_path(branch_path)); + if (!branch) return -1; + const auto mgs = list_branch_tree(branch->target_hash); + std::string result; + for (const auto& [name, mg_hash] : mgs) + result += name + "\t" + mg_hash + "\n"; + *out = heap_str(result); + return 0; +} + +int rnt_list_snapshot_relations(const char* snapshot_hash, char** out) +{ + if (!is_initialized() || !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; + auto* mg = dynamic_cast<nt::ObjectManager::Multigroup*>(entry->object.get()); + 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) + result += name + "\t" + root + "\n"; + *out = heap_str(result); + return 0; } int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_out) { - if (!is_init() || !relation_path || !kv_attrs || !hash_out) return -1; + if (!is_initialized() || !relation_path || !kv_attrs || !hash_out) return -1; const auto parts = split_path(relation_path); - auto* rel = find_relation(parts); - if (!rel) return -1; + std::vector<std::string> branch_path; + std::string mg_name, relation_name; + if (!split_branch_mg_relation(parts, branch_path, mg_name, relation_name)) + return -1; + + auto* branch = find_branch(branch_path); + 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)); - const auto hash = g_rt->storage->Put(std::move(bytes)); + 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 std::string new_root = + nt::Merkle<nt::Hash32>::Insert(*g_rt->storage, old_root, + tuple_bin, tuple_bin); - rel->merkle_root = nt::Merkle::Insert(*g_rt->storage, rel->merkle_root, hash); + if (commit_relation_update(*branch, mg_name, relation_name, new_root).empty()) + return -1; - *hash_out = heap_str(hash); + *hash_out = heap_str(tuple_hash); return 0; } int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash) { - if (!is_init() || !relation_path || !tuple_hash) return -1; - auto* rel = find_relation(split_path(relation_path)); - if (!rel) return -1; - rel->merkle_root = nt::Merkle::Remove(*g_rt->storage, rel->merkle_root, tuple_hash); - return 0; + if (!is_initialized() || !relation_path || !tuple_hash) return -1; + const auto parts = split_path(relation_path); + + std::vector<std::string> branch_path; + std::string mg_name, relation_name; + if (!split_branch_mg_relation(parts, branch_path, mg_name, relation_name)) + return -1; + + auto* branch = find_branch(branch_path); + 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)); + + return commit_relation_update(*branch, mg_name, relation_name, new_root).empty() + ? -1 : 0; } int rnt_clear_relation(const char* relation_path) { - if (!is_init() || !relation_path) return -1; - auto* rel = find_relation(split_path(relation_path)); - if (!rel) return -1; - rel->merkle_root.clear(); - return 0; + if (!is_initialized() || !relation_path) return -1; + const auto parts = split_path(relation_path); + + std::vector<std::string> branch_path; + std::string mg_name, relation_name; + if (!split_branch_mg_relation(parts, branch_path, mg_name, relation_name)) + return -1; + + auto* branch = find_branch(branch_path); + if (!branch) return -1; + + 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_init() || !relation_path || !root_hash_out) return -1; - auto* rel = find_relation(split_path(relation_path)); - if (!rel) return -1; - *root_hash_out = heap_str(rel->merkle_root); - return 0; -} + if (!is_initialized() || !relation_path || !root_hash_out) return -1; + const auto parts = split_path(relation_path); -int rnt_set_relation_root(const char* relation_path, const char* root_hash) -{ - if (!is_init() || !relation_path) return -1; - auto* rel = find_relation(split_path(relation_path)); - if (!rel) return -1; - rel->merkle_root = root_hash ? root_hash : ""; + std::vector<std::string> branch_path; + std::string mg_name, relation_name; + if (!split_branch_mg_relation(parts, branch_path, mg_name, relation_name)) + return -1; + + auto* branch = find_branch(branch_path); + 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 + // this check an unborn branch would silently report every relation empty. + 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; + + *root_hash_out = heap_str(it->second); return 0; } rnt_cursor_t rnt_cursor_open(rnt_handle_t handle) { - if (!is_init() || !handle) return nullptr; + if (!is_initialized() || !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) { - auto* rel = find_relation(h->object->head->path); + auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(h->object->object.get()); if (rel) merkle_root = rel->merkle_root; } @@ -388,7 +858,7 @@ rnt_cursor_t rnt_cursor_open(rnt_handle_t handle) int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out) { - if (!is_init() || !cursor || !tuple_out) return -1; + if (!is_initialized() || !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; } @@ -398,7 +868,7 @@ int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out) int rnt_cursor_close(rnt_cursor_t cursor) { - if (!is_init() || !cursor) return -1; + if (!is_initialized() || !cursor) return -1; g_rt->cursors->Close(static_cast<nt::CursorManager::cursor*>(cursor)); return 0; } @@ -412,17 +882,24 @@ void rnt_free_bytes(uint8_t* p) { delete[] p; } rnt_plan_t rnt_plan_scan(const char* relation_path) { - if (!is_init() || !relation_path) return nullptr; + 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; - // Seed the cursor with the current Merkle root from the ObjectManager. + // 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; - auto* rel = find_relation(parts); - if (rel) merkle_root = rel->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) @@ -507,14 +984,14 @@ void rnt_plan_free(rnt_plan_t plan) rnt_cursor_t rnt_vm_execute_plan(rnt_plan_t plan) { - if (!is_init() || !plan) return nullptr; + if (!is_initialized() || !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_init() || !vm_cursor || !tuple_out) return -1; + if (!is_initialized() || !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; } @@ -524,7 +1001,7 @@ int rnt_vm_cursor_next(rnt_cursor_t vm_cursor, char** tuple_out) int rnt_vm_cursor_close(rnt_cursor_t vm_cursor) { - if (!is_init() || !vm_cursor) return -1; + if (!is_initialized() || !vm_cursor) return -1; // VmCursor destructor closes all cursors and handles. delete static_cast<VmCursor*>(vm_cursor); return 0; |
