Skip to content

Ref And Join

Ref<T> is the high-level reference type. It stores enough information to resolve another entity by stable row id or primary key, and it can lazily load the referenced value through a table binding.

Referenced types need RefTraits<T>. AKKARADB_ENTITY generates it.

struct Author {
uint64_t id;
std::string name;
};
AKKARADB_ENTITY(Author, id, name);
struct Post {
uint64_t id;
akkaradb::Ref<Author> author;
std::string title;
};
AKKARADB_ENTITY(Post, id, author, title);
authors.put({1, "Alice"});
posts.put({100, akkaradb::ref<Author>(1), "Hello"});

When the table or schema has a binding for Author, the ref can resolve lazily.

auto post = posts.get(100);
auto name = post->author->name;

Joins are typed read views over scans and lookups.

auto rows = posts
.join<&Post::author>(authors)
.where([](const Post& post, const Author& author) {
return author.name == "Alice";
})
.toVector();

Plain-field joins specify both sides.

auto rows = posts.join<&PlainPost::authorId, &Author::id>(authors).toVector();

Each join row contains the left Entry and the decoded right entity. where(), first(), any(), count(), and toVector() are evaluated in the join view.

Use Ref<T> when the owner row should keep following the same logical target across primary-key changes. Use a plain foreign-key field when you only need to store and compare a visible key.

Use join<&RefField>(right) for ref fields and join<&LeftField, &RightField>(right) for plain fields. For large relation-heavy workloads, model hot lookup paths with explicit indexes or application-level materialized views.

StateMeaning
key knownid() can return without row-id lookup.
row id knownrowId() can return or resolve through metadata.
loadedoperator-> and operator* can access the value.
dirtyThe referenced value should be written before the owner is stored.
attachedA table binding is available for lazy resolution.

Creating a Ref<T> from a full entity marks it dirty. PackedTable::put() flushes dirty refs before writing the owning entity.

Refs are stable across primary-key changes because the table maintains pk2row and row2pk metadata.

authors.updatePrimaryKey(1, Author{10, "Alice Cooper"});

An existing Ref<Author> can still resolve through the row id after the visible primary key changes.

Join formCost shape
join<&RefField>(right)Left scan plus right row-id lookup.
join<&LeftField, &RightPk>(right)Left scan plus right primary-key lookup.
join<&LeftField, &RightField>(right)Left scan plus right scan for matching fields.

Join helpers do not install a native join engine plan. They are view-level helpers over table scans and lookups.

Lazy Ref<T> resolution is normal C++ behavior. A predicate such as post.author->name == "Alice" crosses from one table into another and cannot be represented as a local raw-row field load.

In the Clang plugin bytecode rewrite path, PackedTable::query(lambda) with a Ref<T> crossing is emitted as an owned HostCallBool descriptor. That preserves normal lazy resolution, but it disables raw row evaluation for that predicate.

BytecodeQueryView::where(lambda) leaves Ref<T> crossings as decoded predicate filters because where descriptors must be bytecode-composable. Explicit join(...).where([](const Left&, const Right&) { ... }) keeps the existing two-entity predicate semantics and is not rewritten into local row bytecode.