aboutsummaryrefslogtreecommitdiffstats
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
parenta4f6637601548a7158869ad155f29db50b151823 (diff)
downloadRNT-ab70e5461b98ddf2b56268c53c171cab89157027.tar.gz
RNT-ab70e5461b98ddf2b56268c53c171cab89157027.zip
Nuke the old code
Diffstat (limited to '')
-rw-r--r--.clang-format15
-rw-r--r--.gitignore38
-rw-r--r--CMakeLists.txt73
-rw-r--r--CMakePresets.json37
-rw-r--r--Doxyfile28
-rw-r--r--README.org33
-rw-r--r--flake.lock44
-rw-r--r--flake.nix195
-rw-r--r--include/Api.h16
-rw-r--r--include/CursorManager.h153
-rw-r--r--include/HandlerManager.h96
-rw-r--r--include/IStorageBackend.h49
-rw-r--r--include/IdentityManager.h47
-rw-r--r--include/InMemoryBackend.h31
-rw-r--r--include/LifecycleManager.h162
-rw-r--r--include/Merkle.h185
-rw-r--r--include/MultigroupCodec.h82
-rw-r--r--include/NT.h17
-rw-r--r--include/NamespaceReferenceManager.h107
-rw-r--r--include/ObjectManager.h335
-rw-r--r--include/PermissionsManager.h88
-rw-r--r--include/RNT_C_API.h501
-rw-r--r--include/Runtime.h23
-rw-r--r--include/SqliteBackend.h41
-rw-r--r--include/TupleCodec.h51
-rw-r--r--include/Types.h159
-rw-r--r--include/VM.h190
-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
-rw-r--r--test/C_API.cpp415
-rw-r--r--test/CursorManager.cpp152
-rw-r--r--test/LifecycleManager.cpp169
-rw-r--r--test/MultigroupCodec.cpp130
-rw-r--r--test/NamespaceReferenceManager.cpp177
-rw-r--r--test/ObjectManager.cpp68
-rw-r--r--test/VM.cpp254
49 files changed, 2 insertions, 6702 deletions
diff --git a/.clang-format b/.clang-format
deleted file mode 100644
index 5325526..0000000
--- a/.clang-format
+++ /dev/null
@@ -1,15 +0,0 @@
----
-Language: Cpp
-BasedOnStyle: LLVM
-IndentWidth: 4
-TabWidth: 4
-UseTab: Never
-ColumnLimit: 100
-BreakBeforeBraces: Attach
-AllowShortFunctionsOnASingleLine: None
-AllowShortIfStatementsOnASingleLine: false
-AllowShortLoopsOnASingleLine: false
-PointerAlignment: Left
-NamespaceIndentation: All
-SortIncludes: true
-...
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index c46cee2..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,38 +0,0 @@
-.vs
-build
-_build
-out
-x64
-Debug
-Release
-Win32
-[Bb]in
-[Oo]bj
-*.aps
-*.db
-*.ipch
-*.iobj
-*.ipdb
-*.ilk
-*.lastbuildstate
-*.log
-*.obj
-*.opendb
-*.pdb
-*.recipe
-*.sdf
-*.suo
-*.tlog
-*.user
-*.vcxproj.user
-RNT
-RNT.exe
-RNT.dll
-RNT.exp
-RNT.lib
-libRNT.so
-libRNT.dylib
-docs/generated/
-.cache
-_build_nix
-.direnv \ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
deleted file mode 100644
index 3c418d3..0000000
--- a/CMakeLists.txt
+++ /dev/null
@@ -1,73 +0,0 @@
-cmake_minimum_required (VERSION 3.16)
-
-project ("RelationalNT")
-
-set(CMAKE_CXX_STANDARD 20)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-
-option(RNT_UNITY_BUILD "Enable unity (jumbo) builds for faster compilation" ON)
-
-find_package(unofficial-sqlite3 CONFIG REQUIRED)
-find_path(PICOSHA2_INCLUDE_DIRS "picosha2.h")
-find_package(Catch2 3 REQUIRED)
-
-add_library(rnt_core OBJECT
- "src/ObjectManager.cpp"
- "src/PermissionsManager.cpp"
- "src/IdentityManager.cpp"
- "src/LifecycleManager.cpp"
- "src/HandlerManager.cpp"
- "src/CursorManager.cpp"
- "src/Merkle.cpp"
- "src/TupleCodec.cpp"
- "src/MultigroupCodec.cpp"
- "src/InMemoryBackend.cpp"
- "src/NamespaceReferenceManager.cpp"
- "src/VM.cpp"
-)
-target_include_directories(rnt_core PUBLIC "include" ${PICOSHA2_INCLUDE_DIRS})
-set_target_properties(rnt_core PROPERTIES
- POSITION_INDEPENDENT_CODE ON
- UNITY_BUILD ${RNT_UNITY_BUILD}
-)
-
-add_executable (RelationalNT
- "src/RelationalNT.cpp"
- "src/SqliteBackend.cpp"
-)
-target_link_libraries(RelationalNT PRIVATE rnt_core unofficial::sqlite3::sqlite3)
-target_compile_definitions(RelationalNT PRIVATE NT_CONSOLE_APP)
-set_target_properties(RelationalNT PROPERTIES UNITY_BUILD ${RNT_UNITY_BUILD})
-
-enable_testing()
-
-add_executable(RNT_tests
- "test/VM.cpp"
- "test/MultigroupCodec.cpp"
- "test/ObjectManager.cpp"
- "test/NamespaceReferenceManager.cpp"
- "test/LifecycleManager.cpp"
- "test/CursorManager.cpp"
-)
-target_link_libraries(RNT_tests PRIVATE rnt_core Catch2::Catch2WithMain)
-target_compile_definitions(RNT_tests PRIVATE NT_CONSOLE_APP)
-
-set_target_properties(RNT_tests PROPERTIES UNITY_BUILD OFF)
-
-include(Catch)
-catch_discover_tests(RNT_tests DISCOVERY_MODE PRE_TEST)
-
-add_library(RNT SHARED
- "src/RNT_C_API.cpp"
- "src/SqliteBackend.cpp"
-)
-target_link_libraries(RNT PRIVATE rnt_core unofficial::sqlite3::sqlite3)
-target_include_directories(RNT PUBLIC "include")
-target_compile_definitions(RNT PRIVATE NT_EXPORTS)
-set_target_properties(RNT PROPERTIES UNITY_BUILD ${RNT_UNITY_BUILD})
-
-add_executable(RNT_capi_tests
- "test/C_API.cpp"
-)
-target_link_libraries(RNT_capi_tests PRIVATE RNT Catch2::Catch2WithMain)
-catch_discover_tests(RNT_capi_tests DISCOVERY_MODE PRE_TEST)
diff --git a/CMakePresets.json b/CMakePresets.json
deleted file mode 100644
index 529b602..0000000
--- a/CMakePresets.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "version": 3,
- "configurePresets": [
- {
- "name": "gnu-base",
- "hidden": true,
- "generator": "Ninja",
- "binaryDir": "${sourceDir}/out/build/${presetName}",
- "installDir": "${sourceDir}/out/install/${presetName}",
- "cacheVariables": {
- "CMAKE_C_COMPILER": "gcc",
- "CMAKE_CXX_COMPILER": "g++"
- },
- "condition": {
- "type": "equals",
- "lhs": "${hostSystemName}",
- "rhs": "Linux"
- }
- },
- {
- "name": "gnu-debug",
- "displayName": "GNU Debug",
- "inherits": "gnu-base",
- "cacheVariables": {
- "CMAKE_BUILD_TYPE": "Debug"
- }
- },
- {
- "name": "gnu-release",
- "displayName": "GNU Release",
- "inherits": "gnu-base",
- "cacheVariables": {
- "CMAKE_BUILD_TYPE": "Release"
- }
- }
- ]
-}
diff --git a/Doxyfile b/Doxyfile
deleted file mode 100644
index c29388d..0000000
--- a/Doxyfile
+++ /dev/null
@@ -1,28 +0,0 @@
-PROJECT_NAME = "Relational NT"
-PROJECT_BRIEF = "A relational database kernel shaped around NT-style object management."
-OUTPUT_DIRECTORY = docs/generated
-INPUT = docs/mainpage.dox include src
-RECURSIVE = YES
-FILE_PATTERNS = *.h *.cpp *.dox
-EXTRACT_ALL = NO
-EXTRACT_PRIVATE = NO
-EXTRACT_STATIC = NO
-HIDE_UNDOC_MEMBERS = NO
-HIDE_UNDOC_CLASSES = NO
-WARNINGS = YES
-WARN_IF_UNDOCUMENTED = YES
-WARN_IF_DOC_ERROR = YES
-WARN_NO_PARAMDOC = NO
-WARN_AS_ERROR = NO
-GENERATE_HTML = YES
-HTML_OUTPUT = html
-GENERATE_LATEX = NO
-GENERATE_XML = NO
-USE_MDFILE_AS_MAINPAGE =
-JAVADOC_AUTOBRIEF = YES
-QT_AUTOBRIEF = NO
-QUIET = NO
-FULL_PATH_NAMES = NO
-STRIP_FROM_PATH = .
-EXCLUDE_PATTERNS = */docs/generated/*
-IMAGE_PATH = images
diff --git a/README.org b/README.org
index 7cefd5d..d9e05bd 100644
--- a/README.org
+++ b/README.org
@@ -31,42 +31,13 @@ The NT kernel is a good model for a dedicated database kernel for three reasons.
* Current Status
-The open-handle pipeline is wired end-to-end and cursors pull tuples lazily from the storage backend one page at a time through the Tarski FOL engine. The active backend (=SqliteBackend=) keeps a single =kv(hash, value)= table for content-addressed tuple data. Relation membership is tracked by a sorted Merkle B-tree (=Merkle=) whose nodes are stored in the same KV table; only the root hash per relation is held in memory. =InMemoryBackend= provides the same interface for tests.
-
-An =extern "C"= API (=RNT_C_API.h=) exposes the full manager pipeline to OCaml via ctypes, enabling Sakura to use this kernel as its storage and query execution layer.
-
-Several manager features are still being designed: dual ref counting, type callbacks, trie-based object lookup, and the namespace reparse layer. These are specified in header comments.
+Rewrite in progress, to be elaborated.
* Building
** GNU/Linux with Nix
-The supported build path is the Nix development shell on GNU/Linux. It provides
-GCC, CMake, Ninja, SQLite, PicoSHA2 headers, Catch2, Doxygen, Graphviz, and GDB.
-
-#+begin_src shell
-nix develop
-cmake --preset gnu-debug
-cmake --build out/build/gnu-debug
-ctest --test-dir out/build/gnu-debug --output-on-failure
-#+end_src
-
-A release build uses =--preset gnu-release=. CI builds the package with =nix build=,
-which also runs the test suite.
-
-* Documentation
-
-API documentation is generated with Doxygen. The root =Doxyfile= reads the public headers, source files, and the documentation main page in =docs/mainpage.dox=.
-
-#+begin_src shell
-nix develop --command doxygen Doxyfile
-#+end_src
-
-Generated HTML is written to =docs/generated/html=.
-
-The comments in the public headers are part of the design record. Keep architectural notes close to the declarations they explain so Doxygen can publish them with the API reference.
-
-The repository also includes a GitHub Actions workflow that publishes the generated Doxygen HTML to GitHub Pages on pushes to =master=. In the repository settings, configure Pages to use =GitHub Actions= as its source.
+To be elaborated.
* License
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 6307e01..0000000
--- a/flake.lock
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "nodes": {
- "nixpkgs": {
- "locked": {
- "lastModified": 1777954456,
- "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixos-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "picosha2": {
- "flake": false,
- "locked": {
- "lastModified": 1746328024,
- "narHash": "sha256-3psCzbrwR+vO9TyTKOx+gEaWuHDx6pSgLOQ3DqrJsnI=",
- "owner": "okdshin",
- "repo": "PicoSHA2",
- "rev": "161cb3fc4170fa7a3eca9e582cebd27cc4d1fe29",
- "type": "github"
- },
- "original": {
- "owner": "okdshin",
- "repo": "PicoSHA2",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "nixpkgs": "nixpkgs",
- "picosha2": "picosha2"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index a9ba21a..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,195 +0,0 @@
-{
- description = "RelationalNT CMake build";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
- picosha2 = {
- url = "github:okdshin/PicoSHA2";
- flake = false;
- };
- };
-
- outputs = { self, nixpkgs, picosha2 }:
- let
- systems = [
- "x86_64-linux"
- "aarch64-linux"
- "x86_64-darwin"
- "aarch64-darwin"
- ];
-
- forAllSystems = nixpkgs.lib.genAttrs systems;
-
- # Shared per-system derivations used by both the package and the dev shell.
- mkDeps = system:
- let
- pkgs = import nixpkgs { inherit system; };
-
- # Maps the unofficial::sqlite3::sqlite3 CMake
- # target to the system SQLite3 package. Required because CMakeLists.txt
- # calls target_link_libraries(... unofficial::sqlite3::sqlite3).
- unofficialSqlite3Config = pkgs.writeTextDir
- "share/unofficial-sqlite3/unofficial-sqlite3-config.cmake"
- ''
- include(CMakeFindDependencyMacro)
- find_dependency(SQLite3 REQUIRED)
-
- if(NOT TARGET unofficial::sqlite3::sqlite3)
- add_library(unofficial::sqlite3::sqlite3 INTERFACE IMPORTED)
- target_link_libraries(unofficial::sqlite3::sqlite3 INTERFACE SQLite::SQLite3)
- endif()
- '';
-
- # PicoSHA2 is a single-header library; extract just the header.
- picosha2Headers = pkgs.runCommand "picosha2-headers" { } ''
- mkdir -p $out/include
- install -m644 ${picosha2}/picosha2.h $out/include/picosha2.h
- '';
-
- stdenv = if pkgs.stdenv.isDarwin then pkgs.stdenv else pkgs.gccStdenv;
- cc = if pkgs.stdenv.isDarwin then pkgs.stdenv.cc else pkgs.gcc;
-
- in { inherit pkgs unofficialSqlite3Config picosha2Headers stdenv cc; };
-
- in
- {
- packages = forAllSystems (system:
- let
- inherit (mkDeps system) pkgs unofficialSqlite3Config picosha2Headers stdenv;
- in
- {
- default = stdenv.mkDerivation {
- pname = "rnt";
- version = "0.1.0";
-
- src = self;
-
- nativeBuildInputs = [
- pkgs.cmake
- pkgs.ninja
- ];
-
- buildInputs = [
- pkgs.catch2_3
- picosha2Headers
- pkgs.sqlite
- unofficialSqlite3Config
- ];
-
- cmakeFlags = [
- "-DCMAKE_BUILD_TYPE=Release"
- ];
-
- doCheck = true;
- checkPhase = ''
- runHook preCheck
- ctest --output-on-failure
- runHook postCheck
- '';
-
- installPhase = ''
- runHook preInstall
- mkdir -p "$out/bin" "$out/lib" "$out/include" "$out/share/licenses/rnt"
- install -m755 RelationalNT "$out/bin/RelationalNT"
- install -m755 RNT_tests "$out/bin/RNT_tests"
- for lib in libRNT.so libRNT.dylib; do
- if [ -f "$lib" ]; then
- install -m755 "$lib" "$out/lib/$lib"
- fi
- done
- install -m644 ../include/RNT_C_API.h "$out/include/RNT_C_API.h"
- install -m644 "${self}/LICENSE" "$out/share/licenses/rnt/LICENSE"
- runHook postInstall
- '';
-
- meta = {
- description = "RNT database kernel inspired by the Windows NT object manager";
- license = pkgs.lib.licenses.agpl3Only;
- mainProgram = "RelationalNT";
- maintainers = [
- { name = "Marcos Magueta"; }
- ];
- };
- };
- });
-
- devShells = forAllSystems (system:
- let
- inherit (mkDeps system) pkgs unofficialSqlite3Config picosha2Headers stdenv cc;
- in
- {
- default = pkgs.mkShell.override { inherit stdenv; } {
- packages = [
- pkgs.catch2_3
- pkgs.clang-tools
- pkgs.cmake
- pkgs.doxygen
- pkgs.graphviz
- pkgs.ninja
- picosha2Headers
- pkgs.sqlite
- unofficialSqlite3Config
- ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
- pkgs.gcc
- pkgs.gdb
- ];
-
- CC = "${cc}/bin/cc";
- CXX = "${cc}/bin/c++";
-
- CMAKE_PREFIX_PATH = pkgs.lib.makeSearchPathOutput "dev" "" [
- picosha2Headers
- pkgs.sqlite
- unofficialSqlite3Config
- ];
-
- CMAKE_INCLUDE_PATH = pkgs.lib.makeSearchPathOutput "dev" "include" [
- picosha2Headers
- pkgs.sqlite
- ];
-
- CMAKE_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
- pkgs.sqlite
- ];
-
- CPATH = pkgs.lib.makeSearchPathOutput "dev" "include" [
- picosha2Headers
- pkgs.sqlite
- ];
-
- C_INCLUDE_PATH = pkgs.lib.makeSearchPathOutput "dev" "include" [
- pkgs.sqlite
- ];
-
- CPLUS_INCLUDE_PATH = pkgs.lib.makeSearchPathOutput "dev" "include" [
- picosha2Headers
- pkgs.sqlite
- ];
-
- LIBRARY_PATH = pkgs.lib.makeLibraryPath [
- pkgs.sqlite
- ];
-
- DYLD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
- pkgs.sqlite
- ];
-
- PKG_CONFIG_PATH = pkgs.lib.makeSearchPathOutput "dev" "lib/pkgconfig" [
- pkgs.sqlite
- ];
-
- shellHook = ''
- export NIX_CFLAGS_COMPILE="-I${picosha2Headers}/include -I${pkgs.sqlite.dev}/include $NIX_CFLAGS_COMPILE"
- export NIX_LDFLAGS="-L${pkgs.sqlite.out}/lib $NIX_LDFLAGS"
- '';
- };
- });
-
- apps = forAllSystems (system: {
- default = {
- type = "app";
- program = "${self.packages.${system}.default}/bin/RelationalNT";
- };
- });
- };
-}
diff --git a/include/Api.h b/include/Api.h
deleted file mode 100644
index 33a7f6d..0000000
--- a/include/Api.h
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-/**
- * @file Api.h
- * @brief Defines symbol visibility for the Relational NT public API.
- */
-
-#if defined(_WIN32) && defined(NT_CONSOLE_APP)
-#define NT_API
-#elif defined(_WIN32) && defined(NT_EXPORTS)
-#define NT_API __declspec(dllexport)
-#elif defined(_WIN32)
-#define NT_API __declspec(dllimport)
-#else
-#define NT_API
-#endif
diff --git a/include/CursorManager.h b/include/CursorManager.h
deleted file mode 100644
index a4c82d7..0000000
--- a/include/CursorManager.h
+++ /dev/null
@@ -1,153 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "HandlerManager.h"
-#include "IStorageBackend.h"
-#include "LifecycleManager.h"
-#include "ObjectManager.h"
-#include "Types.h"
-
-#include <cstddef>
-#include <string>
-#include <vector>
-
-/**
- * @file CursorManager.h
- * @brief Declares paging and cursor orchestration for query execution.
- */
-
-namespace nt {
- /**
- * @class CursorManager
- * @brief Orchestrates paginated data retrieval from a storage backend.
- *
- * CursorManager depends only on IStorageBackend. The concrete backend
- * (SqliteBackend, InMemoryBackend, …) is injected at construction time.
- *
- * Tuples are never loaded all at once. Open() fetches the first page;
- * each subsequent Next() call transparently fetches the next page from
- * the backend once the current page is exhausted.
- *
- * Pagination is driven by the Merkle B-tree: each cursor carries the hex
- * hash of the relation's Merkle root at open time, and LoadPage calls
- * Merkle::Page(backend, merkle_root, fetch_offset, PAGE_SIZE) to retrieve
- * the next page of tuple hashes without loading sibling subtrees.
- */
- class CursorManager {
- public:
- static constexpr std::size_t PAGE_SIZE = 1;
-
- /**
- * @brief Constructs a CursorManager.
- *
- * The @p lifecycles and @p objects references are optional: when
- * left null the cursor performs no snapshot pinning (this matches the
- * test fixtures, which register relations at synthetic paths with no
- * /system/snapshots ancestor). When both are provided, Open finds the
- * resolved snapshot for the handle's relation and Pins it for the
- * cursor's lifetime, releasing the pin in Close.
- */
- explicit CursorManager(IStorageBackend& backend, LifecycleManager* lifecycles = nullptr,
- ObjectManager* objects = nullptr);
-
- /**
- * @brief Active scan state for a single relation.
- *
- * Holds one page of deserialized tuples at a time. When page_position
- * reaches the end of the page, Next() discards the page and fetches
- * the next one from the backend.
- *
- * The pointer returned by Next() is valid only until the next Next()
- * or Close() call — a page replacement invalidates all prior pointers.
- *
- * merkle_root is seeded from ObjectManager::Relation::merkle_root at
- * open time and does not change during iteration; the cursor reads the
- * snapshot captured when it was opened.
- *
- * Notably the cursor does NOT keep a pointer to the HandlerManager
- * handle it was opened from. Everything Next() needs — the relation
- * type, the merkle_root, and the ephemeral generator function — is
- * captured at Open time, so closing the handle before the cursor is
- * exhausted is safe (the API contract still says to close the cursor
- * first, but a violation no longer dereferences a freed handle).
- */
- struct cursor {
- /**
- * Hex hash of the relation's Merkle B-tree root node.
- * Empty string means the relation contains no tuples.
- */
- std::string merkle_root;
- /**
- * Bound argument values for EPHEMERAL_RELATION cursors.
- * Written by the JOIN operator before each probe; ignored for
- * stored RELATION cursors.
- */
- std::vector<std::string> args;
- std::vector<Tuple> page;
- std::size_t page_position = 0;
- std::size_t fetch_offset = 0; // next Merkle::Page() call starts here
- bool exhausted = false;
- /**
- * True for EPHEMERAL_RELATION cursors; selects the generator-driven
- * page fetch path instead of Merkle B-tree paging.
- */
- bool is_ephemeral = false;
- /**
- * Generator function for ephemeral cursors. Copied from the
- * ephemeral_object_type at Open time, so the cursor remains valid
- * even if the source registry entry is later torn down (the
- * snapshot pin held in pinned_snapshot prevents that today, but
- * the copy makes the cursor independent regardless).
- */
- ObjectManager::ephemeral_object_type::Generator generator;
- /**
- * Borrowed pointer to the snapshot the cursor is pinning.
- * Non-null only when a /system/snapshots/<H>/<n>
- * ancestor was found at Open time and LifecycleManager was
- * available. Close unpins it.
- */
- ObjectManager::registry* pinned_snapshot = nullptr;
- };
-
- /**
- * @brief Opens a cursor on the relation referenced by the handle.
- *
- * Fetches the first page immediately for non-empty relations. Returns
- * a valid exhausted cursor when @p merkle_root is empty (empty relation).
- * Returns nullptr when the handle does not refer to a RELATION or
- * EPHEMERAL_RELATION.
- *
- * @param handle Open RELATION or EPHEMERAL_RELATION handle.
- * @param merkle_root Hex hash of the relation's Merkle root at open
- * time. Ignored for EPHEMERAL_RELATION handles.
- */
- cursor* Open(HandlerManager::handle* handle, const std::string& merkle_root = "");
-
- /**
- * @brief Pulls the next tuple from the cursor.
- *
- * Fetches the next page from the backend transparently when the
- * current page is exhausted. Returns nullptr when all pages are done.
- */
- Tuple* Next(cursor* cursor);
-
- /**
- * @brief Closes the cursor and releases its resources.
- * @param cursor Cursor to close.
- */
- void Close(cursor* cursor);
-
- private:
- IStorageBackend& backend_;
- LifecycleManager* lifecycles_ = nullptr;
- ObjectManager* objects_ = nullptr;
-
- /**
- * Fetches a page of hashes via Merkle::Page, resolves each from the
- * KV store, and deserializes them into cursor->page. Clears the
- * existing page. Advances fetch_offset by the number of hashes
- * returned; sets exhausted when fewer than PAGE_SIZE hashes come back.
- */
- void LoadPage(cursor* c);
- };
-} // namespace nt
diff --git a/include/HandlerManager.h b/include/HandlerManager.h
deleted file mode 100644
index b2beb5f..0000000
--- a/include/HandlerManager.h
+++ /dev/null
@@ -1,96 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "ObjectManager.h"
-#include "Types.h"
-
-#include <set>
-#include <string>
-#include <vector>
-
-/**
- * @file HandlerManager.h
- * @brief Declares handle allocation and release for authorized object access.
- */
-
-namespace nt {
- class IdentityManager;
- class LifecycleManager;
- class NamespaceReferenceManager;
- class PermissionsManager;
-
- /** @brief Opens and closes authorized handles to registry objects. */
- class HandlerManager {
- public:
- /**
- * @brief Constructs a HandlerManager with shared runtime dependencies.
- *
- * All managers are injected so that state (registry entries, lifecycle
- * counters, permission policy) persists across calls rather than being
- * rebuilt per open/close.
- */
- HandlerManager(ObjectManager& objects, PermissionsManager& permissions,
- IdentityManager& identities, LifecycleManager& lifecycles,
- NamespaceReferenceManager& references);
-
- /**
- * @brief Represents authorized access to an object in the registry.
- *
- * The access mask is evaluated once during Open and cached here for the
- * lifetime of the handle. Subsequent operations check `granted_access`
- * rather than re-evaluating the security descriptor on every call. This
- * resolves the performance question in PermissionsManager about when to
- * check permissions.
- *
- * Because storage is append-only, a WRITE claim does not mean mutating an
- * existing tuple in-place. It means the session is authorized to commit a
- * new snapshot version of the relation. Two sessions holding WRITE handles
- * on the same relation always produce independent snapshots — there is no
- * data-level conflict.
- *
- * @todo Populate `granted_access` inside HandlerManager::Open from
- * PermissionsManager::Firewall / Access. See docs/reactos-ob-comparison.md §4.
- */
- struct handle {
- /** Object referenced by this handle. */
- ObjectManager::registry* object = nullptr;
- /** Connection metadata associated with the authorized access. */
- void* connection_context = nullptr;
- /**
- * Access claims granted to this handle at open time.
- * Cached here so that per-operation checks read from the handle
- * rather than re-evaluating the security descriptor.
- * @todo Populate in HandlerManager::Open.
- */
- std::set<AUTH_CLAIM> granted_access;
- };
-
- /**
- * @brief Releases a handle allocation.
- * @param handle Handle to release.
- */
- void DeallocateHandle(struct handle* handle);
-
- /**
- * @brief Opens an object by path for a connection.
- * @param object_path Logical object path.
- * @param connection_context Connection metadata for the caller.
- * @return A valid handle pointer on success, or nullptr on failure.
- */
- struct handle* Open(std::vector<std::string> object_path, void* connection_context);
-
- /**
- * @brief Closes a previously opened handle.
- * @param handle Handle to close.
- * @return True when the handle was closed successfully.
- */
- bool Close(struct handle* handle);
-
- private:
- ObjectManager& objects_;
- PermissionsManager& permissions_;
- IdentityManager& identities_;
- LifecycleManager& lifecycles_;
- NamespaceReferenceManager& references_;
- };
-} // namespace nt
diff --git a/include/IStorageBackend.h b/include/IStorageBackend.h
deleted file mode 100644
index a7cc782..0000000
--- a/include/IStorageBackend.h
+++ /dev/null
@@ -1,49 +0,0 @@
-#pragma once
-
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <vector>
-
-/**
- * @file IStorageBackend.h
- * @brief Abstract KV storage backend consumed by CursorManager and Merkle.
- *
- * Storage is strictly content-addressed: every value is stored under its
- * SHA256 hex digest. Relation membership (which tuple hashes belong to a
- * given relation) is now tracked via the Merkle B-tree maintained by the
- * Merkle class, not by this interface. Only Put and Get are required.
- */
-
-namespace nt {
- /**
- * @interface IStorageBackend
- * @brief Contract between CursorManager / Merkle and a physical storage engine.
- *
- * Two primitives are sufficient:
- * - Put: store bytes, get back the content-address (SHA256 hex).
- * - Get: retrieve bytes by content-address.
- *
- * Both are idempotent. The Merkle class stores node blobs via Put/Get and
- * maintains insertion-order-independent relation membership on top of these
- * two primitives.
- */
- class IStorageBackend {
- public:
- virtual ~IStorageBackend() = default;
-
- /**
- * @brief Stores a serialized value and returns its SHA256 hex hash.
- *
- * Idempotent: storing the same bytes twice does not create a duplicate.
- * The returned hash is the only valid key for subsequent Get calls.
- */
- virtual std::string Put(std::vector<uint8_t> value) = 0;
-
- /**
- * @brief Retrieves bytes by their SHA256 hex hash.
- * @return The stored bytes, or std::nullopt when the hash is unknown.
- */
- virtual std::optional<std::vector<uint8_t>> Get(const std::string& hash) = 0;
- };
-} // namespace nt
diff --git a/include/IdentityManager.h b/include/IdentityManager.h
deleted file mode 100644
index 1dd75da..0000000
--- a/include/IdentityManager.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "ObjectManager.h"
-
-/**
- * @file IdentityManager.h
- * @brief Declares object capability checks.
- */
-
-namespace nt {
- /**
- * @brief Checks whether an object supports identity-level operations.
- *
- * @todo Migrate to type callbacks. Once `object_type` holds `OpenProcedure`
- * and `CloseProcedure` function pointers, CanOpen / CanClose become
- * thin dispatchers that invoke the corresponding procedure — or this
- * manager is absorbed into HandlerManager directly. The function pointer
- * should be accompanied by a `const char* name` label on `object_type`
- * so that error logs identify the type without dereferencing the pointer.
- * See docs/reactos-ob-comparison.md §2.
- */
- class IdentityManager {
- public:
- /**
- * @brief Returns true when the object's type supports being opened.
- *
- * Currently checks that the object is non-null (stub). Should invoke
- * `object_type::OpenProcedure` once callbacks are defined.
- *
- * @param object Object to inspect.
- * @return True when the object can be opened.
- */
- const bool CanOpen(ObjectManager::registry* object);
-
- /**
- * @brief Returns true when the object's type supports being closed.
- *
- * Currently checks that the object is non-null (stub). Should invoke
- * `object_type::CloseProcedure` once callbacks are defined.
- *
- * @param object Object to inspect.
- * @return True when the object can be closed.
- */
- const bool CanClose(ObjectManager::registry* object);
- };
-} // namespace nt
diff --git a/include/InMemoryBackend.h b/include/InMemoryBackend.h
deleted file mode 100644
index b66d355..0000000
--- a/include/InMemoryBackend.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "IStorageBackend.h"
-
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-/**
- * @file InMemoryBackend.h
- * @brief In-process KV storage backend backed by an unordered_map.
- *
- * Intended for unit tests where SQLite is not appropriate. Data does not
- * survive process exit and is not thread-safe.
- *
- * Relation membership is tracked by the Merkle class, not this backend.
- */
-
-namespace nt {
- class InMemoryBackend : public IStorageBackend {
- public:
- std::string Put(std::vector<uint8_t> value) override;
- std::optional<std::vector<uint8_t>> Get(const std::string& hash) override;
-
- private:
- std::unordered_map<std::string, std::vector<uint8_t>> kv_;
- };
-} // namespace nt
diff --git a/include/LifecycleManager.h b/include/LifecycleManager.h
deleted file mode 100644
index c1e63d8..0000000
--- a/include/LifecycleManager.h
+++ /dev/null
@@ -1,162 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "IStorageBackend.h"
-#include "ObjectManager.h"
-
-/**
- * @file LifecycleManager.h
- * @brief Declares monitoring, pinning, cleanup, and contention operations.
- */
-
-namespace nt {
- /**
- * @brief Manages runtime lifecycle concerns for registry objects.
- *
- * Object lifetime is governed by two independent counters in registry_head:
- * `handle_count` (sessions holding an open handle) and `reference_count`
- * (structural dependencies such as cursors pinned to a snapshot or
- * ephemeral relations referencing their base relations). An object is
- * eligible for GC only when both reach zero.
- *
- * Monitor / Unmonitor manage `handle_count`. Pin / Unpin manage
- * `reference_count`. See docs/reactos-ob-comparison.md §1.
- */
- class LifecycleManager {
- public:
- /**
- * @brief Constructs a LifecycleManager bound to a registry and store.
- *
- * Needs the registry to perform GC: Unmonitor and Unpin call
- * ObjectManager::Unregister once both counters reach zero. Type-specific
- * cleanup (e.g. cascading a Multigroup's pin to its child Relations,
- * or a BranchTree's pin to its child Multigroups) runs inside the
- * lifecycle manager before the entry is spliced out. The storage
- * backend is needed by CascadeBranchTree to page the merkle nodes.
- */
- LifecycleManager(ObjectManager& objects, IStorageBackend& storage);
-
- /**
- * @brief Starts monitoring an object's lifecycle.
- *
- * Increments `handle_count` on the object's registry_head. Should be
- * called after all authorization checks pass inside HandlerManager::Open.
- *
- * @param object Object to monitor.
- */
- void Monitor(ObjectManager::registry* object);
-
- /**
- * @brief Stops monitoring an object.
- *
- * Decrements `handle_count`. When both `handle_count` and
- * `reference_count` reach zero, the object becomes eligible for GC.
- *
- * @todo Gate GC on both counters reaching zero, not just handle_count.
- * For deferred compaction, enqueue onto a background GC list rather
- * than freeing inline. See docs/reactos-ob-comparison.md §1 and §7.
- *
- * @param object Object to stop monitoring.
- */
- void Unmonitor(ObjectManager::registry* object);
-
- /**
- * @brief Increments the structural dependency count on an object.
- *
- * Call when one object takes a dependency on another — for example when
- * a cursor pins an immutable snapshot version, or when an ephemeral
- * relation is registered against its base relations. Prevents GC of the
- * target object while the dependency is live.
- *
- * @todo Implement. See docs/reactos-ob-comparison.md §1.
- *
- * @param object Object being depended upon.
- */
- void Pin(ObjectManager::registry* object);
-
- /**
- * @brief Decrements the structural dependency count on an object.
- *
- * The inverse of Pin. When both `reference_count` and `handle_count`
- * reach zero the object becomes eligible for GC.
- *
- * @todo Implement. See docs/reactos-ob-comparison.md §1.
- *
- * @param object Object whose dependency is being released.
- */
- void Unpin(ObjectManager::registry* object);
-
- /**
- * @brief Returns true when the object is GC-eligible right now.
- *
- * Eligibility is the joint condition handle_count == 0 &&
- * reference_count == 0. Decrement callers (Unmonitor / Unpin) consult
- * this and, when true, run type-specific cleanup before unregistering.
- */
- bool IsEligibleForGC(ObjectManager::registry* object) const;
-
- /**
- * @brief Serializes contention for changes to mutable reference states.
- *
- * Immutable objects — relation snapshots, multigroup snapshots,
- * transactions — never raise contention. Two sessions opening the same
- * immutable snapshot simultaneously is always safe; they will produce
- * independent results without conflict.
- *
- * Contention applies only to mutable reference states: the HEAD of a
- * branch and namespace entries targeted by atomic multi-reference updates.
- * These are the sole mutable pointers in the system. When a session holds
- * a write handle on a HEAD, a second write opener must block or fail.
- *
- * @todo Implement: read `object_type::exclusive`. Return false immediately
- * for non-exclusive objects. For exclusive objects, return false
- * (contention detected) when `handle_count > 0` for a write-mode
- * opener. See docs/reactos-ob-comparison.md §6.
- *
- * @param object Object to check for contention.
- * @return True when the operation may proceed.
- */
- const bool Contention(ObjectManager::registry* object);
-
- private:
- /**
- * @brief Joint-counters check + type-specific cascade + Unregister.
- *
- * Called by Unmonitor and Unpin after their decrement. No-op when the
- * object is still referenced. When eligible:
- * - MULTIGROUP entries first unpin every Relation registered as a
- * direct child of their /system/snapshots/H subtree.
- * - BRANCH_TREE entries first unpin every Multigroup their
- * content-addressed tree references.
- * - The entry is then removed from the registry; its IObject and
- * registry_head are freed.
- *
- * Inline GC for now; the reaper-queue deferred-deletion pattern from
- * docs/reactos-ob-comparison.md §7 is a later optimisation.
- */
- void TryCollect(ObjectManager::registry* object);
-
- /**
- * @brief Releases pins held by a MULTIGROUP entry on its child Relations.
- *
- * Iterates the registry once, collects every /system/snapshots/<H>/<n>
- * direct child, and calls Unpin on each. Triggered exclusively from
- * TryCollect at the moment the parent snapshot becomes eligible for GC.
- */
- void CascadeMultigroup(ObjectManager::registry* multigroup);
-
- /**
- * @brief Releases pins held by a BRANCH_TREE entry on its child mgs.
- *
- * Pages the Merkle<std::string> tree at the BranchTree's merkle_root,
- * resolves each (mg_name, mg_hash) entry to its
- * /system/snapshots/<mg_hash> Multigroup, and calls Unpin on it.
- * Triggered exclusively from TryCollect at the moment the BranchTree
- * itself becomes eligible for GC.
- */
- void CascadeBranchTree(ObjectManager::registry* branch_tree);
-
- ObjectManager& objects_;
- IStorageBackend& storage_;
- };
-} // namespace nt
diff --git a/include/Merkle.h b/include/Merkle.h
deleted file mode 100644
index 6256ddc..0000000
--- a/include/Merkle.h
+++ /dev/null
@@ -1,185 +0,0 @@
-#pragma once
-
-#include "IStorageBackend.h"
-
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <vector>
-
-/**
- * @file Merkle.h
- * @brief Generic sorted content-addressed B-tree over a templated key type.
- *
- * Maps `Key → Hash32 payload` with `Key` totally ordered. Each tree node is
- * stored as an immutable blob in IStorageBackend via Put/Get. All operations
- * load only the nodes on the path from root to the target leaf — O(log_B(n))
- * nodes in memory at once, never the whole tree.
- *
- * Instantiated three times in RNT:
- *
- * | Level | Key | Payload (Hash32) |
- * |---------------|----------------|-------------------------|
- * | Branch tree | `std::string` | multigroup snapshot hash|
- * | Multigroup | `std::string` | relation merkle root |
- * | Relation | `nt::Hash32` | tuple hash (key=payload)|
- *
- * **Branching factor B = 64**. At 64 children per node and a billion leaves the
- * tree is at most 5 levels deep (~20 KB peak working set).
- *
- * **Immutability**: nodes are never overwritten once written (content-addressed).
- * Insert / Remove produce new nodes only along the modified path; old nodes
- * remain valid for any snapshot that still holds their root hash.
- *
- * **Leaf node** stores a sorted-by-key list of `(Key, Hash32 payload)` pairs,
- * length ≤ B. For the relation level the key and payload happen to carry the
- * same value (the tuple hash); callers pass it twice. Storing both keeps the
- * tree shape uniform across the three levels.
- *
- * **Internal node entry** layout (`leaf_count | min_key | child_hash`):
- * - `leaf_count` enables O(log_B(n) + limit) offset navigation for Page()
- * without loading sibling subtrees.
- * - `min_key` is the smallest key in the subtree — the routing key.
- * - `child_hash` is the content-addressed hash of the child node blob.
- *
- * **Invariants** (asserted in debug builds):
- * - No leaf or internal node is ever empty.
- * - Leaf entries are sorted ascending by key.
- * - Internal entries are sorted ascending by min_key.
- *
- * **Wire format**: tagged 1-byte node kind ('L' or 'I'), then big-endian u32
- * count, then count repetitions of either a leaf entry or a child entry.
- * Key encoding is per-specialisation: `Hash32` is 32 raw bytes; `std::string`
- * is `u32_be length | bytes`. Wire format is content-addressed — any change
- * here invalidates every existing stored snapshot.
- */
-
-namespace nt {
-
- /** @brief 32-byte hash, the universal payload type for all merkle levels. */
- using Hash32 = std::array<uint8_t, 32>;
-
- /** @brief Decode a 64-character lowercase-hex SHA256 string into raw bytes.
- * Throws std::invalid_argument when the string is not exactly 64 hex chars. */
- Hash32 hex_to_bin(const std::string& hex);
-
- /** @brief Encode raw 32 bytes back into a 64-character lowercase-hex string. */
- std::string bin_to_hex(const Hash32& bin);
-
- template <typename Key> class Merkle {
- public:
- static constexpr size_t B = 64;
-
- /**
- * @brief Insert or overwrite a `(key, payload)` mapping.
- * @param store KV backend for node serialisation.
- * @param root_hex Current root hash (empty string = empty tree).
- * @param key Routing/identity key.
- * @param payload Hash this key maps to (child root one level down, or
- * the key itself at the relation level).
- * @return New root hash. Equal to root_hex when an insertion of an
- * identical (key, payload) pair would be a no-op.
- */
- static std::string Insert(IStorageBackend& store, const std::string& root_hex,
- const Key& key, const Hash32& payload);
-
- /**
- * @brief Remove a key from the tree.
- * @return New root hash. Empty string when the tree becomes empty.
- * Equal to root_hex when the key is absent.
- */
- static std::string Remove(IStorageBackend& store, const std::string& root_hex,
- const Key& key);
-
- /**
- * @brief Look up the payload for a key.
- * @return The payload when present, std::nullopt when absent.
- */
- static std::optional<Hash32> Get(IStorageBackend& store, const std::string& root_hex,
- const Key& key);
-
- /** @brief One (key, payload) pair returned by Page(). */
- struct Entry {
- Key key;
- Hash32 payload;
- };
-
- /**
- * @brief Page through entries in key-sorted order.
- *
- * Descends to the leaf at logical position @p offset using cumulative
- * leaf_counts stored in internal nodes, then walks forward collecting at
- * most @p limit entries. Only the nodes on the active path are loaded;
- * sibling subtrees are never touched.
- */
- static std::vector<Entry> Page(IStorageBackend& store, const std::string& root_hex,
- size_t offset, size_t limit);
-
- private:
- struct LeafEntry {
- Key key;
- Hash32 payload;
- };
-
- struct LeafNode {
- std::vector<LeafEntry> entries;
- };
-
- struct ChildEntry {
- uint64_t leaf_count;
- Key min_key;
- Hash32 child_hash;
- };
-
- struct InternalNode {
- std::vector<ChildEntry> entries;
- };
-
- // Wire format
- static std::vector<uint8_t> encode_leaf(const LeafNode& n);
- static std::vector<uint8_t> encode_internal(const InternalNode& n);
- static LeafNode decode_leaf(const std::vector<uint8_t>& bytes);
- static InternalNode decode_internal(const std::vector<uint8_t>& bytes);
- static bool is_leaf_bytes(const std::vector<uint8_t>& bytes);
-
- // Storage helpers
- static std::vector<uint8_t> load_node(IStorageBackend& store, const std::string& hash_hex);
- static std::string store_leaf(IStorageBackend& store, const LeafNode& n);
- static std::string store_internal(IStorageBackend& store, const InternalNode& n);
-
- /** @brief Returns the smallest key in the subtree rooted at @p node_hex. */
- static Key subtree_min_key(IStorageBackend& store, const std::string& node_hex);
-
- /** @brief Routes a key to the rightmost child whose min_key ≤ target. */
- static size_t route(const InternalNode& node, const Key& target);
-
- struct InsertResult {
- std::string new_hash;
- uint64_t leaf_count = 0;
- bool did_split = false;
- uint64_t split_leaf_count = 0;
- Key split_min_key{};
- std::string split_hash;
- };
-
- static InsertResult insert_into(IStorageBackend& store, const std::string& node_hex,
- const Key& key, const Hash32& payload);
-
- struct RemoveResult {
- std::string new_hash;
- uint64_t leaf_count = 0;
- };
-
- static RemoveResult remove_from(IStorageBackend& store, const std::string& node_hex,
- const Key& key);
-
- static void page_from(IStorageBackend& store, const std::string& node_hex, size_t& offset,
- size_t limit, std::vector<Entry>& out);
-
- static std::optional<Hash32> get_from(IStorageBackend& store, const std::string& node_hex,
- const Key& key);
- };
-
-} // namespace nt
diff --git a/include/MultigroupCodec.h b/include/MultigroupCodec.h
deleted file mode 100644
index 9d003ea..0000000
--- a/include/MultigroupCodec.h
+++ /dev/null
@@ -1,82 +0,0 @@
-#pragma once
-
-#include "IStorageBackend.h"
-
-#include <string>
-#include <utility>
-#include <vector>
-
-/**
- * @file MultigroupCodec.h
- * @brief Thin facade over Merkle<std::string> for multigroup snapshots.
- *
- * A multigroup snapshot is a `Merkle<std::string>` B-tree mapping each
- * relation name to that relation's merkle root. The snapshot identifier is
- * the hex hash of the tree's root node — the same path-localised content
- * addressing used at the tuple level inside each relation.
- *
- * Stored relations and ephemeral relations are represented uniformly: only
- * the relation's name and its current merkle_root participate in the tree,
- * regardless of how that root was derived.
- *
- * The codec used to be a flat `SHA256(sorted [(name, root)])` over a single
- * blob. That collapsed all relations into one rehash on any mutation. With
- * the merkle B-tree, a single relation update touches `O(log_B(n_rels))`
- * nodes on the way to the new root — sibling subtrees stay byte-identical.
- *
- * `RelationEntry` is `(relation_name, relation_root_hex)`. Both forms are
- * hex strings on the wire; the codec hex-decodes internally before handing
- * payloads to `Merkle<std::string>`.
- */
-
-namespace nt::MultigroupCodec {
- /** @brief One entry in a multigroup snapshot: (relation_name, root_hex). */
- using RelationEntry = std::pair<std::string, std::string>;
-
- /**
- * @brief Build a multigroup tree from a list of (name, root) entries.
- *
- * Each `Insert` advances the root through `O(log_B)` nodes. The final
- * root hash is the snapshot identifier. The list need not be pre-sorted —
- * the merkle B-tree imposes order intrinsically.
- *
- * @param store KV backend; receives all node blobs.
- * @param relations Entries to load. Duplicates collapse to the last value.
- * @return Root node hash (empty string when `relations` is empty).
- */
- std::string Build(IStorageBackend& store, const std::vector<RelationEntry>& relations);
-
- /**
- * @brief Enumerate all (name, root_hex) pairs at a multigroup root.
- *
- * Pages the entire tree (offset 0, no limit). Returned entries are in
- * key-sorted order. Empty input root yields an empty list.
- */
- std::vector<RelationEntry> List(IStorageBackend& store, const std::string& root_hex);
-
- /**
- * @brief Insert or overwrite one (relation_name → root_hex) entry.
- *
- * Equivalent to `Merkle<std::string>::Insert` after hex-decoding the
- * payload. The caller threads the returned root hash through subsequent
- * mutations. Pass an empty `root_hex` for the prior-state argument to
- * start from an empty tree.
- */
- std::string InsertOne(IStorageBackend& store, const std::string& root_hex,
- const std::string& relation_name, const std::string& relation_root_hex);
-
- /**
- * @brief Remove one relation entry from the multigroup tree.
- *
- * Returns the new root hash; empty when the tree becomes empty.
- */
- std::string RemoveOne(IStorageBackend& store, const std::string& root_hex,
- const std::string& relation_name);
-
- /**
- * @brief Look up one relation's root hex by name.
- * @return Hex root, or empty string when the name is absent.
- */
- std::string Lookup(IStorageBackend& store, const std::string& root_hex,
- const std::string& relation_name);
-} // namespace nt::MultigroupCodec
diff --git a/include/NT.h b/include/NT.h
deleted file mode 100644
index 041c517..0000000
--- a/include/NT.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-/**
- * @file NT.h
- * @brief Includes for the Relational NT public API.
- */
-
-#include "CursorManager.h"
-#include "HandlerManager.h"
-#include "IdentityManager.h"
-#include "LifecycleManager.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "Runtime.h"
-#include "Types.h"
-#include "VM.h"
diff --git a/include/NamespaceReferenceManager.h b/include/NamespaceReferenceManager.h
deleted file mode 100644
index d09f4d4..0000000
--- a/include/NamespaceReferenceManager.h
+++ /dev/null
@@ -1,107 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "IStorageBackend.h"
-#include "ObjectManager.h"
-
-#include <string>
-#include <vector>
-
-/**
- * @file NamespaceReferenceManager.h
- * @brief Declares logical reference mapping and namespace isolation.
- */
-
-namespace nt {
- /**
- * @class NamespaceReferenceManager
- * @brief Handles the logical mapping and isolation of human-readable references.
- *
- * This manager is the reparse layer of the namespace. Its design mirrors the
- * ReactOS ParseProcedure / STATUS_REPARSE mechanism: a namespace entry whose
- * type carries a ParseProcedure can redirect name resolution to a different
- * path, allowing branch references and ephemeral relations to appear as
- * first-class objects in the namespace without exposing their physical
- * storage paths directly.
- *
- * **Branch references.**
- * A path such as `/refs/branch/main` is a namespace entry backed by a
- * reference object. Its ParseProcedure rewrites the path to the physical
- * snapshot path (e.g. `/snapshots/abc123`). The caller's handle lands on the
- * snapshot. To advance the HEAD, the caller must explicitly open the reference
- * object itself with WRITE access, which is the sole contention point for that
- * branch.
- *
- * **Ephemeral relation resolution.**
- * An ephemeral relation produces tuples through a generator function rather
- * than physical storage; the generator may query base relations on behalf
- * of the runtime to materialise its output. The caller's handle and access
- * mask remain bound to the ephemeral relation. Base relations are accessed
- * only via the ephemeral relation's own type callbacks; the caller never
- * receives a handle on them and does not need explicit permission on them.
- * Capability security is enforced at the ephemeral-relation boundary, not
- * the base relation boundary.
- *
- * **Atomic multi-reference updates.**
- * Updating multiple references simultaneously (e.g. advancing both `main` and
- * `audit_log`) must be all-or-nothing. The implementation should acquire
- * exclusive locks on all target namespace entries before modifying any. If
- * LifecycleManager::Contention fails on any entry, all locks are released and
- * the batch is aborted. This preserves audit consistency.
- *
- * **Cycle guard.**
- * Ephemeral relation definitions may reference other ephemeral relations,
- * and branch aliases may chain. Name resolution must track reparse depth
- * and return an error after a bounded number of iterations to prevent
- * infinite cycles.
- *
- * **Audit branch.**
- * Audit requires global access dissociated from any data branch. It should be
- * modelled as an independent branch; its HEAD is the only contention point for
- * audit-affecting operations.
- *
- * @todo Implement `Resolve(path)` with a reparse loop and cycle depth guard.
- * @todo Define the reference object type with `object_type::exclusive = true`
- * and a ParseProcedure that rewrites the resolution path.
- * @todo Define the batch-update API with all-or-nothing contention semantics.
- * See docs/reactos-ob-comparison.md §8.
- */
- class NamespaceReferenceManager {
- public:
- /**
- * @brief Constructs a NamespaceReferenceManager bound to a registry.
- * @param objects Registry used to look up reference objects (branches,
- * sessions) during reparse.
- * @param storage KV backend; required to read the branch-tree merkle
- * nodes when resolving `/system/branches/<n>/<mg>/...`
- * paths through to their snapshot.
- */
- NamespaceReferenceManager(ObjectManager& objects, IStorageBackend& storage);
-
- /**
- * @brief Rewrites a logical path through reference reparse rules.
- *
- * Currently handles branch references:
- * /system/branches/<name>/<sub>... → /system/snapshots/<target_hash>/<sub>...
- * where target_hash is the value stored on the named Branch object. When
- * the branch is unborn (target_hash empty) or the rest of the path is
- * empty (caller is opening the branch object itself, not something
- * under it), the path is returned unchanged.
- *
- * Session paths (/system/sessions/<X>/branches/<name>/...) will be
- * added when SESSION objects exist; for now they are returned as-is.
- *
- * A 30-iteration cycle guard mirrors the ReactOS reparse cap. If the
- * guard trips the returned path is empty, signaling failure to the
- * caller (Find will subsequently miss).
- *
- * @param path Input logical path components.
- * @return Resolved path components, or empty vector on cycle/failure.
- */
- std::vector<std::string> Resolve(std::vector<std::string> path) const;
-
- private:
- ObjectManager& objects_;
- IStorageBackend& storage_;
- };
-} // namespace nt
diff --git a/include/ObjectManager.h b/include/ObjectManager.h
deleted file mode 100644
index 355f4c7..0000000
--- a/include/ObjectManager.h
+++ /dev/null
@@ -1,335 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "Types.h"
-
-#include <cstdint>
-#include <functional>
-#include <map>
-#include <memory>
-#include <set>
-#include <string>
-#include <vector>
-
-/**
- * @file ObjectManager.h
- * @brief Declares the registry responsible for runtime-managed database objects.
- */
-
-namespace nt {
- /**
- * @brief Owns the registry used to locate and reference database objects.
- *
- * The object manager is the central directory for runtime objects. It keeps
- * the metadata needed to reason about object type, path, references, and
- * handles while the concrete storage model is still being designed.
- */
- class ObjectManager {
- public:
- /** @brief Base type for objects stored in the registry. */
- class IObject {
- public:
- virtual ~IObject() = default;
- };
-
- /**
- * @brief Registry object representing a multigroup snapshot.
- *
- * `merkle_root` is the hex hash of the Merkle<std::string> root node
- * that maps relation_name → relation_merkle_root for this snapshot.
- * Updates are path-localised: inserting one tuple advances the root
- * through O(log_B) node rewrites, leaving sibling subtrees byte-
- * identical.
- *
- * Every commit produces a new Multigroup entry under
- * /system/snapshots/<merkle_root> with one child Relation per leaf at
- * /system/snapshots/<merkle_root>/relations/<relation_name>. Snapshots are
- * immutable (disposable=false, exclusive=false); the rest of the
- * system refers to them by hash. A Multigroup is eligible for GC
- * only when its reference_count drops to zero (no BRANCH_TREE still
- * pins it, no pinned cursor still references it).
- */
- class Multigroup : public IObject {
- public:
- std::string merkle_root;
- };
- /**
- * @brief Registry object representing a stored relation.
- *
- * merkle_root is the hex hash of the Merkle B-tree root node stored in
- * the KV backend. Empty string means the relation contains no tuples.
- * Updated atomically by rnt_link_tuple / rnt_unlink_tuple / rnt_clear_relation.
- */
- class Relation : public IObject {
- public:
- std::string merkle_root;
- };
- /**
- * @brief Registry object representing an ephemeral relation.
- *
- * Ephemeral relations have no tuple storage of their own; tuples are
- * produced on demand by the generator function declared in the
- * ephemeral_object_type descriptor. They compose into a multigroup's
- * tree on equal footing with stored relations — only `merkle_root`
- * participates in the Merkle<std::string> entry for this relation,
- * regardless of how that root was derived.
- *
- * `merkle_root` is the SHA256 hex digest derived from the generator's
- * identity, the current schema, and the merkle_roots of the declared
- * base relations. Any mutation to structure or rebinding of a base
- * produces a new merkle_root, which then propagates into a new
- * multigroup snapshot exactly like a tuple insertion in a stored
- * relation.
- *
- * `dependencies` lists the logical paths of base relations this
- * ephemeral relation is defined atop of. The lifecycle manager Pins
- * each entry while this object is alive, preventing GC of a stored
- * relation that still backs an ephemeral one. The dependency list is
- * also the foundation for attribute-level provenance tracking added
- * later.
- *
- * @todo Define the merkle_root composition function (hashes the
- * generator identity + schema + sorted base merkle_roots).
- * Until ephemeral relations are registered through a writer
- * path this remains a documentation-only contract.
- */
- class EphemeralRelation : public IObject {
- public:
- std::string merkle_root;
- std::vector<std::string> dependencies;
- };
- /** @brief Abstract registry object representing a transaction. */
- class Transaction : public IObject {};
-
- /**
- * @brief Registry object representing an active connection's session.
- *
- * `connection_context` carries whatever auth/connection metadata the
- * caller passed to rnt_session_open. The runtime treats it as an
- * opaque pointer; ownership of the pointee is the caller's concern.
- *
- * `branch_overrides` maps a branch name to the BRANCH_TREE root this
- * session should see for that branch, instead of the global HEAD.
- * NamespaceReferenceManager::Resolve consults the override first when
- * a path of the form /system/sessions/<X>/branches/<name>/<sub>...
- * is requested, falling back to the global /system/branches/<name>
- * lookup when no override is present.
- *
- * The session→branch reference direction is intentional: global
- * branches do not track viewing sessions, which keeps the hot path
- * cheap. GC of a stale snapshot must walk all sessions' override
- * maps to discover live references — amortised cost only paid at GC
- * time. A reverse index can be added later if session counts make
- * the walk hot.
- */
- class Session : public IObject {
- public:
- void* connection_context = nullptr;
- std::map<std::string, std::string> branch_overrides;
- };
-
- /**
- * @brief A named mutable reference to a branch-tree root.
- *
- * `target_hash` is the merkle_root of a BRANCH_TREE — a
- * `Merkle<std::string>` root mapping `mg_name -> mg_hash`. Path
- * resolution walks the branch tree to translate
- * `/system/branches/<n>/multigroups/<mg>/relations/<rel>` into the
- * right `/system/snapshots/<mg_hash>/relations/<rel>`. Empty string
- * means the branch has no commits yet (unborn).
- *
- * Branches are the sole mutable pointer in the system; concurrent
- * writers serialize through LifecycleManager::Contention because the
- * BRANCH object_type carries exclusive=true. Readers never contend —
- * they resolve the branch through its tree to the immutable
- * Multigroups and operate on those.
- */
- class Branch : public IObject {
- public:
- std::string name;
- std::string target_hash;
- };
-
- /**
- * @brief Registry object for a branch-tree blob.
- *
- * `merkle_root` is the Merkle<std::string> root hash of the (mg_name,
- * mg_hash) tree this entry indexes. The same blob is referenced by
- * every BRANCH and session override sharing this tip. Lifecycles:
- *
- * - Pinned once per referencing BRANCH or session override.
- * - When its reference count drops to zero,
- * LifecycleManager::CascadeBranchTree pages the tree and unpins
- * each multigroup it references before unregistering the entry.
- */
- class BranchTree : public IObject {
- public:
- std::string merkle_root;
- };
-
- /**
- * @brief Describes the behavior and capabilities of an object category.
- *
- * @todo Replace `methods` with typed callback fields: `OpenProcedure`,
- * `CloseProcedure`, `DeleteProcedure`, and `ParseProcedure`.
- * Store a `const char* name` label alongside each callback so that
- * logs and assertions can identify which type fired without having
- * to dereference a pointer. `IdentityManager::CanOpen` / `CanClose`
- * then become thin dispatchers that invoke the corresponding
- * procedure, or are absorbed into `HandlerManager` directly.
- * See docs/reactos-ob-comparison.md §2.
- *
- * @todo Add an `exclusive` flag for mutable reference objects (branch
- * HEADs, namespace entries). Non-exclusive objects — immutable
- * snapshots, transactions — must skip contention checks entirely.
- * See docs/reactos-ob-comparison.md §6.
- */
- struct object_type {
- /** Object category label. */
- OBJECT_TYPE label;
- /** True when the object is not durable. For example: TRANSACTION. */
- bool disposable;
- /**
- * Methods supported by this object type.
- * @todo Replace with typed callback function pointers. See class-level todo.
- */
- std::set<METHOD> methods;
- /**
- * When true, only one write-mode handle may be open at a time.
- * Applies to mutable reference states such as branch HEADs.
- * Immutable snapshot objects must leave this false.
- * @todo Wire into LifecycleManager::Contention.
- */
- bool exclusive = false;
-
- virtual ~object_type() = default;
- };
-
- /**
- * @brief Object type descriptor for EPHEMERAL_RELATION objects.
- *
- * An ephemeral relation has no physical storage. Its tuples are produced
- * on demand by a generator function. Cardinality may be finite (e.g. a
- * projected stored relation) or AlephZero (e.g. the eq/lt builtins).
- *
- * The generator receives the bound argument values that were written into
- * the cursor by the JOIN operator before each probe, together with a
- * pagination offset and limit. It must return at most `limit` tuples
- * starting at logical position `offset`.
- *
- * For membership-probe builtins (all args ground), the generator returns
- * at most one tuple. For enumeration over AlephZero relations the generator
- * maps `offset` to the appropriate pair via a bijective enumeration scheme
- * (e.g. Cantor pairing); callers must bound such scans with a TAKE node.
- */
- struct ephemeral_object_type : object_type {
- enum class Cardinality { Finite, ConstrainedFinite, AlephZero, Continuum };
-
- /**
- * @brief Produces a page of tuples for the given bound arguments.
- * @param args Bound argument values written by the JOIN before probing.
- * @param offset Zero-based logical tuple offset (for pagination).
- * @param limit Maximum number of tuples to return.
- */
- using Generator = std::function<std::vector<Tuple>(
- const std::vector<std::string>& args, std::size_t offset, std::size_t limit)>;
-
- Cardinality cardinality;
- Generator generator;
- };
-
- /**
- * @brief Shared metadata stored at the head of a registry entry.
- *
- * Two independent reference counters govern object lifetime.
- * An object is eligible for garbage collection only when both reach zero:
- *
- * - `handle_count` — open sessions holding a cursor or handle on this object.
- * Managed exclusively by LifecycleManager::Monitor / Unmonitor.
- * - `reference_count` — structural dependencies such as an ephemeral relation
- * referencing its base relations, or a cursor pinned to an immutable snapshot
- * version.
- * Managed by LifecycleManager::Pin / Unpin (not yet implemented).
- *
- * All snapshot versions are immutable. An insertion or deletion produces a new
- * snapshot rather than mutating an existing one. A snapshot's `reference_count`
- * is non-zero while any cursor is pinned to it and drops to zero only when all
- * such cursors are released.
- *
- * @todo Implement `reference_count` tracking. Add LifecycleManager::Pin /
- * Unpin. Update LifecycleManager::Unmonitor to gate GC on both counters
- * reaching zero. See docs/reactos-ob-comparison.md §1.
- *
- * @todo Define and attach a security_descriptor. When added, store it as a
- * pointer into a shared, content-addressed SD cache rather than inline.
- * Permissions are strictly capability-based and must never be inherited
- * from a parent namespace entry. See docs/reactos-ob-comparison.md §5.
- */
- struct registry_head {
- /**
- * Structural dependency count. Non-zero while another registry object
- * references this one (e.g. an ephemeral relation depending on a base
- * relation, or a cursor pinned to a snapshot version).
- * @todo Implement via LifecycleManager::Pin / Unpin.
- */
- uint32_t reference_count = 0;
- /** Number of open handles. Maintained by LifecycleManager::Monitor/Unmonitor. */
- uint32_t handle_count = 0;
- /** Object type metadata. Owned by this head. */
- std::unique_ptr<object_type> type;
- /** Logical object path inside the namespace. */
- std::vector<std::string> path;
- };
-
- /** @brief Node in the object registry. */
- struct registry {
- /** Shared metadata for this object. */
- std::unique_ptr<registry_head> head;
- /** Abstract object payload. Owned by this entry. */
- std::unique_ptr<IObject> object;
- /**
- * Next entry in the registry. Currently a flat linked list.
- * @todo Replace with a trie whose nodes are per-path-segment hash maps.
- * Each directory level holds a hash map from segment string to
- * child registry*. Find() and Register() walk the trie
- * component-by-component instead of scanning a flat list.
- * Consider pulling a well-tested radix-tree via the Nix flake rather than
- * implementing from scratch. See docs/reactos-ob-comparison.md §3.
- */
- std::unique_ptr<registry> next;
- };
-
- /** @brief Head of the registry linked list. Owned by this manager. */
- std::unique_ptr<registry> entries;
-
- /**
- * @brief Registers an object under the given path, taking ownership of it.
- * @param path Logical object path.
- * @param object Object payload. Ownership is transferred to the registry.
- * @param type Object type descriptor. Ownership is transferred to the registry.
- */
- void Register(std::vector<std::string> path, std::unique_ptr<IObject> object,
- std::unique_ptr<object_type> type);
-
- /**
- * @brief Looks into this registry and attempts to retrieve an entry.
- * @param object_path Logical path to search for.
- * @return Borrowed pointer to the matching entry, or nullptr when not found.
- */
- registry* Find(const std::vector<std::string> object_path);
-
- /**
- * @brief Removes the entry registered at @p object_path, if any.
- *
- * Splices the matching node out of the registry list and frees its
- * owned object and head. No reference-count checks are performed —
- * the caller is responsible for releasing handles and pins before
- * calling this. Intended for disposable objects (sessions today,
- * other disposables once GC lands in step 6).
- *
- * @param object_path Logical path of the entry to remove.
- * @return True when an entry was removed, false when nothing matched.
- */
- bool Unregister(const std::vector<std::string>& object_path);
- };
-} // namespace nt
diff --git a/include/PermissionsManager.h b/include/PermissionsManager.h
deleted file mode 100644
index af72154..0000000
--- a/include/PermissionsManager.h
+++ /dev/null
@@ -1,88 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "ObjectManager.h"
-#include "Types.h"
-
-#include <set>
-
-/**
- * @file PermissionsManager.h
- * @brief Declares authentication and object-access checks.
- */
-
-namespace nt {
- /**
- * @class Capability-based permission manager
- * @brief Evaluates connection claims and object permissions.
- *
- * Permission checks are intentionally separated from lookup. The object
- * manager can retrieve a candidate object, while this manager decides
- * whether the caller is allowed to use it.
- *
- * Permissions are strictly capability-based and must be explicitly granted.
- * There is no implicit flow of access rights from a parent namespace entry to
- * its children (no SD inheritance). A caller who holds READ on a multigroup
- * does not thereby hold READ on its constituent relations — that is a separate,
- * explicit capability.
- *
- * @todo Define a security_descriptor structure. Store instances in a shared,
- * content-addressed cache (keyed by descriptor content, not by namespace
- * ancestry) so that identical ACLs are not duplicated across relations.
- * See docs/reactos-ob-comparison.md §5 and §7.
- *
- * @todo Cache the result of Access() in HandlerManager::handle::granted_access
- * at open time. Per-operation checks should then read from the handle
- * rather than re-evaluating the descriptor on every call.
- * See docs/reactos-ob-comparison.md §4.
- */
- class PermissionsManager {
- public:
- /**
- * @brief Validates a connection during login.
- * @param method Authentication method used by the connection.
- * @return Claims granted to the authenticated connection.
- */
- std::set<AUTH_CLAIM> Firewall(AUTH_METHOD method);
-
- /**
- * @brief Validates permission to an object.
- *
- * Checks the security descriptor on the head of the object found in
- * ObjectManager::registry. It should also check connection metadata
- * that is still to be defined, such as user group and policy.
- *
- * It is still undecided whether this should run on every attempt to get
- * hold of an object, since that might produce significant overhead.
- *
- * Note that the permissions systems also rely on objects with capabilities,
- * so imagining the case an ephemeral relation is created based on
- * 3 other stored relations, while you can access the ephemeral relation,
- * you cannot directly access the other 3 relations. You need explicit
- * permission to create a handle on the others.
- *
- * Here's an example tree:
- * /system
- * /multigroups
- * /coffee_shop
- * /relations
- * /user {type : stored}
- * /order {type : stored}
- * /user_and_order {type : ephemeral; deps =
- * [/system/multigroups/coffee_shop/relations/users,
- * /system/multigroups/coffee_shop/relations/orders]} /users /peter /permissions
- * /multigroups/coffee_shop/relations/user_and_order {descriptor : [read]}
- * /paul
- * /permissions
- * /multigroups/coffee_shop/relations/user {descriptor : [write, read]}
- *
- * Meaning that `peter` has access to read the `user_and_order` ephemeral
- * relation, but cannot open a handle on `user` and `order`.
- *
- * @param object Object being accessed.
- * @param connection_context Connection metadata for the caller.
- * @return True when access is allowed.
- */
- const bool Access(const ObjectManager::registry* object, const void* connection_context);
- };
-} // namespace nt
diff --git a/include/RNT_C_API.h b/include/RNT_C_API.h
deleted file mode 100644
index 1a59785..0000000
--- a/include/RNT_C_API.h
+++ /dev/null
@@ -1,501 +0,0 @@
-#pragma once
-
-/**
- * @file RNT_C_API.h
- * @brief C-callable surface over the RNT manager pipeline for OCaml ctypes.
- *
- * All types are opaque void pointers on the C side; the implementation casts
- * them to the appropriate C++ types internally. Callers must never dereference
- * handle or cursor pointers directly.
- *
- * ## Memory contract
- * - Strings returned via out-parameters (char**) are heap-allocated by the
- * API and must be released with rnt_free_string().
- * - Payloads returned via (uint8_t**, size_t*) are heap-allocated and must
- * be released with rnt_free_bytes().
- * - Handles and cursors are owned by the caller and must be closed before
- * the program exits.
- *
- * ## Error convention
- * - Functions returning int use 0 for success and a negative value for error.
- * - Functions returning a pointer return NULL on failure.
- *
- * ## Thread safety
- * This API is **not thread-safe**. The global runtime (g_rt) is a single
- * in-process instance; all callers share the same ObjectManager and storage
- * backend. The expected usage model is a single OCaml thread (or domain)
- * driving the API at a time. If concurrent access is needed in the future,
- * a per-object or per-relation mutex strategy should be introduced.
- *
- * @todo Implement AUTH_CLAIM::READ/WRITE enforcement in rnt_open_handle once
- * PermissionsManager::Access is wired to a real policy engine. Currently
- * all handles open with full access regardless of the claims parameter.
- */
-
-#include <stddef.h>
-#include <stdint.h>
-
-#include "Api.h"
-#include "VM.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/** Opaque handle to a registry object opened through HandlerManager. */
-typedef void* rnt_handle_t;
-
-/** Opaque cursor over a relation, driven by the VM or used directly. */
-typedef void* rnt_cursor_t;
-
-/* ------------------------------------------------------------------ */
-/* Runtime lifecycle */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Initializes the RNT runtime with the selected storage backend.
- *
- * Must be called before any other API function. Once the runtime is
- * successfully initialized, subsequent calls are no-ops and return 0.
- * If initialization fails (returns negative), the call may be retried
- * with corrected parameters — the runtime is left in a clean state.
- *
- * @param driver Storage driver to use: "sqlite" or "memory".
- * "sqlite" persists data to @p storage_path.
- * "memory" keeps all data in process memory (ignores
- * @p storage_path); intended for tests.
- * @param storage_path File path for the SQLite database, or ":memory:" for
- * an ephemeral SQLite store. Ignored when driver is
- * "memory".
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_init(const char* driver, const char* storage_path);
-
-/* ------------------------------------------------------------------ */
-/* Authentication */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Runs PermissionsManager::Firewall for a connection.
- *
- * @param auth_method "plain_text" or "certificate".
- * @param claims_out Set to a heap-allocated, newline-separated claim string.
- * Release with rnt_free_string(). NULL on error.
- * @return 0 on success, negative when authentication is rejected.
- */
-NT_API int rnt_firewall(const char* auth_method, char** claims_out);
-
-/* ------------------------------------------------------------------ */
-/* Session lifecycle */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Opens a session and registers it at /system/sessions/<hash>.
- *
- * The runtime mints a random 256-bit hex hash for the session; the caller
- * has no influence over the value. The returned string is the opaque
- * identifier required by every other rnt_session_* call.
- *
- * @param connection_context Caller-owned pointer carried on the session.
- * The runtime treats it as opaque; ownership of
- * the pointee remains with the caller.
- * @param session_hash_out Set to a heap-allocated 64-char hex string.
- * Release with rnt_free_string().
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_session_open(void* connection_context, char** session_hash_out);
-
-/**
- * @brief Closes a session, unregistering it from /system/sessions/<hash>.
- *
- * After this call the session hash is invalid; subsequent rnt_session_* or
- * resolver lookups against it will fail. Branch overrides held by the
- * session are dropped.
- *
- * @param session_hash Hash returned by rnt_session_open.
- * @return 0 on success, negative when the hash does not match an active session.
- */
-NT_API int rnt_session_close(const char* session_hash);
-
-/**
- * @brief Sets a per-session override for a branch.
- *
- * While the override is in effect, paths resolved through
- * /system/sessions/<hash>/branches/<branch_name>/... point at
- * /system/snapshots/<target_hash>/... regardless of where the global branch
- * HEAD currently sits. Pass an empty string for @p target_hash to remove
- * the override and resume falling back to the global branch.
- *
- * Non-empty target hashes must correspond to an existing
- * /system/snapshots/<target_hash> entry; otherwise the call fails.
- *
- * @param session_hash Session identifier.
- * @param branch_name Branch the override applies to (e.g. "main").
- * @param target_hash Snapshot hash to bind, or "" to clear.
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_session_set_branch(const char* session_hash, const char* branch_name,
- const char* target_hash);
-
-/* ------------------------------------------------------------------ */
-/* Handle lifecycle */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Opens a handle to the object at the given slash-separated path.
- *
- * Runs the full HandlerManager::Open pipeline:
- * ObjectManager::Find → PermissionsManager::Access →
- * IdentityManager::CanOpen → LifecycleManager::Contention →
- * LifecycleManager::Monitor.
- *
- * @param path Slash-separated logical path, e.g. "/system/branches/main".
- * @param claims Claim string returned by rnt_firewall (may be NULL).
- * @return Handle pointer, or NULL when the path is not found or access is denied.
- */
-NT_API rnt_handle_t rnt_open_handle(const char* path, const char* claims);
-
-/**
- * @brief Closes a handle, running HandlerManager::Close and Unmonitor.
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_close_handle(rnt_handle_t handle);
-
-/**
- * @brief Reads the current branch-tree root hash from a BRANCH object.
- *
- * Only valid for handles opened on BRANCH objects. The returned string is
- * the merkle_root of the BRANCH_TREE this branch currently points at — a
- * `Merkle<std::string>` root mapping mg_name → mg_hash. An empty string
- * indicates an unborn branch with no commits yet.
- *
- * @param handle Handle to a BRANCH object.
- * @param target_hash_out Set to a heap-allocated copy of the target hash
- * (possibly empty). Release with rnt_free_string().
- * @return 0 on success, negative when the handle is not a BRANCH.
- */
-NT_API int rnt_branch_target(rnt_handle_t handle, char** target_hash_out);
-
-/**
- * @brief Atomically advances a branch to point at a new branch-tree root.
- *
- * The new hash must already exist as a content-addressed blob in the KV
- * store — branch-tree roots are produced by mutating calls (rnt_link_tuple,
- * rnt_register_relation, …) and cannot be invented externally. Pass an
- * empty string to reset the branch to unborn.
- *
- * Write exclusion is structural: BRANCH objects carry @c exclusive=true in
- * their object_type, so LifecycleManager::Contention prevents a second handle
- * from being opened while a writer holds the branch. AUTH_CLAIM::WRITE is not
- * checked at the C API boundary.
- *
- * @param branch_path Slash-separated branch path, e.g. "/system/branches/main".
- * @param new_hash Branch-tree root to advance to (64-char lowercase hex),
- * or "" to reset to unborn.
- * @return 0 on success, negative when the branch is not found, the type is
- * not BRANCH, or @p new_hash is non-empty but no matching blob
- * exists in the KV store.
- */
-NT_API int rnt_branch_advance(const char* branch_path, const char* new_hash);
-
-/* ------------------------------------------------------------------ */
-/* Object registration */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Registers a RELATION object at the given path.
- *
- * Idempotent: if an object already exists at the path, the call succeeds
- * without re-registering.
- *
- * @param path Slash-separated path of the form
- * "/system/branches/<branch>/multigroups/<mg>/relations/<rel>".
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_register_relation(const char* path);
-
-/**
- * @brief Registers a BRANCH object at the given path pointing at a branch-tree root.
- *
- * If a BRANCH already exists at this path, returns 0 without modifying it.
- * When @p target_hash is non-NULL and non-empty it must already exist as a
- * content-addressed blob in the KV store (produced by an earlier mutation
- * through the same runtime); otherwise the call fails. Pass NULL or "" to
- * register an unborn branch.
- *
- * @param path Slash-separated path, e.g. "/system/branches/main".
- * @param target_hash Branch-tree root this branch points at (may be NULL
- * or "" for unborn).
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_register_branch(const char* path, const char* target_hash);
-
-/**
- * @brief Lists all relations of a specific multigroup under a branch.
- *
- * Returns a newline-delimited string of "name\troot_hash" pairs, one per
- * relation in the named multigroup at the branch's current tree. An unborn
- * branch or an mg absent from the branch tree yields an empty string. The
- * caller must release the string with rnt_free_string().
- *
- * @param branch_mg_path Slash-separated path of the form
- * "/system/branches/<name>/multigroups/<mg>".
- * @param out Set to a heap-allocated "name\troot\n" string.
- * Release with rnt_free_string(). NULL on error.
- * @return 0 on success, negative when the path shape is wrong or the branch
- * is not found.
- */
-NT_API int rnt_list_relations(const char* branch_mg_path, char** out);
-
-/**
- * @brief Lists all multigroups bound to a branch.
- *
- * Reads the branch-tree root from `/system/branches/<name>` and pages each
- * (mg_name, mg_hash) entry. Returns "name\thash\n" lines. Unborn branches
- * yield an empty string.
- *
- * @param branch_path Slash-separated branch path, e.g. "/system/branches/main".
- * @param out Heap-allocated string; release with rnt_free_string().
- * @return 0 on success, negative when the branch is not found.
- */
-NT_API int rnt_list_branch_multigroups(const char* branch_path, char** out);
-
-/**
- * @brief Lists all relations stored in a specific snapshot.
- *
- * Reads the multigroup codec directly from the snapshot at
- * /system/snapshots/<snapshot_hash> without following any branch HEAD.
- * Use this when the desired snapshot hash is already known (e.g. detached
- * checkout). Returns the same "name\troot_hash\n" format as rnt_list_relations.
- *
- * @param snapshot_hash 64-char hex hash of the snapshot.
- * @param out Set to heap-allocated string. Release with rnt_free_string().
- * @return 0 on success, negative when the snapshot is not registered.
- */
-NT_API int rnt_list_snapshot_relations(const char* snapshot_hash, char** out);
-
-/* ------------------------------------------------------------------ */
-/* Tuple storage */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Stores a tuple and links it to the relation at the given path.
- *
- * Tuples are encoded as a flat key=value string, one attribute per line:
- * "name=Blathers\nprofession=Museum Curator\n"
- * Attributes are sorted by name before hashing to ensure content-addressing
- * is order-independent.
- *
- * @param relation_path Slash-separated relation path.
- * @param kv_attrs Newline-delimited "key=value" attribute string.
- * @param hash_out Set to the 64-character hex SHA-256 hash of the tuple.
- * Release with rnt_free_string(). NULL on error.
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_link_tuple(const char* relation_path, const char* kv_attrs, char** hash_out);
-
-/**
- * @brief Removes a tuple from the relation's Merkle tree and tuple store.
- *
- * Calls Merkle::Remove on the relation's current root and updates
- * ObjectManager::Relation::merkle_root atomically. The tuple bytes remain in
- * the KV store (they may be referenced by older snapshots); only the
- * membership in the current tree is removed.
- *
- * @param relation_path Slash-separated relation path.
- * @param tuple_hash 64-character hex SHA-256 of the tuple to remove.
- * @return 0 on success, negative when the relation is not found.
- */
-NT_API int rnt_unlink_tuple(const char* relation_path, const char* tuple_hash);
-
-/**
- * @brief Resets a relation's Merkle root to the empty-tree state.
- *
- * Sets ObjectManager::Relation::merkle_root to the empty string. Existing
- * tuple bytes remain in the KV store. After this call the relation contains
- * no tuples from the perspective of cursor iteration.
- *
- * @param relation_path Slash-separated relation path.
- * @return 0 on success, negative when the relation is not found.
- */
-NT_API int rnt_clear_relation(const char* relation_path);
-
-/**
- * @brief Returns the current Merkle root hash for a relation.
- *
- * OCaml calls this after rnt_link_tuple or rnt_unlink_tuple to read the
- * updated root back and store it in the in-memory multigroup (tree_pointer).
- * An empty string is returned for a relation that contains no tuples.
- *
- * @param relation_path Slash-separated relation path.
- * @param root_hash_out Set to the 64-character hex root hash, or to an empty
- * heap-allocated string for an empty relation.
- * Release with rnt_free_string().
- * @return 0 on success, negative when the relation is not found.
- */
-NT_API int rnt_relation_root(const char* relation_path, char** root_hash_out);
-
-/* ------------------------------------------------------------------ */
-/* Cursor and VM */
-/* ------------------------------------------------------------------ */
-
-/**
- * @brief Opens a cursor on the relation referenced by handle.
- *
- * The returned cursor is positioned before the first tuple. Advance it with
- * rnt_cursor_next(). The cursor must be closed with rnt_cursor_close() before
- * closing the handle.
- *
- * @param handle Open RELATION handle.
- * @return Cursor pointer, or NULL on error.
- */
-NT_API rnt_cursor_t rnt_cursor_open(rnt_handle_t handle);
-
-/**
- * @brief Advances the cursor and returns the next tuple as a kv string.
- *
- * The tuple is encoded as a newline-delimited "key=value" string matching the
- * format accepted by rnt_link_tuple:
- * "name=Blathers\nprofession=Museum Curator\n"
- *
- * @param cursor Open cursor.
- * @param tuple_out Set to a heap-allocated kv string, or NULL when exhausted.
- * Release with rnt_free_string() when non-NULL.
- * @return 1 when a tuple was returned, 0 when exhausted, negative on error.
- */
-NT_API int rnt_cursor_next(rnt_cursor_t cursor, char** tuple_out);
-
-/**
- * @brief Closes a cursor and releases its resources.
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_cursor_close(rnt_cursor_t cursor);
-
-/* ------------------------------------------------------------------ */
-/* VM plan builder */
-/* ------------------------------------------------------------------ */
-
-/** Opaque plan node tree, built via rnt_plan_assemble calls. */
-typedef void* rnt_plan_t;
-
-/**
- * SCAN context: reads all tuples from a stored relation.
- *
- * @param relation_path Absolute slash-separated path, e.g.
- * "/system/branches/main/multigroups/warehouse/relations/public:users".
- */
-typedef struct {
- const char* relation_path;
-} PlanArgsScan;
-
-/**
- * JOIN context: nested-loop join of two child plans.
- *
- * Takes ownership of both children. They are released when the assembled plan
- * is freed or executed; on failure both are freed.
- */
-typedef struct {
- rnt_plan_t left;
- rnt_plan_t right;
-} PlanArgsJoin;
-
-/**
- * TAKE context: passes at most @p limit tuples from @p source, then stops.
- *
- * Takes ownership of @p source; on failure @p source is freed.
- */
-typedef struct {
- rnt_plan_t source;
- size_t limit;
-} PlanArgsTake;
-
-/**
- * PROJECT context: keeps only the named attributes of @p source.
- *
- * @p attrs is a NULL-terminated array of attribute names to retain, e.g.
- * <tt>{ "name", "profession", NULL }</tt>. Takes ownership of @p source; on
- * failure @p source is freed.
- */
-typedef struct {
- rnt_plan_t source;
- const char** attrs;
-} PlanArgsProject;
-
-/**
- * A single plan-construction request. @p operation selects which context member
- * is read; the others are ignored. The members are laid out side by side rather
- * than overlapped in a union so the struct maps cleanly onto OCaml ctypes, which
- * has no union support. Only the member matching @p operation need be populated.
- */
-typedef struct {
- nt::Operation operation;
- PlanArgsScan scan;
- PlanArgsJoin join;
- PlanArgsTake take;
- PlanArgsProject project;
-} PlanAction;
-
-/**
- * @brief Builds one plan node from @p action and returns the resulting subtree.
- *
- * Sole entry point for plan construction. Validates runtime state once, then
- * dispatches on @p action.op. Child plans for JOIN/TAKE/PROJECT are themselves
- * results of prior rnt_plan_assemble calls; ownership transfers per the
- * per-operator rules documented on each PlanArgs* struct.
- *
- * @return Plan node, or NULL when the runtime is uninitialised or construction
- * fails (e.g. relation does not exist).
- */
-NT_API rnt_plan_t rnt_plan_assemble(PlanAction action);
-
-/**
- * @brief Releases a plan that was built but not yet executed.
- *
- * Closes any open cursors and handles owned by the plan tree, then frees all
- * nodes. Safe to call with NULL. Do NOT call after rnt_vm_execute_plan —
- * that function takes ownership.
- */
-NT_API void rnt_plan_free(rnt_plan_t plan);
-
-/**
- * @brief Executes a plan tree and returns a streaming VM cursor.
- *
- * Takes ownership of @p plan; the caller must not call rnt_plan_free after this.
- * The returned cursor is advanced with rnt_vm_cursor_next() and must be closed
- * with rnt_vm_cursor_close(), which also releases all plan resources.
- *
- * @return VM cursor, or NULL when the plan is NULL.
- */
-NT_API rnt_cursor_t rnt_vm_execute_plan(rnt_plan_t plan);
-
-/**
- * @brief Advances a VM cursor and returns the next merged tuple as a kv string.
- *
- * Same encoding and return-value semantics as rnt_cursor_next.
- *
- * @param vm_cursor Cursor returned by rnt_vm_execute_plan.
- * @param tuple_out Set to a heap-allocated kv string, or NULL when exhausted.
- * Release with rnt_free_string() when non-NULL.
- * @return 1 when a tuple was returned, 0 when exhausted, negative on error.
- */
-NT_API int rnt_vm_cursor_next(rnt_cursor_t vm_cursor, char** tuple_out);
-
-/**
- * @brief Closes a VM cursor, releases all plan nodes, cursors, and handles.
- * @return 0 on success, negative on error.
- */
-NT_API int rnt_vm_cursor_close(rnt_cursor_t vm_cursor);
-
-/* ------------------------------------------------------------------ */
-/* Memory management */
-/* ------------------------------------------------------------------ */
-
-/** @brief Releases a string allocated by the API. Safe to call with NULL. */
-NT_API void rnt_free_string(char* s);
-
-/** @brief Releases a byte buffer allocated by the API. Safe to call with NULL. */
-NT_API void rnt_free_bytes(uint8_t* p);
-
-#ifdef __cplusplus
-}
-#endif
diff --git a/include/Runtime.h b/include/Runtime.h
deleted file mode 100644
index 0fb41a5..0000000
--- a/include/Runtime.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include "Api.h"
-
-/**
- * @file Runtime.h
- * @brief Declares the top-level Relational NT runtime facade.
- */
-
-namespace nt {
- /** @brief Top-level runtime object used by applications and the console entry point. */
- class NT {
- public:
- /**
- * @brief Reports whether the runtime is active.
- * @return True when the runtime is running.
- */
- bool IsRunning() const;
-
- /** @brief Simulates an external entry call into the runtime. */
- void SimulateEntryCall();
- };
-} // namespace nt
diff --git a/include/SqliteBackend.h b/include/SqliteBackend.h
deleted file mode 100644
index 037d8e6..0000000
--- a/include/SqliteBackend.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "IStorageBackend.h"
-
-#include <sqlite3.h>
-#include <string>
-#include <vector>
-
-/**
- * @file SqliteBackend.h
- * @brief IStorageBackend implementation backed by an in-process SQLite database.
- *
- * SQLite schema:
- *
- * kv(hash TEXT PRIMARY KEY, value BLOB NOT NULL)
- * — content-addressed store; key is the SHA256 hex digest of value.
- *
- * Relation membership is tracked by the Merkle B-tree maintained in-process.
- * The former relation_index table has been removed; tuple hashes are looked up
- * via Merkle::Page which reads only the KV store.
- */
-
-namespace nt {
- class SqliteBackend : public IStorageBackend {
- public:
- /**
- * @brief Opens (or creates) a SQLite database at @p path.
- * @param path File path, or ":memory:" for an in-process database.
- * @throws std::runtime_error if the database cannot be opened.
- */
- explicit SqliteBackend(const std::string& path = ":memory:");
- ~SqliteBackend() override;
-
- std::string Put(std::vector<uint8_t> value) override;
- std::optional<std::vector<uint8_t>> Get(const std::string& hash) override;
-
- private:
- sqlite3* db_ = nullptr;
- };
-} // namespace nt
diff --git a/include/TupleCodec.h b/include/TupleCodec.h
deleted file mode 100644
index 6c74c1e..0000000
--- a/include/TupleCodec.h
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma once
-
-#include "Types.h"
-
-#include <cstdint>
-#include <string>
-#include <vector>
-
-/**
- * @file TupleCodec.h
- * @brief Content-addressing utilities for tuples.
- *
- * Mirrors Sakura's hashing.ml and physical.ml:
- * - Hash() ≈ hash_tuple: deterministic SHA256 over sorted attribute content
- * - Serialize() ≈ Marshal.to_bytes: raw bytes stored as the KV value
- * - Deserialize() ≈ Marshal.from_bytes: reconstruct attributes from raw bytes
- *
- * The hash input format is:
- * "path_seg1/path_seg2/..|name1:value1;name2:value2;"
- * where attributes are sorted by name for determinism.
- *
- * The serialization format is a sequence of length-prefixed UTF-8 pairs:
- * [uint32_le name_len][name bytes][uint32_le value_len][value bytes] ...
- */
-
-namespace nt::TupleCodec {
- /**
- * @brief Computes the SHA256 hex digest of the serialized tuple.
- *
- * Equivalent to SHA256(Serialize(attrs)), so the value returned here
- * matches the key returned by IStorageBackend::Put(Serialize(attrs)).
- *
- * @param attrs Attribute set. Need not be pre-sorted.
- * @return 64-character lowercase hex string.
- */
- std::string Hash(const std::vector<Attribute>& attrs);
-
- /**
- * @brief Serializes a tuple's attributes to raw bytes for KV storage.
- * @param attrs Attribute set. Need not be pre-sorted.
- * @return Serialized bytes.
- */
- std::vector<uint8_t> Serialize(const std::vector<Attribute>& attrs);
-
- /**
- * @brief Reconstructs attributes from the bytes returned by Serialize().
- * @param bytes Bytes produced by Serialize().
- * @return Reconstructed attribute list in serialization order.
- */
- std::vector<Attribute> Deserialize(const std::vector<uint8_t>& bytes);
-} // namespace nt::TupleCodec
diff --git a/include/Types.h b/include/Types.h
deleted file mode 100644
index 6956e09..0000000
--- a/include/Types.h
+++ /dev/null
@@ -1,159 +0,0 @@
-#pragma once
-
-#include <string>
-#include <vector>
-
-/**
- * @file Types.h
- * @brief Shared enum types and core data types used by the runtime.
- */
-
-/** @brief Runtime object categories known by the object manager. */
-enum OBJECT_TYPE {
- /** A database snapshot grouping relations and tuples. */
- MULTIGROUP,
- /** A relation object backed by physical storage. */
- RELATION,
- /** An attribute object (schema metadata). */
- ATTRIBUTE,
- /** A transaction object. */
- TRANSACTION,
- /**
- * A relation whose tuples are produced on demand by a generator function
- * rather than read from physical storage. Behaves like a RELATION in every
- * other respect: owns a `merkle_root`, has mutable schema, composes into its
- * multigroup's hash, and may be opened, scanned, and pinned through the
- * same handle pipeline.
- *
- * The generator receives unsigned-int paginators (offset, limit) plus any
- * bound argument values written by an upstream JOIN, and yields tuples.
- * Cardinality may be finite (e.g. a projection over a stored relation) or
- * AlephZero (e.g. the eq builtin). The object_type for this label must be
- * an ephemeral_object_type.
- *
- * Tracked as a distinct label — rather than collapsed into RELATION —
- * for three reasons:
- * 1. Sakura distinguishes generated relations as a separate subclass at
- * the wire layer, even though the runtime treats them uniformly.
- * 2. They have no physical tuple-storage backend; reads always go
- * through the generator.
- * 3. They carry a dependency list of base relation paths they are
- * defined atop of. This drives structural reference counting (a
- * base relation cannot be GC'd while an ephemeral relation depends
- * on it) and is the foundation for attribute-level provenance
- * tracking added later.
- */
- EPHEMERAL_RELATION,
- /**
- * A named mutable reference to a multigroup snapshot, analogous to a git
- * branch. A BRANCH object carries a `target_hash` — the merkle_root of
- * the snapshot it currently points at — and nothing else.
- *
- * Branch objects are the entry point for database connections: a client
- * opens a handle to /system/branches/<name>, reads target_hash, and uses
- * that hash to open the snapshot at /system/snapshots/<hash>. Multigroup
- * state is reconstructed by walking the content-addressed graph from
- * that snapshot, not by deserializing bytes attached to the branch.
- *
- * The `exclusive` flag on the object_type should be set to true so that
- * LifecycleManager::Contention serializes concurrent writers.
- */
- BRANCH,
- /**
- * A per-connection runtime context. SESSION objects own the
- * connection_context pointer (auth metadata, etc.) and a map of per-session
- * branch overrides: `branch_name -> target_hash`. Overrides let one
- * connection observe a different snapshot for a branch than the global
- * HEAD without affecting other sessions.
- *
- * Sessions are identified by random 256-bit hex hashes minted at
- * rnt_session_open. The hash is opaque to the caller and is the only
- * handle for subsequent rnt_session_* calls. SESSION objects live at
- * /system/sessions/<hash>; the resolver consults
- * /system/sessions/<hash>/branches/<n> first and falls back to the global
- * /system/branches/<n> when there is no override.
- */
- SESSION,
- /**
- * The content-addressed Merkle<string> root of a branch's multigroup map.
- * A BRANCH points at a BRANCH_TREE via target_hash; the tree maps
- * `mg_name -> mg_hash` and is paged at resolve time to translate
- * `/system/branches/<n>/<mg>/<rel>` into `/system/snapshots/<mg_hash>/<rel>`.
- *
- * BRANCH_TREE objects live at `/system/branch_trees/<hash>`. They are
- * immutable (disposable=false, exclusive=false) and pinned by the BRANCH
- * objects that reference them; the mgs they reference are in turn pinned
- * by the BRANCH_TREE via LifecycleManager::CascadeBranchTree at GC time.
- * The same blob is reused across all branches and session overrides
- * that share a tip.
- */
- BRANCH_TREE
-};
-
-/** @brief Operations that may be supported by an object type. */
-enum METHOD {
- /** Opens an object and creates an authorized handle. */
- OPEN,
- /** Closes a previously opened handle. */
- CLOSE,
- /** Parses through a secondary namespace. */
- PARSE,
- /** Performs security checks. */
- SECURITY
-};
-
-/** @brief Claims granted to an authenticated connection. */
-enum AUTH_CLAIM {
- /** Permission to read object state. */
- READ,
- /** Permission to mutate object state. */
- WRITE
-};
-
-/** @brief Authentication strategies supported by the permissions layer. */
-enum AUTH_METHOD {
- /** Certificate-based authentication. */
- CERTIFICATE,
- /** Plain-text authentication. */
- PLAIN_TEXT
-};
-
-namespace nt {
- /** @brief A single named value in a tuple, produced by the cursor layer. */
- struct Attribute {
- std::string name;
- std::string value;
- };
-
- /**
- * @brief A single row of data streamed lazily from the cursor layer.
- *
- * Follows the Volcano pull model: callers repeatedly call Next() to consume
- * one attribute at a time. Returns nullptr when all attributes are exhausted.
- */
- class Tuple {
- public:
- explicit Tuple(std::vector<Attribute> attributes) : attributes_(std::move(attributes)) {
- }
-
- /** @brief Returns the next attribute, or nullptr when exhausted. */
- const Attribute* Next() {
- if (position_ < attributes_.size())
- return &attributes_[position_++];
- return nullptr;
- }
-
- void Reset() {
- position_ = 0;
- }
-
- /** @brief Direct read-only access to all attributes without affecting position. */
- const std::vector<Attribute>& attrs() const {
- return attributes_;
- }
-
- private:
- std::vector<Attribute> attributes_;
- size_t position_ = 0;
- };
-} // namespace nt
diff --git a/include/VM.h b/include/VM.h
deleted file mode 100644
index 3d97779..0000000
--- a/include/VM.h
+++ /dev/null
@@ -1,190 +0,0 @@
-#pragma once
-
-#include "Api.h"
-#include "CursorManager.h"
-#include "Types.h"
-
-#include <optional>
-#include <string>
-#include <unordered_set>
-#include <vector>
-
-/**
- * @file VM.h
- * @brief Declares the logic execution boundary for relational and higher-order workloads.
- */
-
-namespace nt {
- /**
- * @class VM
- * @brief Hosts the first-order logic (FOL) and second-order logic (SOL) cores.
- *
- * This partitioning exists to ensure retrieval terminates when execution is
- * constrained to first-order logic.
- *
- * - Tarski runtime (FOL): a relational engine that implements Tarski's
- * relational calculus. It uses a deterministic Volcano-iterator model to
- * perform relational algebra operations such as joins, projections, and
- * selects directly against the CursorManager and a prepared plan.
- *
- * - Karuta runtime (SOL): an isolated WAM-based environment for
- * higher-order logic programming. It handles recursion, choice points, and
- * other programming constructs that do not guarantee termination.
- *
- * @remark Lazy evaluation and paging strategy
- *
- * Both the Tarski first-order engine and the Karuta second-order engine are
- * strictly pull-based. Data is streamed lazily from the CursorManager to
- * preserve a constant memory footprint regardless of relation size.
- *
- * Pipeline dynamics:
- * - Demand-driven: the Karuta WAM requests a fact only when a goal needs
- * satisfaction. A request propagates down the algebra tree as a chain of
- * Next() calls.
- *
- * - Short-circuiting: if the Karuta engine finds a solution or reaches a
- * failure that invalidates a branch, the Tarski iterator is discarded or
- * reset without materializing the remaining tuples.
- *
- * - Resource stewardship: by maintaining laziness up to the highest logical
- * level, the runtime minimizes pressure on ObjectManager and keeps
- * snapshots pinned through LifecycleManager::Monitor only for the minimum
- * time required.
- */
-
- /**
- * @brief One segment of a parameterized relation path in a SCAN node.
- *
- * Var segments are resolved at runtime from the outer tuple of a JOIN:
- * the JOIN writes the bound value into the cursor's args vector before
- * each probe. Const segments are ground literals fixed at plan construction.
- *
- * The resolved args vector is passed to the ephemeral relation's generator
- * (for EPHEMERAL_RELATION cursors) or ignored (for stored RELATION cursors,
- * which need no per-probe parameterization).
- */
- struct PathArg {
- enum class Kind { Var, Const };
-
- Kind kind;
- std::string name; /**< Attribute name to look up (Var), or literal value (Const). */
-
- static PathArg Var(std::string attr) {
- return {Kind::Var, std::move(attr)};
- }
- static PathArg Const(std::string val) {
- return {Kind::Const, std::move(val)};
- }
- };
-
- enum Operation {
- FOL_OPERATION_SCAN = 1,
- FOL_OPERATION_JOIN,
- FOL_OPERATION_TAKE,
- FOL_OPERATION_PROJECT
- };
-
- /**
- * @brief A node in the Volcano operator tree executed by the Tarski (FOL) runtime.
- *
- * Each node represents one relational algebra operator. The tree is demand-driven:
- * VM::Next() propagates pull requests from root to leaves.
- *
- * Supported operators:
- * - SCAN : pulls tuples from a pre-opened cursor (stored or ephemeral relation).
- * - JOIN : nested-loop join. For each outer (left) tuple, resolves scan_args on
- * the inner (right) SCAN, resets its cursor, and probes it. Works for
- * both stored relations (full re-scan of inner) and ephemeral relations
- * (O(1) membership probe when all args are bound).
- * - TAKE : passes at most take_limit tuples through, then returns nullptr. Use
- * this to bound scans over AlephZero ephemeral relations.
- * - PROJECT: filters each input tuple to the named attributes in project_attrs.
- */
- struct PlanNode {
- Operation op;
- PlanNode* left = nullptr;
- PlanNode* right = nullptr;
-
- /**
- * SCAN: pre-opened cursor. For ephemeral relations this cursor is
- * initially exhausted; the JOIN resets it with resolved args before
- * each probe.
- */
- CursorManager::cursor* scan_cursor = nullptr;
-
- /**
- * SCAN: argument template for parameterized relations.
- * Var entries are resolved from the outer JOIN tuple; Const entries
- * are used as-is. The resolved values are written into scan_cursor->args
- * by the JOIN before it calls Next() on this node.
- */
- std::vector<PathArg> scan_args;
-
- /** TAKE: maximum number of tuples to emit before returning nullptr. */
- std::size_t take_limit = 0;
- std::size_t take_count = 0; /**< Runtime counter; mutable during execution. */
-
- /** PROJECT: unordered set of attribute names to keep. */
- std::unordered_set<std::string> project_attrs;
- std::optional<Tuple> project_buffer;
-
- /**
- * JOIN runtime state.
- *
- * join_left holds the current outer tuple while probing the inner
- * (right) side. The pointer is stable for the lifetime of one outer
- * iteration because Next(left) is only called when join_left is reset
- * to nullptr, which happens only after the inner side is exhausted.
- *
- * join_buffer holds the merged output tuple and is overwritten on each
- * successful join. Callers must consume the returned pointer before
- * calling Next() again — same contract as a cursor page pointer.
- */
- Tuple* join_left = nullptr;
- std::optional<Tuple> join_buffer;
- };
-
- class VM {
- public:
- // TODO: CursorManager should be injected as a shared runtime instance,
- // matching the pattern flagged for HandlerManager and LifecycleManager.
- //
- // A single CursorManager instance supports multiple simultaneous cursors —
- // all iteration state lives in each cursor struct, not in the manager.
- // A JOIN plan, for example, holds two independent cursors (one per SCAN leaf)
- // and both are driven through the same CursorManager reference.
- explicit VM(CursorManager& cursors);
-
- /**
- * @brief Pulls the next tuple from the plan tree (Tarski/FOL core).
- *
- * Propagates a Next() call down the operator tree:
- * - SCAN delegates to CursorManager::Next.
- * - JOIN drives the nested-loop: for each outer tuple it resolves
- * scan_args on the inner SCAN, resets the inner cursor, and
- * probes until a match is found or the inner is exhausted.
- * - TAKE counts emitted tuples and returns nullptr once the limit
- * is reached.
- * - PROJECT filters an input tuple to the requested attribute names.
- *
- * @param node Root of the plan subtree to evaluate.
- * @return Next matching tuple, or nullptr when the plan is exhausted.
- */
- Tuple* Next(PlanNode* node);
-
- private:
- CursorManager& cursors_;
-
- /**
- * Resolves a scan_args template against an outer tuple, producing the
- * concrete args vector to write into the inner cursor before probing.
- */
- static std::vector<std::string> ResolveArgs(const std::vector<PathArg>& tmpl, Tuple* from);
-
- /** Resets the inner cursor state and writes resolved args into it. */
- static void ResetInner(CursorManager::cursor* c, std::vector<std::string> args);
-
- /** Merges left and right tuple attributes into node->join_buffer. */
- static Tuple* MergeInto(PlanNode* node, Tuple* left, Tuple* right);
- };
-} // namespace nt
diff --git a/src/CursorManager.cpp b/src/CursorManager.cpp
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
diff --git a/test/C_API.cpp b/test/C_API.cpp
deleted file mode 100644
index 892ed24..0000000
--- a/test/C_API.cpp
+++ /dev/null
@@ -1,415 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "RNT_C_API.h"
-
-#include <cstring>
-#include <string>
-
-// ---------------------------------------------------------------------------
-// End-to-end C API tests covering step 2 (Branch hash-pointer), step 3+4
-// (copy-on-write writes and resolver wiring through rnt_open_handle), and
-// step 5 (session lifecycle and overrides).
-//
-// The C API holds a single process-global runtime; rnt_init is idempotent.
-// Each test uses unique branch and session identifiers so state from one
-// test does not affect another.
-// ---------------------------------------------------------------------------
-
-namespace {
- struct InitGuard {
- InitGuard() {
- REQUIRE(rnt_init("memory", nullptr) == 0);
- }
- };
-
- std::string take_string(char* p) {
- std::string s = p ? p : "";
- rnt_free_string(p);
- return s;
- }
-} // namespace
-
-// ---------------------------------------------------------------------------
-// Step 2 — Branch hash-pointer model
-// ---------------------------------------------------------------------------
-
-TEST_CASE("rnt_register_branch with empty target creates an unborn branch",
- "[step2][capi][branch]") {
- InitGuard _;
- const char* path = "/system/branches/test_step2_unborn";
- REQUIRE(rnt_register_branch(path, "") == 0);
-
- rnt_handle_t h = rnt_open_handle(path, nullptr);
- REQUIRE(h != nullptr);
-
- char* target = nullptr;
- REQUIRE(rnt_branch_target(h, &target) == 0);
- REQUIRE(take_string(target) == "");
-
- REQUIRE(rnt_close_handle(h) == 0);
-}
-
-TEST_CASE("rnt_register_branch rejects an unregistered target hash", "[step2][capi][branch]") {
- InitGuard _;
- REQUIRE(rnt_register_branch(
- "/system/branches/test_step2_bad_init",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-}
-
-TEST_CASE("rnt_branch_advance rejects an unregistered target hash", "[step2][capi][branch]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step2_bad_advance";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
-
- REQUIRE(rnt_branch_advance(
- bpath, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-
- // Target unchanged.
- rnt_handle_t h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(h, &target) == 0);
- REQUIRE(take_string(target) == "");
- rnt_close_handle(h);
-}
-
-TEST_CASE("Writing a tuple advances the branch to a new snapshot", "[step2][step3][capi][branch]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step2_advance";
- const char* rpath = "/system/branches/test_step2_advance/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // After register_relation, the branch is bound to the first snapshot.
- rnt_handle_t h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* first = nullptr;
- REQUIRE(rnt_branch_target(h, &first) == 0);
- const std::string first_hash = take_string(first);
- REQUIRE(first_hash.size() == 64);
- rnt_close_handle(h);
-
- // A tuple write advances the branch again.
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
- rnt_free_string(tuple_hash);
-
- h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* second = nullptr;
- REQUIRE(rnt_branch_target(h, &second) == 0);
- const std::string second_hash = take_string(second);
- rnt_close_handle(h);
-
- REQUIRE(second_hash.size() == 64);
- REQUIRE(second_hash != first_hash);
-}
-
-// ---------------------------------------------------------------------------
-// Step 3+4 — Copy-on-write writes and resolver wiring
-// ---------------------------------------------------------------------------
-
-TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it", "[step3][capi][cow]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step3_link";
- const char* rpath = "/system/branches/test_step3_link/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // Empty relation: root is "".
- char* root = nullptr;
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\nname=alpha\n", &tuple_hash) == 0);
- REQUIRE(tuple_hash != nullptr);
- rnt_free_string(tuple_hash);
-
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- const std::string new_root = take_string(root);
- REQUIRE(new_root.size() == 64); // a real Merkle root now
-}
-
-TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots", "[step3][capi][cow]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step3_unlink";
- const char* rpath = "/system/branches/test_step3_unlink/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- const std::string tuple_hash = take_string(th);
-
- char* root = nullptr;
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) != "");
-
- REQUIRE(rnt_unlink_tuple(rpath, tuple_hash.c_str()) == 0);
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-
- // After re-link and clear, root is empty again.
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
- REQUIRE(rnt_clear_relation(rpath) == 0);
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-}
-
-TEST_CASE("rnt_open_handle resolves branch-relative paths to snapshot relations",
- "[step4][capi][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step4_resolve";
- const char* rpath = "/system/branches/test_step4_resolve/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
- rnt_free_string(tuple_hash);
-
- // Opening the branch-relative path resolves to the snapshot-bound entry,
- // and a cursor reads the tuple just written.
- rnt_handle_t h = rnt_open_handle(rpath, nullptr);
- REQUIRE(h != nullptr);
- rnt_cursor_t c = rnt_cursor_open(h);
- REQUIRE(c != nullptr);
-
- char* row = nullptr;
- REQUIRE(rnt_cursor_next(c, &row) == 1);
- REQUIRE(row != nullptr);
- rnt_free_string(row);
-
- // Exhausted on the next call.
- REQUIRE(rnt_cursor_next(c, &row) == 0);
- REQUIRE(row == nullptr);
-
- rnt_cursor_close(c);
- rnt_close_handle(h);
-}
-
-TEST_CASE("Reading an unborn branch's relation fails cleanly", "[step4][capi][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step4_unborn";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
-
- // No relation registered: relation_root miss returns nonzero.
- char* root = nullptr;
- REQUIRE(
- rnt_relation_root("/system/branches/test_step4_unborn/multigroups/public/relations/missing",
- &root) != 0);
-}
-
-// ---------------------------------------------------------------------------
-// Step 5 — Session lifecycle and overrides
-// ---------------------------------------------------------------------------
-
-TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash", "[step5][capi][session]") {
- InitGuard _;
- char* a = nullptr;
- char* b = nullptr;
- REQUIRE(rnt_session_open(nullptr, &a) == 0);
- REQUIRE(rnt_session_open(nullptr, &b) == 0);
- const std::string ha = take_string(a);
- const std::string hb = take_string(b);
- REQUIRE(ha.size() == 64);
- REQUIRE(hb.size() == 64);
- REQUIRE(ha != hb);
-
- REQUIRE(rnt_session_close(ha.c_str()) == 0);
- REQUIRE(rnt_session_close(hb.c_str()) == 0);
-}
-
-TEST_CASE("rnt_session_close on an unknown hash fails", "[step5][capi][session]") {
- InitGuard _;
- REQUIRE(rnt_session_close("0000000000000000000000000000000000000000000000000000000000000000") !=
- 0);
-}
-
-TEST_CASE("Session override redirects branch-relative reads to a different snapshot",
- "[step5][capi][session][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step5_override";
- const char* rpath = "/system/branches/test_step5_override/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // Snapshot S1: one tuple.
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- rnt_free_string(th);
-
- // Capture S1's hash and pin it via a session override before the next
- // commit — without the override the next link_tuple would cascade-collect
- // S1 as soon as the branch advances away from it.
- rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
- REQUIRE(bh != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(bh, &target) == 0);
- const std::string s1 = take_string(target);
- rnt_close_handle(bh);
-
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_override", s1.c_str()) == 0);
-
- // Advance the branch to S2 — global HEAD moves on; the session override
- // keeps S1 resident.
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
-
- // Session view: still S1 (one tuple).
- const std::string spath = "/system/sessions/" + sid +
- "/branches/test_step5_override/multigroups/public/relations/items";
-
- rnt_handle_t h_session = rnt_open_handle(spath.c_str(), nullptr);
- REQUIRE(h_session != nullptr);
- rnt_cursor_t c_session = rnt_cursor_open(h_session);
- REQUIRE(c_session != nullptr);
-
- int rows_under_session = 0;
- char* row = nullptr;
- while (rnt_cursor_next(c_session, &row) == 1) {
- ++rows_under_session;
- rnt_free_string(row);
- }
- REQUIRE(rows_under_session == 1);
-
- rnt_cursor_close(c_session);
- rnt_close_handle(h_session);
-
- // Global view sees both tuples.
- rnt_handle_t h_global = rnt_open_handle(rpath, nullptr);
- REQUIRE(h_global != nullptr);
- rnt_cursor_t c_global = rnt_cursor_open(h_global);
- REQUIRE(c_global != nullptr);
- int rows_global = 0;
- while (rnt_cursor_next(c_global, &row) == 1) {
- ++rows_global;
- rnt_free_string(row);
- }
- REQUIRE(rows_global == 2);
- rnt_cursor_close(c_global);
- rnt_close_handle(h_global);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-TEST_CASE("Clearing a session override falls back to the global branch HEAD",
- "[step5][capi][session][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step5_clear";
- const char* rpath = "/system/branches/test_step5_clear/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- rnt_free_string(th);
- rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
- REQUIRE(bh != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(bh, &target) == 0);
- const std::string s1 = take_string(target);
- rnt_close_handle(bh);
-
- // Same pattern as above: install the override on S1 before advancing.
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", s1.c_str()) == 0);
-
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
-
- // Clear the override.
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", "") == 0);
-
- // Now the session sees the global view (two tuples).
- const std::string spath =
- "/system/sessions/" + sid + "/branches/test_step5_clear/multigroups/public/relations/items";
- rnt_handle_t h = rnt_open_handle(spath.c_str(), nullptr);
- REQUIRE(h != nullptr);
- rnt_cursor_t c = rnt_cursor_open(h);
- REQUIRE(c != nullptr);
- int rows = 0;
- char* row = nullptr;
- while (rnt_cursor_next(c, &row) == 1) {
- ++rows;
- rnt_free_string(row);
- }
- REQUIRE(rows == 2);
- rnt_cursor_close(c);
- rnt_close_handle(h);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot", "[step5][capi][session]") {
- InitGuard _;
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
-
- REQUIRE(rnt_session_set_branch(
- sid.c_str(), "anything",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-// ---------------------------------------------------------------------------
-// Multi-mg-per-branch — sibling multigroups under one branch evolve
-// independently. Inserting into one mg must not perturb the other's hash
-// or its enumerable relation set.
-// ---------------------------------------------------------------------------
-
-TEST_CASE("Two multigroups under one branch advance independently", "[capi][multi-mg]") {
- InitGuard _;
- const std::string bpath = "/system/branches/test_multi_mg";
- REQUIRE(rnt_register_branch(bpath.c_str(), "") == 0);
-
- // Register one relation in each of two distinct multigroups.
- REQUIRE(rnt_register_relation((bpath + "/multigroups/warehouse/relations/orders").c_str()) ==
- 0);
- REQUIRE(rnt_register_relation((bpath + "/multigroups/audit/relations/events").c_str()) == 0);
-
- char* mgs_raw = nullptr;
- REQUIRE(rnt_list_branch_multigroups(bpath.c_str(), &mgs_raw) == 0);
- const std::string mgs = take_string(mgs_raw);
- REQUIRE(mgs.find("warehouse\t") != std::string::npos);
- REQUIRE(mgs.find("audit\t") != std::string::npos);
-
- // Insert into warehouse; record audit's root before/after.
- char* audit_before = nullptr;
- REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_before) == 0);
- const std::string audit_before_s = take_string(audit_before);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple((bpath + "/multigroups/warehouse/relations/orders").c_str(), "id=1\n",
- &th) == 0);
- rnt_free_string(th);
-
- char* audit_after = nullptr;
- REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_after) == 0);
- const std::string audit_after_s = take_string(audit_after);
-
- // audit's relation set is byte-identical: the warehouse-side mutation
- // touched only the warehouse subtree at the multigroup level.
- REQUIRE(audit_before_s == audit_after_s);
-
- // warehouse/orders is now non-empty.
- char* root = nullptr;
- REQUIRE(rnt_relation_root((bpath + "/multigroups/warehouse/relations/orders").c_str(), &root) ==
- 0);
- REQUIRE(!take_string(root).empty());
-
- // audit/events still empty.
- char* root2 = nullptr;
- REQUIRE(rnt_relation_root((bpath + "/multigroups/audit/relations/events").c_str(), &root2) ==
- 0);
- REQUIRE(take_string(root2).empty());
-}
diff --git a/test/CursorManager.cpp b/test/CursorManager.cpp
deleted file mode 100644
index c823ec5..0000000
--- a/test/CursorManager.cpp
+++ /dev/null
@@ -1,152 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#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 "TupleCodec.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 6 / cursor-independence follow-up
-//
-// Two things to verify:
-// 1. A cursor opened on a /system/snapshots/<H>/relations/<n> relation
-// pins the parent Multigroup for its lifetime; releasing the cursor
-// releases the pin.
-// 2. Closing the handle a cursor was opened from no longer dereferences
-// a freed handle pointer inside Next() — the cursor captures the type
-// label and the ephemeral generator at Open time, so iteration after
-// the handle's death is degenerate but safe.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-
- std::unique_ptr<nt::ObjectManager::ephemeral_object_type>
- make_constant_eph_type(std::vector<nt::Attribute> attrs) {
- auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->methods = {OPEN, CLOSE};
- t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::Finite;
- t->generator = [attrs](const std::vector<std::string>&, std::size_t offset,
- std::size_t limit) -> std::vector<nt::Tuple> {
- if (offset > 0 || limit == 0)
- return {};
- return {nt::Tuple(attrs)};
- };
- return t;
- }
-
- struct Stack {
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles{objects, backend};
- nt::NamespaceReferenceManager references{objects, backend};
- nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
- nt::CursorManager cursors{backend, &lifecycles, &objects};
- };
-} // namespace
-
-TEST_CASE("Cursor pins the parent snapshot and unpins on close", "[step6][cursor][pin]") {
- Stack s;
-
- // Stand up a minimal snapshot with one relation.
- auto& om = s.objects;
- om.Register({"system", "snapshots", "Hcur"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- om.Register({"system", "snapshots", "Hcur", "relations", "items"},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
-
- auto* mg_entry = om.Find({"system", "snapshots", "Hcur"});
- auto* rel_entry = om.Find({"system", "snapshots", "Hcur", "relations", "items"});
-
- // Parent's pin on the child Relation (mirrors register_snapshot in the C
- // API). An external pin on the Multigroup simulates a branch holding the
- // snapshot — without it, closing the cursor would drop ref_count to 0 and
- // cascade-collect mg_entry mid-test.
- s.lifecycles.Pin(rel_entry);
- s.lifecycles.Pin(mg_entry);
-
- // Open a handle and a cursor on the child Relation.
- int conn = 1;
- auto* h = s.handler.Open({"system", "snapshots", "Hcur", "relations", "items"}, &conn);
- REQUIRE(h != nullptr);
- REQUIRE(mg_entry->head->reference_count == 1); // external pin only
-
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
- REQUIRE(c->pinned_snapshot == mg_entry);
- REQUIRE(mg_entry->head->reference_count == 2); // external + cursor pin
-
- s.cursors.Close(c);
- REQUIRE(mg_entry->head->reference_count == 1); // back to external only
-
- s.handler.Close(h);
- s.lifecycles.Unpin(mg_entry); // releases the external pin; cascade-collects.
-}
-
-TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
- "[step6][cursor][handle-independence]") {
- Stack s;
- // Register an ephemeral relation; its generator yields one tuple.
- s.objects.Register({"eph"}, std::make_unique<nt::ObjectManager::Relation>(),
- make_constant_eph_type({{"k", "v"}}));
-
- int conn = 1;
- auto* h = s.handler.Open({"eph"}, &conn);
- REQUIRE(h != nullptr);
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
-
- // Ephemeral cursors start exhausted; reset to simulate a JOIN probe.
- c->exhausted = false;
- c->args = {};
-
- // Now violate the documented contract: close the handle BEFORE Next().
- // The cursor must still produce its tuple from the captured generator
- // without dereferencing the freed handle.
- s.handler.Close(h);
-
- auto* tuple = s.cursors.Next(c);
- REQUIRE(tuple != nullptr);
- bool found = false;
- for (const auto& a : tuple->attrs())
- if (a.name == "k" && a.value == "v") {
- found = true;
- break;
- }
- REQUIRE(found);
-
- s.cursors.Close(c);
-}
-
-TEST_CASE("Cursor opened against a non-snapshot relation does not pin anything",
- "[step6][cursor][pin]") {
- Stack s;
- s.objects.Register({"plain", "rel"}, std::make_unique<nt::ObjectManager::Relation>(),
- make_type(RELATION));
- int conn = 1;
- auto* h = s.handler.Open({"plain", "rel"}, &conn);
- REQUIRE(h != nullptr);
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
- REQUIRE(c->pinned_snapshot == nullptr);
-
- s.cursors.Close(c);
- s.handler.Close(h);
-}
diff --git a/test/LifecycleManager.cpp b/test/LifecycleManager.cpp
deleted file mode 100644
index 3712522..0000000
--- a/test/LifecycleManager.cpp
+++ /dev/null
@@ -1,169 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "LifecycleManager.h"
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 6 — Pin/Unpin and the cascade
-//
-// LifecycleManager owns object lifetime. Pin/Unpin adjust reference_count;
-// Monitor/Unmonitor adjust handle_count. Both decrement paths call into
-// TryCollect, which gates GC on the joint condition (both counters zero)
-// and, for MULTIGROUP entries, cascades through CascadeMultigroup to
-// unpin every Relation registered under their snapshot subtree.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-} // namespace
-
-TEST_CASE("Pin and Unpin adjust reference_count symmetrically", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
- REQUIRE(entry->head->reference_count == 0);
-
- lm.Pin(entry);
- lm.Pin(entry);
- REQUIRE(entry->head->reference_count == 2);
-
- lm.Unpin(entry);
- REQUIRE(entry->head->reference_count == 1);
-
- // Still alive at ref_count=1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-}
-
-TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- REQUIRE_NOTHROW(lm.Pin(nullptr));
- REQUIRE_NOTHROW(lm.Unpin(nullptr));
-}
-
-TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Unpin(entry);
- REQUIRE(entry->head->reference_count == 0);
-}
-
-TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Monitor(entry);
- lm.Pin(entry);
-
- // handle_count=1, ref_count=1.
- lm.Unpin(entry); // ref_count → 0, but handle_count still 1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-
- lm.Pin(entry);
- lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-
- lm.Unpin(entry); // both zero → GC.
- REQUIRE(om.Find({"r1"}) == nullptr);
-}
-
-TEST_CASE("Unmonitor that brings both counters to zero collects the entry",
- "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Monitor(entry);
- REQUIRE(entry->head->handle_count == 1);
- lm.Unmonitor(entry);
- REQUIRE(om.Find({"r1"}) == nullptr);
-}
-
-TEST_CASE("IsEligibleForGC reflects the joint-zero rule", "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- REQUIRE(lm.IsEligibleForGC(entry));
- lm.Pin(entry);
- REQUIRE_FALSE(lm.IsEligibleForGC(entry));
-}
-
-TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection",
- "[step6][lifecycle][cascade]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
-
- om.Register({"system", "snapshots", "H1"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- auto* mg_entry = om.Find({"system", "snapshots", "H1"});
-
- for (const auto& n : {std::string{"foo"}, std::string{"bar"}}) {
- om.Register({"system", "snapshots", "H1", "relations", n},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- // The parent Multigroup's pin on each child, mirroring what
- // register_snapshot does in the C API layer.
- lm.Pin(om.Find({"system", "snapshots", "H1", "relations", n}));
- }
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) != nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) != nullptr);
-
- // One external pin on the Multigroup (e.g. a branch). Release it: cascade.
- lm.Pin(mg_entry);
- lm.Unpin(mg_entry);
-
- REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr);
-}
-
-TEST_CASE("Cascade survives a child with an open handle", "[step6][lifecycle][cascade]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
-
- om.Register({"system", "snapshots", "H2"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- auto* mg_entry = om.Find({"system", "snapshots", "H2"});
-
- om.Register({"system", "snapshots", "H2", "relations", "live"},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* child = om.Find({"system", "snapshots", "H2", "relations", "live"});
- lm.Pin(child); // parent's pin
- lm.Monitor(child); // simulate an open handle on the child
-
- // Drop the Multigroup's external ref: cascade unpins child (ref → 0),
- // but handle_count is still 1 — child must survive.
- lm.Pin(mg_entry);
- lm.Unpin(mg_entry);
-
- REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr);
-
- // Closing the handle now releases the child.
- lm.Unmonitor(child);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr);
-}
diff --git a/test/MultigroupCodec.cpp b/test/MultigroupCodec.cpp
deleted file mode 100644
index b04c405..0000000
--- a/test/MultigroupCodec.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "MultigroupCodec.h"
-
-#include <string>
-#include <vector>
-
-// ---------------------------------------------------------------------------
-// Multigroup snapshot — Merkle<string> tree mapping relation name → relation root.
-//
-// The codec is a thin facade over Merkle<std::string>; these tests confirm
-// the tree is order-independent (it sorts intrinsically), that mutations are
-// path-localised (sibling entries are untouched), and that snapshots can be
-// re-enumerated from a cold backend by hash alone.
-// ---------------------------------------------------------------------------
-
-namespace {
-
- // Pad a short label into a deterministic 64-char hex string so the codec can
- // decode it as a 32-byte payload.
- std::string mk_root(char c, char d = '0') {
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
- }
-
-} // namespace
-
-TEST_CASE("Build is order-independent - tree sorts intrinsically", "[multigroup-codec]") {
- nt::InMemoryBackend a, b;
-
- std::vector<nt::MultigroupCodec::RelationEntry> in_order = {
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
- {"charlie", mk_root('c')},
- };
- std::vector<nt::MultigroupCodec::RelationEntry> shuffled = {
- {"charlie", mk_root('c')},
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
- };
-
- REQUIRE(nt::MultigroupCodec::Build(a, in_order) == nt::MultigroupCodec::Build(b, shuffled));
-}
-
-TEST_CASE("Build distinguishes content changes", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> base = {{"foo", mk_root('a')}};
- std::vector<nt::MultigroupCodec::RelationEntry> different_root = {{"foo", mk_root('b')}};
- std::vector<nt::MultigroupCodec::RelationEntry> different_name = {{"bar", mk_root('a')}};
- std::vector<nt::MultigroupCodec::RelationEntry> extra_member = {{"foo", mk_root('a')},
- {"bar", mk_root('b')}};
-
- const auto h_base = nt::MultigroupCodec::Build(backend, base);
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_root));
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_name));
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, extra_member));
-}
-
-TEST_CASE("Build then List round-trips entries in key-sorted order", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a', '1')},
- {"bar", mk_root('b', '2')},
- {"baz", mk_root('c', '3')},
- };
- const auto root = nt::MultigroupCodec::Build(backend, entries);
-
- auto listed = nt::MultigroupCodec::List(backend, root);
- REQUIRE(listed.size() == 3);
- REQUIRE(listed[0].first == "bar");
- REQUIRE(listed[1].first == "baz");
- REQUIRE(listed[2].first == "foo");
- REQUIRE(listed[0].second == mk_root('b', '2'));
-}
-
-TEST_CASE("Lookup returns payload by name", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root = nt::MultigroupCodec::Build(backend, entries);
-
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "foo") == mk_root('a'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "bar") == mk_root('b'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "missing").empty());
-}
-
-TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root0 = nt::MultigroupCodec::Build(backend, entries);
-
- // Mutate foo only; bar's payload must round-trip unchanged.
- const auto root1 = nt::MultigroupCodec::InsertOne(backend, root0, "foo", mk_root('d'));
- REQUIRE(root1 != root0);
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo") == mk_root('d'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
-}
-
-TEST_CASE("RemoveOne deletes one entry; sibling preserved", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root0 = nt::MultigroupCodec::Build(backend, entries);
-
- const auto root1 = nt::MultigroupCodec::RemoveOne(backend, root0, "foo");
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo").empty());
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
-}
-
-TEST_CASE("Empty multigroup builds to an empty root", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
- std::vector<nt::MultigroupCodec::RelationEntry> empty;
- REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty());
- REQUIRE(nt::MultigroupCodec::List(backend, "").empty());
-}
diff --git a/test/NamespaceReferenceManager.cpp b/test/NamespaceReferenceManager.cpp
deleted file mode 100644
index 90e65e5..0000000
--- a/test/NamespaceReferenceManager.cpp
+++ /dev/null
@@ -1,177 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "Merkle.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Resolver — two-level reparse through the branch tree.
-//
-// NamespaceReferenceManager::Resolve rewrites
-// /system/branches/<name>/multigroups/<mg>/<sub>...
-// into
-// /system/snapshots/<mg_hash>/<sub>...
-// by reading branch.target_hash (a Merkle<string> root) from storage and
-// looking up <mg>. Session paths consult the per-session override map first
-// and fall back to the global branch when no override is set.
-// ---------------------------------------------------------------------------
-
-namespace {
-
- using Path = std::vector<std::string>;
-
- // Pad a label into a deterministic 64-char hex string so the merkle codec can
- // decode it as a 32-byte payload.
- std::string mk_hash(char c, char d = '0') {
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
- }
-
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-
- void register_branch(nt::ObjectManager& om, const std::string& name,
- const std::string& target_hash) {
- auto br = std::make_unique<nt::ObjectManager::Branch>();
- br->name = name;
- br->target_hash = target_hash;
- om.Register({"system", "branches", name}, std::move(br), make_type(BRANCH));
- }
-
- void register_session(nt::ObjectManager& om, const std::string& id,
- std::map<std::string, std::string> overrides = {}) {
- auto s = std::make_unique<nt::ObjectManager::Session>();
- s->branch_overrides = std::move(overrides);
- om.Register({"system", "sessions", id}, std::move(s), make_type(SESSION));
- }
-
- // Builds a branch tree with a single (mg_name → mg_hash) entry. Returns the
- // branch-tree root hex.
- std::string build_branch_tree(nt::InMemoryBackend& store, const std::string& mg_name,
- const std::string& mg_hash_hex) {
- return nt::Merkle<std::string>::Insert(store, "", mg_name, nt::hex_to_bin(mg_hash_hex));
- }
-
-} // namespace
-
-TEST_CASE("Branch sub-path rewrites through the branch tree to the mg snapshot", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_hash = mk_hash('a');
- const std::string tree = build_branch_tree(store, "warehouse", mg_hash);
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "branches", "main", "multigroups", "warehouse", "relations",
- "items"}) == Path{"system", "snapshots", mg_hash, "relations", "items"});
-}
-
-TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an unborn branch leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- register_branch(om, "main", ""); // unborn
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main", "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main", "multigroups", "audit", "relations", "events"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "ghost", "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Non-branch paths pass through unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"relations", "anywhere"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Session override takes precedence over the global branch HEAD", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_global = mk_hash('a');
- const std::string mg_session = mk_hash('b');
- const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
- const std::string tree_session = build_branch_tree(store, "warehouse", mg_session);
-
- register_branch(om, "main", tree_global);
- register_session(om, "sess123", {{"main", tree_session}});
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
- "warehouse", "relations", "items"}) ==
- Path{"system", "snapshots", mg_session, "relations", "items"});
-}
-
-TEST_CASE("Session resolution falls back to global branch when override is absent", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_global = mk_hash('a');
- const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
-
- register_branch(om, "main", tree_global);
- register_session(om, "sess123"); // no overrides
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
- "warehouse", "relations", "items"}) ==
- Path{"system", "snapshots", mg_global, "relations", "items"});
-}
-
-TEST_CASE("Session resolution with neither override nor global branch is unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- register_session(om, "sess123");
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "sessions", "sess123", "branches", "main",
- "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Empty input path is returned unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({}).empty());
-}
diff --git a/test/ObjectManager.cpp b/test/ObjectManager.cpp
deleted file mode 100644
index f90de3a..0000000
--- a/test/ObjectManager.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 5 — Unregister
-//
-// Sessions are disposable and must be removable from the registry on close.
-// Unregister is the splice-out primitive that rnt_session_close, the
-// LifecycleManager TryCollect path, and any future explicit-delete API all
-// share.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> session_type() {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = SESSION;
- t->disposable = true;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-} // namespace
-
-TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"system", "sessions", "alpha"}, std::make_unique<nt::ObjectManager::Session>(),
- session_type());
- REQUIRE(om.Find({"system", "sessions", "alpha"}) != nullptr);
-
- REQUIRE(om.Unregister({"system", "sessions", "alpha"}) == true);
- REQUIRE(om.Find({"system", "sessions", "alpha"}) == nullptr);
-}
-
-TEST_CASE("Unregister returns false for missing paths", "[step5][object-manager]") {
- nt::ObjectManager om;
- REQUIRE(om.Unregister({"never", "registered"}) == false);
-}
-
-TEST_CASE("Unregister splices a middle entry without disturbing neighbours",
- "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"a"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"b"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"c"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
-
- REQUIRE(om.Unregister({"b"}) == true);
-
- REQUIRE(om.Find({"a"}) != nullptr);
- REQUIRE(om.Find({"b"}) == nullptr);
- REQUIRE(om.Find({"c"}) != nullptr);
-}
-
-TEST_CASE("Unregister at the head of the list works", "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"first"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"second"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
-
- // The most recently registered entry is the list head (LIFO).
- REQUIRE(om.Unregister({"second"}) == true);
- REQUIRE(om.Find({"second"}) == nullptr);
- REQUIRE(om.Find({"first"}) != nullptr);
-
- // And the new head is removable too.
- REQUIRE(om.Unregister({"first"}) == true);
- REQUIRE(om.Find({"first"}) == nullptr);
-}
diff --git a/test/VM.cpp b/test/VM.cpp
deleted file mode 100644
index 7e61da6..0000000
--- a/test/VM.cpp
+++ /dev/null
@@ -1,254 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "CursorManager.h"
-#include "HandlerManager.h"
-#include "IdentityManager.h"
-#include "InMemoryBackend.h"
-#include "LifecycleManager.h"
-#include "Merkle.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "TupleCodec.h"
-#include "VM.h"
-
-#include <algorithm>
-#include <string>
-#include <vector>
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-// Stores a tuple in the backend and updates the Relation object's Merkle root.
-static void store_tuple(nt::InMemoryBackend& backend, nt::ObjectManager& objects,
- const std::vector<std::string>& path, std::vector<nt::Attribute> attrs) {
- auto bytes = nt::TupleCodec::Serialize(attrs);
- auto hash = backend.Put(std::move(bytes));
-
- auto* entry = objects.Find(path);
- if (!entry)
- return;
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(entry->object.get());
- if (!rel)
- return;
- auto hash_bin = nt::hex_to_bin(hash);
- rel->merkle_root =
- nt::Merkle<nt::Hash32>::Insert(backend, rel->merkle_root, hash_bin, hash_bin);
-}
-
-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};
- return t;
-}
-
-// Generator for eq[A, B]: returns one tuple when A == B, nothing otherwise.
-// Treats both args as integers when parseable, strings otherwise.
-static std::unique_ptr<nt::ObjectManager::ephemeral_object_type> make_eq_type() {
- auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->disposable = false;
- t->methods = {OPEN, CLOSE};
- t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::AlephZero;
- t->generator = [](const std::vector<std::string>& args, std::size_t offset,
- std::size_t limit) -> std::vector<nt::Tuple> {
- if (args.size() < 2 || offset > 0 || limit == 0)
- return {};
- if (args[0] != args[1])
- return {};
- return {nt::Tuple({{"left", args[0]}, {"right", args[1]}})};
- };
- return t;
-}
-
-// ---------------------------------------------------------------------------
-// Fixture: shared manager stack
-// ---------------------------------------------------------------------------
-
-struct Fixture {
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles{objects, backend};
- nt::NamespaceReferenceManager references{objects, backend};
- nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
- nt::CursorManager cursors{backend, &lifecycles, &objects};
- int conn = 1;
-};
-
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
-TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "villager"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}});
- store_tuple(f.backend, f.objects, path, {{"name", "Peter"}, {"age", "25"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode plan;
- plan.op = nt::FOL_OPERATION_SCAN;
- plan.scan_cursor = cursor;
-
- nt::VM vm(f.cursors);
- int count = 0;
- while (vm.Next(&plan))
- ++count;
-
- REQUIRE(count == 2);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}
-
-TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]") {
- Fixture f;
- const std::vector<std::string> people_path = {"relations", "people"};
- const std::vector<std::string> eq_path = {"builtins", "eq"};
-
- // Register stored relation: people(name, age)
- f.objects.Register(people_path, std::make_unique<nt::ObjectManager::Relation>(),
- make_relation_type());
-
- store_tuple(f.backend, f.objects, people_path, {{"name", "John"}, {"age", "30"}});
- store_tuple(f.backend, f.objects, people_path, {{"name", "Peter"}, {"age", "25"}});
- store_tuple(f.backend, f.objects, people_path, {{"name", "Jude"}, {"age", "25"}});
-
- // Register ephemeral eq relation
- f.objects.Register(eq_path, std::make_unique<nt::ObjectManager::Relation>(), make_eq_type());
-
- // Open handles and cursors
- auto* people_handle = f.handler.Open(people_path, &f.conn);
- REQUIRE(people_handle != nullptr);
- auto* eq_handle = f.handler.Open(eq_path, &f.conn);
- REQUIRE(eq_handle != nullptr);
-
- auto* people_rel =
- dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(people_path)->object.get());
- auto* people_cursor = f.cursors.Open(people_handle, people_rel->merkle_root);
- REQUIRE(people_cursor != nullptr);
- auto* eq_cursor = f.cursors.Open(eq_handle); // EPHEMERAL: starts exhausted, JOIN resets
- REQUIRE(eq_cursor != nullptr);
-
- // Plan: JOIN( SCAN(people), SCAN(eq[ Var("age"), Const("25") ]) )
- nt::PlanNode right;
- right.op = nt::FOL_OPERATION_SCAN;
- right.scan_cursor = eq_cursor;
- right.scan_args = {nt::PathArg::Var("age"), nt::PathArg::Const("25")};
-
- nt::PlanNode left;
- left.op = nt::FOL_OPERATION_SCAN;
- left.scan_cursor = people_cursor;
-
- nt::PlanNode root;
- root.op = nt::FOL_OPERATION_JOIN;
- root.left = &left;
- root.right = &right;
-
- // Execute and collect matched names
- nt::VM vm(f.cursors);
- std::vector<std::string> matched;
- while (nt::Tuple* t = vm.Next(&root)) {
- for (const auto& attr : t->attrs())
- if (attr.name == "name")
- matched.push_back(attr.value);
- }
-
- // Jude (age=25) and Peter (age=25) should match; Jogn (age=30) should not
- REQUIRE(matched.size() == 2);
- REQUIRE(std::find(matched.begin(), matched.end(), "Jude") != matched.end());
- REQUIRE(std::find(matched.begin(), matched.end(), "Peter") != matched.end());
- REQUIRE(std::find(matched.begin(), matched.end(), "John") == matched.end());
-
- f.cursors.Close(people_cursor);
- f.cursors.Close(eq_cursor);
- f.handler.Close(people_handle);
- f.handler.Close(eq_handle);
-}
-
-TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "items"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"id", "1"}});
- store_tuple(f.backend, f.objects, path, {{"id", "2"}});
- store_tuple(f.backend, f.objects, path, {{"id", "3"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode scan;
- scan.op = nt::FOL_OPERATION_SCAN;
- scan.scan_cursor = cursor;
-
- nt::PlanNode take;
- take.op = nt::FOL_OPERATION_TAKE;
- take.left = &scan;
- take.take_limit = 2;
-
- nt::VM vm(f.cursors);
- int count = 0;
- while (vm.Next(&take))
- ++count;
-
- REQUIRE(count == 2);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}
-
-TEST_CASE("PROJECT filters tuple attributes by name", "[project]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "people"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}, {"city", "Lisbon"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode scan;
- scan.op = nt::FOL_OPERATION_SCAN;
- scan.scan_cursor = cursor;
-
- nt::PlanNode project;
- project.op = nt::FOL_OPERATION_PROJECT;
- project.left = &scan;
- project.project_attrs = {"name", "city"};
-
- nt::VM vm(f.cursors);
- nt::Tuple* t = vm.Next(&project);
- REQUIRE(t != nullptr);
-
- std::vector<std::string> names;
- for (const auto& attr : t->attrs())
- names.push_back(attr.name);
-
- REQUIRE(names.size() == 2);
- REQUIRE(std::find(names.begin(), names.end(), "name") != names.end());
- REQUIRE(std::find(names.begin(), names.end(), "city") != names.end());
- REQUIRE(std::find(names.begin(), names.end(), "age") == names.end());
- REQUIRE(vm.Next(&project) == nullptr);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}