diff options
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | include/VM.h | 11 | ||||
| -rw-r--r-- | src/VM.cpp | 17 | ||||
| -rw-r--r-- | test/VM.cpp | 40 |
5 files changed, 69 insertions, 3 deletions
@@ -0,0 +1 @@ +use flake @@ -1,5 +1,6 @@ .vs build +_build out x64 Debug @@ -32,6 +33,8 @@ RNT.lib libRNT.so libRNT.dylib docs/generated/ +.cache +_build_nix # vcpkg manifest mode artifacts vcpkg_installed diff --git a/include/VM.h b/include/VM.h index 96cf493..3bfe521 100644 --- a/include/VM.h +++ b/include/VM.h @@ -6,6 +6,7 @@ #include <optional> #include <string> +#include <unordered_set> #include <vector> /** @@ -88,12 +89,11 @@ namespace nt * (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. - * - * TODO: Add PROJECT operator as the query layer grows. + * - PROJECT: filters each input tuple to the named attributes in project_attrs. */ struct PlanNode { - enum class Op { SCAN, JOIN, TAKE }; + enum class Op { SCAN, JOIN, TAKE, PROJECT }; Op op; PlanNode* left = nullptr; @@ -118,6 +118,10 @@ namespace nt 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. * @@ -156,6 +160,7 @@ namespace nt * 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. @@ -1,4 +1,5 @@ #include "VM.h" +#include "Types.h" namespace nt { @@ -50,6 +51,22 @@ namespace nt case PlanNode::Op::SCAN: return cursors_.Next(node->scan_cursor); + case PlanNode::Op::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 PlanNode::Op::JOIN: { while (true) diff --git a/test/VM.cpp b/test/VM.cpp index b0a602f..f4ce453 100644 --- a/test/VM.cpp +++ b/test/VM.cpp @@ -221,3 +221,43 @@ TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]") 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::PlanNode::Op::SCAN; + scan.scan_cursor = cursor; + + nt::PlanNode project; + project.op = nt::PlanNode::Op::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); +} |
