aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorEstevan Castilho <tevo@riajuu.net>2026-07-02 00:31:57 +0000
committerEstevan Castilho <tevo@riajuu.net>2026-07-02 00:31:57 +0000
commitab70e5461b98ddf2b56268c53c171cab89157027 (patch)
tree3e57218edacc8e1366e0d1a86bdb922af7e51dca /src
parenta4f6637601548a7158869ad155f29db50b151823 (diff)
downloadRNT-ab70e5461b98ddf2b56268c53c171cab89157027.tar.gz
RNT-ab70e5461b98ddf2b56268c53c171cab89157027.zip
Nuke the old code
Diffstat (limited to 'src')
-rw-r--r--src/CursorManager.cpp131
-rw-r--r--src/HandlerManager.cpp59
-rw-r--r--src/IdentityManager.cpp11
-rw-r--r--src/InMemoryBackend.cpp18
-rw-r--r--src/LifecycleManager.cpp143
-rw-r--r--src/Merkle.cpp552
-rw-r--r--src/MultigroupCodec.cpp71
-rw-r--r--src/NamespaceReferenceManager.cpp110
-rw-r--r--src/ObjectManager.cpp40
-rw-r--r--src/PermissionsManager.cpp17
-rw-r--r--src/RNT_C_API.cpp1057
-rw-r--r--src/RelationalNT.cpp113
-rw-r--r--src/SqliteBackend.cpp55
-rw-r--r--src/TupleCodec.cpp67
-rw-r--r--src/VM.cpp99
15 files changed, 0 insertions, 2543 deletions
diff --git a/src/CursorManager.cpp b/src/CursorManager.cpp
deleted file mode 100644
index 0d71b1c..0000000
--- a/src/CursorManager.cpp
+++ /dev/null
@@ -1,131 +0,0 @@
-#include "CursorManager.h"
-#include "Merkle.h"
-#include "ObjectManager.h"
-#include "TupleCodec.h"
-
-namespace nt {
- CursorManager::CursorManager(IStorageBackend& backend, LifecycleManager* lifecycles,
- ObjectManager* 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)
- return nullptr;
-
- const auto& p = handle->object->head->path;
- 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]});
- }
-
- 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) {
- auto bytes = backend_.Get(bin_to_hex(entry.payload));
- if (!bytes)
- continue;
- c->page.emplace_back(TupleCodec::Deserialize(*bytes));
- }
-
- c->fetch_offset += entries.size();
- if (entries.size() < PAGE_SIZE)
- c->exhausted = true;
- }
-
- CursorManager::cursor* CursorManager::Open(HandlerManager::handle* handle,
- 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;
- auto* snap = find_parent_snapshot(*objects_, handle);
- if (snap == nullptr)
- return;
- lifecycles_->Pin(snap);
- c->pinned_snapshot = snap;
- };
-
- 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
- // to dereference the handle again.
- auto* etype = static_cast<ObjectManager::ephemeral_object_type*>(
- handle->object->head->type.get());
-
- auto* c = new cursor();
- c->is_ephemeral = true;
- c->generator = etype->generator;
- c->exhausted = true;
- pin_snapshot(c);
- return c;
- }
-
- if (label != RELATION)
- return nullptr;
-
- auto* c = new cursor();
- c->merkle_root = merkle_root;
- pin_snapshot(c);
-
- if (merkle_root.empty()) {
- // Empty relation: valid but immediately exhausted.
- c->exhausted = true;
- return c;
- }
-
- LoadPage(c);
- return c;
- }
-
- 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->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;
- return &c->page[c->page_position++];
- }
-
- LoadPage(c);
- if (c->page.empty())
- return nullptr;
- return &c->page[c->page_position++];
- }
-
- 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
deleted file mode 100644
index 505b393..0000000
--- a/src/HandlerManager.cpp
+++ /dev/null
@@ -1,59 +0,0 @@
-#include "HandlerManager.h"
-
-#include "IdentityManager.h"
-#include "LifecycleManager.h"
-#include "NamespaceReferenceManager.h"
-#include "PermissionsManager.h"
-
-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) {
- }
-
- 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)) {
- // Starts monitoring this guy
- lifecycles_.Monitor(retrieved_object);
- 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) {
- if (handle == nullptr) {
- return false;
- }
- // Somehow grab the object pointer and connection (maybe drop it...) from the handle
- ObjectManager::registry* object = handle->object;
- 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)
- // 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 {
- // 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
deleted file mode 100644
index 9b131a9..0000000
--- a/src/IdentityManager.cpp
+++ /dev/null
@@ -1,11 +0,0 @@
-#include "IdentityManager.h"
-
-namespace nt {
- const bool IdentityManager::CanOpen(ObjectManager::registry* object) {
- return object != nullptr;
- }
-
- const bool IdentityManager::CanClose(ObjectManager::registry* object) {
- return object != nullptr;
- }
-} // namespace nt
diff --git a/src/InMemoryBackend.cpp b/src/InMemoryBackend.cpp
deleted file mode 100644
index 8aebc65..0000000
--- a/src/InMemoryBackend.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "InMemoryBackend.h"
-
-#include <picosha2.h>
-
-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) {
- auto it = kv_.find(hash);
- if (it == kv_.end())
- return std::nullopt;
- return it->second;
- }
-} // namespace nt
diff --git a/src/LifecycleManager.cpp b/src/LifecycleManager.cpp
deleted file mode 100644
index 4a12e09..0000000
--- a/src/LifecycleManager.cpp
+++ /dev/null
@@ -1,143 +0,0 @@
-#include "LifecycleManager.h"
-
-#include "Merkle.h"
-#include "Types.h"
-
-#include <vector>
-
-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;
- --object->head->handle_count;
- TryCollect(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;
- --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;
- }
-
- 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;
-
- // Named-lifetime types are managed explicitly and must not auto-collect:
- // BRANCH — named branches persist until a future explicit-delete API
- // tears them down. Their counters going to zero just means
- // nobody currently holds them open.
- // SESSION — rnt_session_close is the sole removal path.
- // Everything else (MULTIGROUP, BRANCH_TREE, RELATION,
- // 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 == 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;
- const auto& mg_path = multigroup->head->path;
- 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()) {
- const auto& p = cur->head->path;
- 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;
-
- children.push_back(cur);
- }
-
- for (auto* child : children)
- Unpin(child);
- }
-
- 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;
-
- // Page through the (mg_name, mg_hash) tree and collect each referenced
- // multigroup before unpinning. Decoupling iteration from mutation
- // keeps us safe against Unpin cascading further GC under our feet.
- 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) {
- static const Hash32 zero{};
- 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);
- }
- if (page.size() < kPageSize)
- break;
- offset += page.size();
- }
-
- for (auto* mg : mgs)
- Unpin(mg);
- }
-} // namespace nt
diff --git a/src/Merkle.cpp b/src/Merkle.cpp
deleted file mode 100644
index 09329f9..0000000
--- a/src/Merkle.cpp
+++ /dev/null
@@ -1,552 +0,0 @@
-#include "Merkle.h"
-
-#include <algorithm>
-#include <cassert>
-#include <cstring>
-#include <stdexcept>
-
-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];
- }
- 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;
- }
-
- 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;
- }
-
- 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;
- }
-
- 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;
- }
-
- 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;
- }
- 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;
- }
- 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);
- }
-
- 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});
-
- if (leaf.entries.size() <= B) {
- InsertResult r;
- r.new_hash = store_leaf(store, leaf);
- r.leaf_count = leaf.entries.size();
- 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()};
-
- 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);
- 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);
-
- 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_internal(store, node);
- r.leaf_count = total;
- return r;
- }
-
- // 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)) {
- auto node = decode_internal(bytes);
- if (node.entries.size() == 1)
- return bin_to_hex(node.entries[0].child_hash);
- }
- 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()};
- }
-
- // ── Internal node ───────────────────────────────────────────────────────
- auto node = decode_internal(bytes);
-
- 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);
- }
-
- 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;
- 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);
-
- for (const auto& entry : node.entries) {
- if (out.size() >= limit)
- break;
-
- if (offset >= entry.leaf_count) {
- offset -= entry.leaf_count;
- continue;
- }
-
- page_from(store, bin_to_hex(entry.child_hash), offset, limit, out);
- }
- }
-
- // ── Explicit instantiations ──────────────────────────────────────────────────
-
- template class Merkle<Hash32>;
- template class Merkle<std::string>;
-
-} // namespace nt
diff --git a/src/MultigroupCodec.cpp b/src/MultigroupCodec.cpp
deleted file mode 100644
index 0d0cf14..0000000
--- a/src/MultigroupCodec.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-#include "MultigroupCodec.h"
-
-#include "Merkle.h"
-
-#include <map>
-
-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{};
- return nt::hex_to_bin(hex);
- }
-
- static std::string decode_payload(const nt::Hash32& bin) {
- static const nt::Hash32 zero{};
- if (bin == zero)
- return "";
- return nt::bin_to_hex(bin);
- }
-
- 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));
- return root;
- }
-
- 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;
- for (auto& entry : page)
- 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) {
- return nt::Merkle<std::string>::Insert(store, root_hex, relation_name,
- encode_payload(relation_root_hex));
- }
-
- 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) {
- auto found = nt::Merkle<std::string>::Get(store, root_hex, relation_name);
- if (!found)
- return "";
- return decode_payload(*found);
- }
-} // namespace nt::MultigroupCodec
diff --git a/src/NamespaceReferenceManager.cpp b/src/NamespaceReferenceManager.cpp
deleted file mode 100644
index 0d99b73..0000000
--- a/src/NamespaceReferenceManager.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-#include "NamespaceReferenceManager.h"
-
-#include "Merkle.h"
-
-namespace nt {
- NamespaceReferenceManager::NamespaceReferenceManager(ObjectManager& objects,
- 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 "";
- auto found = Merkle<std::string>::Get(storage, branch_tree_root, mg_name);
- if (!found)
- return "";
- // All-zero payload = "no snapshot bound" sentinel matching MultigroupCodec.
- static const Hash32 zero{};
- if (*found == zero)
- return "";
- return bin_to_hex(*found);
- }
-
- std::vector<std::string>
- NamespaceReferenceManager::Resolve(std::vector<std::string> path) const {
- constexpr int kMaxReparseDepth = 30;
-
- 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]};
- 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) {
- auto it = session->branch_overrides.find(path[4]);
- if (it != session->branch_overrides.end())
- branch_tree_root = it->second;
- }
- }
-
- // 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]};
- 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;
- }
- }
-
- 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};
- for (size_t i = 7; i < path.size(); ++i)
- rewritten.push_back(path[i]);
-
- path = std::move(rewritten);
- continue;
- }
-
- // /system/branches/<name>/multigroups/<mg>/<sub>... →
- // /system/snapshots/<mg_hash>/<sub>...
- // 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]};
- auto* entry = objects_.Find(branch_path);
- if (!entry || !entry->object)
- 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;
-
- std::vector<std::string> rewritten{"system", "snapshots", mg_hash};
- for (size_t i = 5; i < path.size(); ++i)
- rewritten.push_back(path[i]);
-
- path = std::move(rewritten);
- continue;
- }
-
- return path;
- }
-
- // Cycle guard tripped — surface as a lookup miss.
- return {};
- }
-} // namespace nt
diff --git a/src/ObjectManager.cpp b/src/ObjectManager.cpp
deleted file mode 100644
index 70e622b..0000000
--- a/src/ObjectManager.cpp
+++ /dev/null
@@ -1,40 +0,0 @@
-#include "ObjectManager.h"
-
-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;
- head->path = std::move(path);
- head->type = std::move(type);
-
- auto entry = std::make_unique<registry>();
- entry->head = std::move(head);
- entry->object = std::move(object);
- entry->next = std::move(entries);
- entries = std::move(entry);
- }
-
- ObjectManager::registry* ObjectManager::Find(const std::vector<std::string> object_path) {
- registry* current = entries.get();
- while (current != nullptr) {
- if (current->head->path == object_path)
- return current;
- current = current->next.get();
- }
- return nullptr;
- }
-
- 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) {
- *slot = std::move((*slot)->next);
- return true;
- }
- slot = &(*slot)->next;
- }
- return false;
- }
-} // namespace nt
diff --git a/src/PermissionsManager.cpp b/src/PermissionsManager.cpp
deleted file mode 100644
index bcdfdd7..0000000
--- a/src/PermissionsManager.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#include "PermissionsManager.h"
-
-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?
- return object != nullptr;
- }
-} // namespace nt
diff --git a/src/RNT_C_API.cpp b/src/RNT_C_API.cpp
deleted file mode 100644
index abd683c..0000000
--- a/src/RNT_C_API.cpp
+++ /dev/null
@@ -1,1057 +0,0 @@
-#include "RNT_C_API.h"
-
-#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 "SqliteBackend.h"
-#include "TupleCodec.h"
-#include "VM.h"
-
-#include <algorithm>
-#include <cstring>
-#include <memory>
-#include <mutex>
-#include <random>
-#include <sstream>
-#include <stdexcept>
-#include <string>
-#include <vector>
-
-// ---------------------------------------------------------------------------
-// Global runtime state
-//
-// A single Runtime instance is created per process on the first successful
-// rnt_init call. All managers share this instance; they do not hold their
-// own state. Initialisation is protected by g_init_mutex so that concurrent
-// first-callers are safe; once g_init_done is true the mutex is never
-// contended again.
-// ---------------------------------------------------------------------------
-
-namespace {
- struct Runtime {
- std::unique_ptr<nt::IStorageBackend> storage;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- 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;
- };
-
- static std::unique_ptr<Runtime> g_rt;
-
- static std::vector<std::string> split_path(const char* path) {
- std::vector<std::string> parts;
- if (!path)
- return parts;
- std::string s(path);
- 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);
- return parts;
- }
-
- static std::vector<nt::Attribute> parse_kv(const char* kv) {
- std::vector<nt::Attribute> attrs;
- if (!kv)
- return attrs;
- std::stringstream ss(kv);
- std::string line;
- 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)});
- }
- return attrs;
- }
-
- static std::string tuple_to_kv(nt::Tuple* t) {
- std::string out;
- t->Reset();
- const nt::Attribute* a = t->Next();
- while (a) {
- out += a->name + "=" + a->value + "\n";
- a = t->Next();
- }
- return out;
- }
-
- 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;
- t->disposable = false;
- 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;
- t->disposable = false;
- 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;
- 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
- // -----------------------------------------------------------------------
-
- /**
- * Wraps an entire plan subtree. All PlanNode allocations and all
- * CursorManager::cursor* / HandlerManager::handle* opened by SCAN nodes
- * 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
- };
-
- 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;
- }
-
- /**
- * A VM cursor owns the materialised plan tree and all resources opened
- * 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);
- cursors = std::move(pw->cursors);
- handles = std::move(pw->handles);
- 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);
- }
- };
-
- // ---- 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
-//
-// Section order mirrors the NT open-handle pipeline:
-// Runtime lifecycle → Authentication → Handle lifecycle →
-// Object registration → Tuple storage → Cursor / VM plan builder
-// ---------------------------------------------------------------------------
-
-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->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);
- 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>();
- branch->name = "master";
- g_rt->objects.Register(master_path, std::move(branch), make_branch_type());
- }
-
- return 0;
- } catch (...) {
- g_rt.reset();
- 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 (!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 (!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 (!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 (!path)
- return nullptr;
- return g_rt->handler->Open(split_path(path), nullptr);
-}
-
-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 (!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;
-
- auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(h->object->object.get());
- 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 (!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;
-
- auto* branch = dynamic_cast<nt::ObjectManager::Branch*>(entry->object.get());
- 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;
-
- 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 (!path)
- return -1;
- const auto parts = split_path(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;
-
- // 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 char* target_hash) {
- if (!path)
- return -1;
- const auto parts = split_path(path);
- 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;
-
- 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;
-}
-
-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;
-
- 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 (!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 (!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 (!relation_path || !kv_attrs || !hash_out)
- 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);
-
- 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);
-
- if (commit_relation_update(*branch, mg_name, relation_name, new_root).empty())
- return -1;
-
- *hash_out = heap_str(tuple_hash);
- return 0;
-}
-
-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;
- 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 (!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 (!relation_path || !root_hash_out)
- 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;
-
- // 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 (!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 = dynamic_cast<nt::ObjectManager::Relation*>(h->object->object.get());
- 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 (!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;
- }
- *tuple_out = heap_str(tuple_to_kv(t));
- 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;
-}
-
-// ---------------------------------------------------------------------------
-// VM plan builder
-// ---------------------------------------------------------------------------
-
-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);
- }
-
- return nullptr;
-}
-
-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 (!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 (!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;
- }
- *tuple_out = heap_str(tuple_to_kv(t));
- 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
deleted file mode 100644
index f27179f..0000000
--- a/src/RelationalNT.cpp
+++ /dev/null
@@ -1,113 +0,0 @@
-#include "Runtime.h"
-
-#ifdef NT_CONSOLE_APP
-#include "CursorManager.h"
-#include "HandlerManager.h"
-#include "IdentityManager.h"
-#include "LifecycleManager.h"
-#include "Merkle.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "SqliteBackend.h"
-#include "TupleCodec.h"
-#include "VM.h"
-
-#include <iostream>
-#endif
-
-namespace nt {
- bool NT::IsRunning() const {
- return true;
- }
-
- void NT::SimulateEntryCall() {
- }
-} // namespace nt
-
-#ifdef NT_CONSOLE_APP
-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>();
- type->label = RELATION;
- type->disposable = false;
- type->methods = {OPEN, CLOSE};
-
- 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());
-
- // --- 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_bin = nt::hex_to_bin(hash);
- villager_rel->merkle_root =
- 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"}});
- nt::CursorManager cursors(backend);
-
- // --- Open a handle on villager through the full manager pipeline ---
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles(objects, backend);
- nt::NamespaceReferenceManager references(objects, backend);
- nt::HandlerManager handler(objects, permissions, identities, lifecycles, references);
-
- int connection = 1; // dummy connection context
- nt::HandlerManager::handle* handle = handler.Open(path, &connection);
- 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) {
- std::cout << "Failed to open cursor for villager\n";
- handler.Close(handle);
- return 1;
- }
-
- // --- Build a SCAN plan and pull all tuples lazily through the FOL VM ---
- nt::PlanNode plan;
- 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)) {
- std::cout << " ";
- const nt::Attribute* attr = t->Next();
- while (attr != nullptr) {
- std::cout << attr->name << "=" << attr->value;
- attr = t->Next();
- if (attr != nullptr)
- std::cout << ", ";
- }
- std::cout << '\n';
- }
-
- // --- Cleanup ---
- cursors.Close(cursor);
- handler.Close(handle);
-
-#ifdef NT_WAIT_ON_EXIT
- std::cout << "Press Enter to exit...";
- std::cin.get();
-#endif
-
- return runtime.IsRunning() ? 0 : 1;
-}
-#endif
diff --git a/src/SqliteBackend.cpp b/src/SqliteBackend.cpp
deleted file mode 100644
index 3e01058..0000000
--- a/src/SqliteBackend.cpp
+++ /dev/null
@@ -1,55 +0,0 @@
-#include "SqliteBackend.h"
-
-#include <picosha2.h>
-#include <stdexcept>
-
-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"
- ");";
-
- char* err = nullptr;
- if (sqlite3_exec(db_, schema, nullptr, nullptr, &err) != SQLITE_OK) {
- std::string msg(err);
- sqlite3_free(err);
- throw std::runtime_error(msg);
- }
- }
-
- SqliteBackend::~SqliteBackend() {
- sqlite3_close(db_);
- }
-
- 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_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_step(stmt);
- sqlite3_finalize(stmt);
- return 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_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_TRANSIENT);
-
- std::optional<std::vector<uint8_t>> result;
- 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);
- }
- sqlite3_finalize(stmt);
- return result;
- }
-} // namespace nt
diff --git a/src/TupleCodec.cpp b/src/TupleCodec.cpp
deleted file mode 100644
index a78ed5b..0000000
--- a/src/TupleCodec.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-#include "TupleCodec.h"
-
-#include <picosha2.h>
-
-#include <algorithm>
-#include <cstdint>
-#include <cstring>
-#include <numeric>
-
-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) {
- auto bytes = Serialize(attrs);
- return picosha2::hash256_hex_string(bytes.begin(), bytes.end());
- }
-
- 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;
- auto write_u32 = [&](uint32_t n) {
- out.push_back(static_cast<uint8_t>(n));
- out.push_back(static_cast<uint8_t>(n >> 8));
- out.push_back(static_cast<uint8_t>(n >> 16));
- out.push_back(static_cast<uint8_t>(n >> 24));
- };
-
- 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()));
- out.insert(out.end(), a.value.begin(), a.value.end());
- }
- return out;
- }
-
- 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);
- };
-
- std::vector<Attribute> result;
- size_t pos = 0;
- 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;
- std::string value(reinterpret_cast<const char*>(bytes.data() + pos), value_len);
- pos += value_len;
-
- result.push_back({std::move(name), std::move(value)});
- }
- return result;
- }
-} // namespace nt::TupleCodec
diff --git a/src/VM.cpp b/src/VM.cpp
deleted file mode 100644
index 5f66f83..0000000
--- a/src/VM.cpp
+++ /dev/null
@@ -1,99 +0,0 @@
-#include "VM.h"
-#include "Types.h"
-
-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> resolved;
- resolved.reserve(tmpl.size());
- 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;
- }
- resolved.push_back(std::move(val));
- }
- return resolved;
- }
-
- 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;
- }
-
- 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);
- 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 Operation::FOL_OPERATION_SCAN:
- return cursors_.Next(node->scan_cursor);
-
- case Operation::FOL_OPERATION_PROJECT: {
- Tuple* next = Next(node->left);
- if (next == nullptr)
- return nullptr;
-
- 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);
- }
-
- 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)
- return nullptr;
-
- 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);
-
- node->join_left = nullptr;
- }
- }
-
- 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