#include <catch2/catch_test_macros.hpp>
#include "CursorManager.h"
#include "HandlerManager.h"
#include "IdentityManager.h"
#include "InMemoryBackend.h"
#include "LifecycleManager.h"
#include "ObjectManager.h"
#include "PermissionsManager.h"
#include "TupleCodec.h"
#include "VM.h"
#include <algorithm>
#include <string>
#include <vector>
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static void store_tuple(nt::InMemoryBackend& backend,
const std::vector<std::string>& path,
std::vector<nt::Attribute> attrs)
{
auto bytes = nt::TupleCodec::Serialize(attrs);
auto hash = backend.Put(std::move(bytes));
backend.LinkTuple(path, hash);
}
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;
nt::HandlerManager handler { objects, permissions, identities, lifecycles };
nt::CursorManager cursors { backend };
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, path, { { "name", "John" }, { "age", "30" } });
store_tuple(f.backend, path, { { "name", "Peter" }, { "age", "25" } });
auto* handle = f.handler.Open(path, &f.conn);
REQUIRE(handle != nullptr);
auto* cursor = f.cursors.Open(handle);
REQUIRE(cursor != nullptr);
nt::PlanNode plan;
plan.op = nt::PlanNode::Op::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, people_path, { { "name", "John" }, { "age", "30" } });
store_tuple(f.backend, people_path, { { "name", "Peter" }, { "age", "25" } });
store_tuple(f.backend, 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_cursor = f.cursors.Open(people_handle);
REQUIRE(people_cursor != nullptr);
auto* eq_cursor = f.cursors.Open(eq_handle); // starts exhausted — JOIN resets it
REQUIRE(eq_cursor != nullptr);
// Plan: JOIN( SCAN(people), SCAN(eq[ Var("age"), Const("25") ]) )
nt::PlanNode right;
right.op = nt::PlanNode::Op::SCAN;
right.scan_cursor = eq_cursor;
right.scan_args = { nt::PathArg::Var("age"), nt::PathArg::Const("25") };
nt::PlanNode left;
left.op = nt::PlanNode::Op::SCAN;
left.scan_cursor = people_cursor;
nt::PlanNode root;
root.op = nt::PlanNode::Op::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, path, { { "id", "1" } });
store_tuple(f.backend, path, { { "id", "2" } });
store_tuple(f.backend, path, { { "id", "3" } });
auto* handle = f.handler.Open(path, &f.conn);
REQUIRE(handle != nullptr);
auto* cursor = f.cursors.Open(handle);
REQUIRE(cursor != nullptr);
nt::PlanNode scan;
scan.op = nt::PlanNode::Op::SCAN;
scan.scan_cursor = cursor;
nt::PlanNode take;
take.op = nt::PlanNode::Op::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);
}