diff options
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 @@ -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); -} |
