diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/CursorManager.cpp | 64 | ||||
| -rw-r--r-- | src/InMemoryBackend.cpp | 55 | ||||
| -rw-r--r-- | src/RelationalNT.cpp | 37 | ||||
| -rw-r--r-- | src/SqliteBackend.cpp | 123 | ||||
| -rw-r--r-- | src/TupleCodec.cpp | 72 |
5 files changed, 313 insertions, 38 deletions
diff --git a/src/CursorManager.cpp b/src/CursorManager.cpp index f80ac54..6a44767 100644 --- a/src/CursorManager.cpp +++ b/src/CursorManager.cpp @@ -1,50 +1,66 @@ #include "CursorManager.h" - -#include <numeric> +#include "TupleCodec.h" namespace nt { - static std::string JoinPath(const std::vector<std::string>& path) + CursorManager::CursorManager(IStorageBackend& backend) + : backend_(backend) + {} + + void CursorManager::LoadPage(cursor* c, const std::vector<std::string>& path) { - return std::accumulate( - std::next(path.begin()), path.end(), path.front(), - [](std::string a, const std::string& b) { return std::move(a) + "/" + b; }); + auto hashes = backend_.TupleHashes(path, c->fetch_offset, PAGE_SIZE); + c->page.clear(); + c->page_position = 0; + + for (const auto& hash : hashes) + { + auto bytes = backend_.Get(hash); + if (!bytes) continue; + c->page.emplace_back(TupleCodec::Deserialize(*bytes)); + } + + c->fetch_offset += hashes.size(); + if (hashes.size() < PAGE_SIZE) + c->exhausted = true; } CursorManager::cursor* CursorManager::Open(HandlerManager::handle* handle) { if (handle == nullptr || handle->object == nullptr) return nullptr; - if (handle->object->head->type->label != RELATION) return nullptr; - const std::string key = JoinPath(handle->object->head->path); - auto it = mock_store_.find(key); - if (it == mock_store_.end()) - return nullptr; - auto* c = new cursor(); c->handle = handle; - for (const auto& row : it->second) - c->tuples.emplace_back(row); - return c; - } + LoadPage(c, handle->object->head->path); - Tuple* CursorManager::Next(cursor* cursor) - { - if (cursor == nullptr || cursor->position >= cursor->tuples.size()) + if (c->page.empty()) + { + delete c; return nullptr; - return &cursor->tuples[cursor->position++]; + } + return c; } - void CursorManager::Close(cursor* cursor) + Tuple* CursorManager::Next(cursor* c) { - delete cursor; + if (c == nullptr) return nullptr; + + if (c->page_position < c->page.size()) + return &c->page[c->page_position++]; + + if (c->exhausted) return nullptr; + + LoadPage(c, c->handle->object->head->path); + if (c->page.empty()) return nullptr; + + return &c->page[c->page_position++]; } - void CursorManager::MockInsert(std::vector<std::string> path, std::vector<Attribute> tuple) + void CursorManager::Close(cursor* c) { - mock_store_[JoinPath(path)].push_back(std::move(tuple)); + delete c; } } diff --git a/src/InMemoryBackend.cpp b/src/InMemoryBackend.cpp new file mode 100644 index 0000000..4d637fe --- /dev/null +++ b/src/InMemoryBackend.cpp @@ -0,0 +1,55 @@ +#include "InMemoryBackend.h" + +#include <picosha2.h> +#include <numeric> + +namespace nt +{ + std::string InMemoryBackend::PathKey(const std::vector<std::string>& path) + { + return std::accumulate( + std::next(path.begin()), path.end(), path.front(), + [](std::string a, const std::string& b) { return std::move(a) + "/" + b; }); + } + + 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; + } + + void InMemoryBackend::LinkTuple(const std::vector<std::string>& relation_path, + const std::string& tuple_hash) + { + auto& hashes = index_[PathKey(relation_path)]; + for (const auto& h : hashes) + if (h == tuple_hash) return; + hashes.push_back(tuple_hash); + } + + std::vector<std::string> InMemoryBackend::TupleHashes( + const std::vector<std::string>& relation_path, + std::size_t offset, + std::size_t limit) + { + auto it = index_.find(PathKey(relation_path)); + if (it == index_.end() || offset >= it->second.size()) + return {}; + + const auto& all = it->second; + auto begin = all.begin() + static_cast<ptrdiff_t>(offset); + auto end = (offset + limit <= all.size()) + ? begin + static_cast<ptrdiff_t>(limit) + : all.end(); + return { begin, end }; + } +} diff --git a/src/RelationalNT.cpp b/src/RelationalNT.cpp index 8661ffa..e82b8c9 100644 --- a/src/RelationalNT.cpp +++ b/src/RelationalNT.cpp @@ -7,6 +7,8 @@ #include "LifecycleManager.h" #include "ObjectManager.h" #include "PermissionsManager.h" +#include "SqliteBackend.h" +#include "TupleCodec.h" #include "VM.h" #include <iostream> @@ -30,7 +32,7 @@ int main() const nt::NT runtime; std::cout << "RNT running: " << (runtime.IsRunning() ? "yes" : "no") << '\n'; - // --- Mock: register user in the object registry --- + // --- 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>(); @@ -38,25 +40,32 @@ int main() type->disposable = false; type->methods = { OPEN, CLOSE }; - objects.Register({ "multigroups", "sakura", "relations", "user" }, std::make_unique<nt::ObjectManager::Relation>(), std::move(type)); - - // --- Mock: populate the cursor store with three tuples --- - nt::CursorManager cursors; - cursors.MockInsert({ "multigroups", "sakura", "relations", "user" }, { { "name", "Alice" }, { "age", "30" } }); - cursors.MockInsert({ "multigroups", "sakura", "relations", "user" }, { { "name", "Bob" }, { "age", "25" } }); - cursors.MockInsert({ "multigroups", "sakura", "relations", "user"}, {{"name", "Carol"}, {"age", "35"}}); - - // --- Open a handle on user through the full manager pipeline --- + objects.Register({ "multigroups", "sakura", "relations", "villager" }, std::make_unique<nt::ObjectManager::Relation>(), std::move(type)); + + // --- Populate SQLite backend with three tuples --- + nt::SqliteBackend backend; + const std::vector<std::string> path = { "multigroups", "sakura", "relations", "villager" }; + auto store = [&](std::vector<nt::Attribute> attrs) { + auto bytes = nt::TupleCodec::Serialize(attrs); + auto hash = backend.Put(bytes); + backend.LinkTuple(path, hash); + }; + 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; nt::HandlerManager handler(objects, permissions, identities, lifecycles); int connection = 1; // dummy connection context - nt::HandlerManager::handle* handle = handler.Open({ "multigroups", "sakura", "relations", "user" }, &connection); + nt::HandlerManager::handle* handle = handler.Open({ "multigroups", "sakura", "relations", "villager" }, &connection); if (handle == nullptr) { - std::cout << "Failed to open handle for user\n"; + std::cout << "Failed to open handle for villager\n"; return 1; } @@ -64,7 +73,7 @@ int main() nt::CursorManager::cursor* cursor = cursors.Open(handle); if (cursor == nullptr) { - std::cout << "Failed to open cursor for user\n"; + std::cout << "Failed to open cursor for villager\n"; handler.Close(handle); return 1; } @@ -75,7 +84,7 @@ int main() plan.scan_cursor = cursor; nt::VM vm(cursors); - std::cout << "Tuples in user:\n"; + std::cout << "Tuples in villager:\n"; while (nt::Tuple* t = vm.Next(&plan)) { std::cout << " "; diff --git a/src/SqliteBackend.cpp b/src/SqliteBackend.cpp new file mode 100644 index 0000000..4741033 --- /dev/null +++ b/src/SqliteBackend.cpp @@ -0,0 +1,123 @@ +#include "SqliteBackend.h" + +#include <picosha2.h> +#include <numeric> +#include <stdexcept> + +namespace nt +{ + static std::string JoinPath(const std::vector<std::string>& path) + { + return std::accumulate( + std::next(path.begin()), path.end(), path.front(), + [](std::string a, const std::string& b) { return std::move(a) + "/" + b; }); + } + + 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" + ");" + "CREATE TABLE IF NOT EXISTS relation_index (" + " path TEXT NOT NULL," + " tuple_hash TEXT NOT NULL," + " UNIQUE(path, tuple_hash)" + ");"; + + 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::PathKey(const std::vector<std::string>& path) + { + return JoinPath(path); + } + + 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; + } + + void SqliteBackend::LinkTuple(const std::vector<std::string>& relation_path, + const std::string& tuple_hash) + { + const std::string path_key = PathKey(relation_path); + + sqlite3_stmt* stmt = nullptr; + sqlite3_prepare_v2(db_, + "INSERT OR IGNORE INTO relation_index (path, tuple_hash) VALUES (?, ?)", + -1, &stmt, nullptr); + sqlite3_bind_text(stmt, 1, path_key.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, tuple_hash.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + + std::vector<std::string> SqliteBackend::TupleHashes( + const std::vector<std::string>& relation_path, + std::size_t offset, + std::size_t limit) + { + const std::string path_key = PathKey(relation_path); + + sqlite3_stmt* stmt = nullptr; + sqlite3_prepare_v2(db_, + "SELECT tuple_hash FROM relation_index" + " WHERE path = ? ORDER BY rowid LIMIT ? OFFSET ?", + -1, &stmt, nullptr); + sqlite3_bind_text(stmt, 1, path_key.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 2, static_cast<sqlite3_int64>(limit)); + sqlite3_bind_int64(stmt, 3, static_cast<sqlite3_int64>(offset)); + + std::vector<std::string> hashes; + while (sqlite3_step(stmt) == SQLITE_ROW) + hashes.emplace_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0))); + sqlite3_finalize(stmt); + return hashes; + } +} diff --git a/src/TupleCodec.cpp b/src/TupleCodec.cpp new file mode 100644 index 0000000..9b76046 --- /dev/null +++ b/src/TupleCodec.cpp @@ -0,0 +1,72 @@ +#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; + } +} |
