Use this page when you want to store C++ structs through AkkaraDB Native without handling raw byte buffers directly.
Include And Open
Section titled “Include And Open”The high-level API is exposed through akkaradb/AkkaraDB.hpp.
#include "akkaradb/AkkaraDB.hpp"
#include <cstdint>#include <string>
struct User { uint64_t id; std::string name; uint32_t age;};
AKKARADB_ENTITY(User, id, name, age);
int main() { auto db = akkaradb::AkkaraDB::open("data/app", akkaradb::StartupMode::NORMAL); auto users = db->table<&User::id>("users");
users.put(User{1, "Alice", 30}); auto alice = users.get(1);}AkkaraDB::open(path, mode) is the simplest entry point. Use AkkaraDB::open(AkkaraDB::Options) when you need to override engine thresholds, VersionLog, codecs, blob settings, API server settings, or startup behavior.
Startup Modes
Section titled “Startup Modes”StartupMode is passed through to the underlying engine configuration.
| Mode | Typical use |
|---|---|
ULTRA_FAST | Smoke tests, benchmarks, short-lived local experiments. Disables WAL, SST, blob, manifest, and VersionLog, disables forced flush/sync on close, and uses a large memtable threshold. |
FAST | Fast startup where full durable defaults are not required. Uses async WAL sync, disables VersionLog, promotes SST reads, and uses a larger memtable threshold. |
NORMAL | General embedded use. Uses async WAL sync with the default component set. |
DURABLE | Use when durability should be favored over startup/write speed. Uses sync WAL and enables VersionLog. |
The exact low-level option mapping belongs to the engine layer. Treat the mode as a profile and use AkkaraDB::Options::overrides when you need explicit control.
AkkaraDB::Options::overrides currently exposes overrides for the per-shard memtable threshold, VersionLog, SST codec, blob codec, blob threshold, SST read promotion, Bloom filter bits per key, and max L0 SST files. Options::api forwards API server backend, port, transport, TLS/PSK, HTTP, TCP, and gRPC limits to the underlying engine options.
Define An Entity
Section titled “Define An Entity”PackedTable is parameterized by a member pointer to the primary-key field.
struct Profile { uint64_t id; std::string email; std::string name; uint32_t age;};
AKKARADB_ENTITY(Profile, id, email, name, age);
auto profiles = db->table<&Profile::id>("profiles");AKKARADB_ENTITY(Type, PrimaryKey, ...) does two things:
| Generated metadata | Purpose |
|---|---|
RefTraits<Type> | Lets Ref<Type>, schema registration, and row-id references know the primary key. |
| Query proxy fields | Lets table.query([](auto row) { ... }) expose named fields. |
If you only need query fields and not Ref<T> traits, use AKKARADB_QUERYABLE(Type, ...).
BinPack Supported Shapes
Section titled “BinPack Supported Shapes”Entities are encoded with BinPack. For aggregate structs, fields are encoded in declaration order through Boost.PFR, so storage compatibility depends on field order and type layout.
| Shape | Notes |
|---|---|
bool, signed/unsigned integers, float, double | Built-in adapters. |
enum | Encoded through the underlying integer type. |
std::string, std::string_view | string_view is write-only in the adapter; stored values decode as owning strings where used. |
std::vector<T>, std::vector<uint8_t>, std::array<T, N> | Elements must also have adapters. |
std::map<K, V>, std::unordered_map<K, V> | Keys and values must have adapters. |
std::optional<T> | Encodes a presence byte and then the value when present. |
std::pair<A, B>, std::tuple<Ts...> | Encodes elements in order. |
| aggregate structs | Trivially copyable aggregates use a memcpy fast path; other aggregates are field-by-field. |
akkaradb::Ref<T> | Encodes the referenced row id. |
akkaradb::Immutable<T> | Encodes the wrapped value and returns sealed values on decode. |
Changing field order, field type, or table name after data exists should be treated as a storage migration.
Basic Operations
Section titled “Basic Operations”PackedTable stores one encoded entity per primary key.
profiles.put(Profile{1, "alice@example.test", "Alice", 30});
bool found = profiles.exists(1);auto profile = profiles.get(1);
Profile out{};bool decoded = profiles.getInto(1, out);
profiles.remove(1);Core Method Reference
Section titled “Core Method Reference”| Method | Return | Notes |
|---|---|---|
put(entity) | void | Inserts or replaces by the entity primary key. |
get(pk) | std::optional<Entity> | Decodes and returns the entity when present. |
getInto(pk, out) | bool | Decodes into an existing object and returns whether a row was found. |
exists(pk) | bool | Checks the table row key. |
remove(pk) | void | Removes the row, table indexes, and row-id metadata. |
upsert(pk, fn) | void | Loads or default-constructs an entity, assigns the primary key, runs fn, then writes. |
updatePrimaryKey(oldPk, entity) | void | Moves a row to entity's primary key while preserving the stable row id. |
rowIdOf(pk) | std::optional<RowId> | Resolves a primary key to the internal row id. |
primaryKeyOf(rowId) | std::optional<PK> | Resolves the current primary key for a row id. |
getByRowId(rowId) | std::optional<Entity> | Reads the current entity through row-id metadata. |
getIntoByRowId(rowId, out) | bool | Row-id version of getInto. |
count() | size_t | Counts rows in the table key range. |
Upsert
Section titled “Upsert”upsert() is useful when the update is easier to express as a mutation callback.
profiles.upsert(1, [](Profile& profile) { profile.id = 1; profile.name = "Alice Updated"; profile.age += 1;});The table sets entity.*PrimaryKeyPtr = pk before the callback. The callback should keep that primary key stable; use updatePrimaryKey() when the primary key itself must change.
Update The Primary Key
Section titled “Update The Primary Key”updatePrimaryKey() moves an existing row to a new primary key and keeps its internal row id.
profiles.updatePrimaryKey( 1, Profile{10, "alice@example.test", "Alice Cooper", 31});This matters for Ref<T> because references resolve through row-id metadata rather than through the visible primary-key bytes. The destination primary key must not already exist.
scanAll() and scan() return cursor-like ranges with hasNext() and next().
auto rows = profiles.scanAll();while (rows.hasNext()) { auto entry = rows.next(); // entry.id is the primary key, entry.value is the decoded Profile.}Range scans use primary-key order.
auto page = profiles.scan(100ULL, 200ULL);while (page.hasNext()) { auto entry = page.next();}scan(startPk) scans from startPk to the end of the table namespace. Numeric primary keys are encoded in sortable order, so signed and unsigned numeric keys scan in natural numeric order.
Secondary Indexes
Section titled “Secondary Indexes”Register an index with a member pointer.
auto byAge = profiles.index<&Profile::age>();
profiles.put(Profile{1, "a@example.test", "Alice", 30});profiles.put(Profile{2, "b@example.test", "Bob", 30});
auto age30 = byAge.find(30);while (age30.hasNext()) { auto entry = age30.next();}You can also register and chain indexes without keeping the returned index object.
profiles.indexed<&Profile::email>() .indexed<&Profile::age>() .indexed<&Profile::name>();Use prefixIndexed<&Field>() separately for string prefix search. It is string-like-field only, does not serve findBy<&Field>(), and is useful for hot startsWith() predicates and simple trailing-percent like("prefix%") patterns.
profiles.prefixIndexed<&Profile::email>();findBy<&Field>(value) returns the first matching entity and requires the field index to be registered.
auto bob = profiles.findBy<&Profile::email>("b@example.test");Indexes are maintained when put(), remove(), or updatePrimaryKey() modifies an indexed row. Register indexes before writing rows that should be findable through that index; the current API does not automatically backfill an index over existing rows.
The default C++ query API builds typed expression objects at compile time through proxy fields. Native builds can also run the optional akkara-query Clang plugin, which rewrites supported typed entity lambdas into bytecode descriptors.
auto adults = profiles .query([](auto profile) { return profile.age >= 18; }) .limit(10) .toVector();
auto alice = profiles .query([](auto profile) { return profile.email == "alice@example.test"; }) .first();The query proxy is generated by AKKARADB_ENTITY or AKKARADB_QUERYABLE. Supported operators include equality, inequality, comparisons, logical && / ||, in, notIn, startsWith, contains, like, null checks, nested fields, and map lookups.
Use [](auto row) for the normal proxy DSL. Use [](const Entity& row) when the Clang plugin should attempt native bytecode rewrite.
Query View Methods
Section titled “Query View Methods”| Method | Notes |
|---|---|
where(fn) | Adds another predicate with logical AND. |
limit(n) | Stops after n matched rows. |
first() | Returns std::optional<Entry>. |
any() | Returns whether at least one row matches. |
count() | Counts matching rows. |
toVector() | Materializes matching entries. |
Query result order is not a stable API contract. Sort materialized results in application code when order matters.
Indexed Planning
Section titled “Indexed Planning”The planner looks for a usable predicate on a registered index. The full expression is still evaluated before a row is returned, so the index only narrows the candidate set.
auto result = profiles.query([](auto profile) { return profile.email == "b@example.test" && profile.age >= 18;}).first();In this example, an email index can seed the scan, and age >= 18 is applied as the remaining filter.
| Predicate shape | Indexed plan behavior |
|---|---|
field == literal | Equality range over the field index. |
field != literal | Full field-index scan, then expression filtering. |
numeric field >/>=/</<= literal | Ordered index range for arithmetic non-bool fields. |
field.in(values) | One equality range per value, with candidate primary-key dedupe. |
field.notIn(values) | Full field-index scan, then expression filtering. |
optional field.isNull() | Equality range for the encoded empty optional. |
optional field.isNotNull() | Full field-index scan, then expression filtering. |
string startsWith | Prefix-index range when prefixIndexed<&Field>() is registered; otherwise a full field-index scan if a normal index exists. |
string contains | Full field-index scan, then expression filtering. |
string like("exact") | Equality range. |
string like("prefix%") | Prefix-index range for simple prefix patterns when prefixIndexed<&Field>() is registered; otherwise a full field-index scan if a normal index exists. |
predicates under && | The planner scores usable indexed sides and chooses the stronger candidate source. |
| predicates under ` | |
| nested fields, map lookups | Correctly evaluated, but not currently index-seeded by themselves. |
See Query for planner priority, literal conversion rules, like() wildcard semantics, native bytecode rewrite, and query lambda boundaries.
Optional, Nested, And Map Fields
Section titled “Optional, Nested, And Map Fields”The query proxy supports optional fields, nested struct fields, and map lookups.
auto unnamed = users.query([](auto user) { return user.nickname.isNull();}).toVector();
auto tokyo = users.query([](auto user) { return user.address.template field<&Address::city>() == "Tokyo";}).toVector();
auto gold = users.query([](auto user) { return user.tags.get("tier") == std::optional<std::string>{"gold"};}).toVector();Nested fields use template field<&Nested::field>() because the expression is a dependent template call.
Immutable Fields
Section titled “Immutable Fields”Wrap a field in akkaradb::Immutable<T> when a value may be set at creation time but should not change after it has been persisted.
struct Account { uint64_t id; akkaradb::Immutable<std::string> handle; uint32_t age;};
AKKARADB_ENTITY(Account, id, handle, age);put() seals immutable fields after loading or writing. A later replacement that changes a sealed immutable field throws. Primary-key fields cannot use Immutable<T>.
Update Hooks
Section titled “Update Hooks”onUpdate<&Field>() runs only when an existing row is replaced and the selected field changes.
profiles.onUpdate<&Profile::age>( [](const auto& oldAge, const auto& newAge, const Profile& oldProfile, Profile& newProfile) { newProfile.email = std::format("age-{}@example.test", newAge); });Handlers can observe the old entity and mutate the new entity before it is encoded. Use them for normalization, derived fields, and small validation rules. Keep heavy side effects outside table hooks because they run inside the table write path.
References
Section titled “References”Ref<T> represents another entity through either a primary key, row id, or loaded value. Once attached to a table binding, it can resolve lazily.
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"});
auto post = posts.get(100);auto authorName = post->author->name;When a Ref<T> is created from a full entity, the referenced entity is considered dirty. PackedTable::put() flushes dirty refs before writing the owner entity.
Joins are typed views over scans and lookups.
auto joined = posts .join<&Post::author>(authors) .where([](const Post& post, const Author& author) { return author.name == "Alice"; }) .toVector();For plain fields, provide both member pointers.
auto joined = posts.join<&Post::authorId, &Author::id>(authors).toVector();If the right field is the right table's primary key, the join uses primary-key lookups. Otherwise it scans the right table for each left row.
Schema And Foreign Keys
Section titled “Schema And Foreign Keys”AkkaraDB::Schema registers typed tables and installs hook-based foreign-key behavior.
auto schema = db->schema() .table<&Author::id>("authors") .table<&Post::id>("posts") .foreignKey<&Post::author>({akkaradb::OnDelete::Cascade}, {akkaradb::OnUpdate::Cascade}) .open();
auto& authors = schema.table<Author>();auto& posts = schema.table<Post>();For a plain field relation, provide the source field and target field.
schema.foreignKey<&PlainPost::authorId, &Author::id>( {akkaradb::OnDelete::Restrict}, {akkaradb::OnUpdate::Cascade});Foreign-Key Actions
Section titled “Foreign-Key Actions”| Action | Delete behavior | Primary-key update behavior |
|---|---|---|
Restrict | Rejects deleting a referenced target. | Rejects moving a referenced target key. |
Cascade | Removes source rows that reference the target. | Rewrites source fields to the new target key. |
SetNull | Sets nullable source fields to std::nullopt. | Sets nullable source fields to std::nullopt. |
Only one action can be selected for delete and one for update. SetNull requires a nullable source field. Foreign keys are implemented by table hooks and scans, not by a native constraint subsystem.
When the target field is not the target table primary key, foreignKey<FieldPtr, TargetFieldPtr>() registers an index on the target field so existence checks can use indexed lookup. Delete and update actions still scan the source table to find referencing rows. For Ref<T> fields, delete actions require the target primary key; update actions on refs are effectively skipped because refs follow row ids.
VersionLog Bridge
Section titled “VersionLog Bridge”The native high-level API keeps typed data in the same engine key space as the low-level API. When VersionLog is enabled on the underlying engine, table rows participate in the same version history because each typed row is ultimately an engine key/value write.
This C++ high-level header currently exposes current-row CRUD, row-id lookup, scan, query, index, ref, join, and schema behavior. It does not expose typed getAt() or typed history() helpers in PackedTable; use db->engine() and the low-level VersionLog APIs when you need explicit historical reads or rollback control.
Common Failure Modes
Section titled “Common Failure Modes”| Symptom | Likely cause |
|---|---|
findBy<&Field>() throws | The field index was not registered with index<&Field>() or indexed<&Field>(). |
| Newly registered index misses old rows | Index registration does not backfill existing rows. Rewrite or rebuild rows after registering the index. |
Ref<T> cannot resolve | The ref is detached, the target table is not registered in the schema/binding, or the target row was removed. |
foreignKey<&RefField>() throws target table not registered | The schema does not include the referenced entity table. |
SetNull setup fails | The source field is not std::optional<T>. |
updatePrimaryKey() throws destination exists | The new primary key is already present. |
Loaded Immutable<T> field rejects assignment | The value was sealed after decode or persist. |
| Data appears missing after table rename | The table name is part of the hashed storage prefix. |
| Struct change corrupts decode assumptions | BinPack aggregate encoding depends on field order and compatible field types. |
Practical Starting Point
Section titled “Practical Starting Point”For a first C++ embedding:
- Define plain structs with stable primary-key fields.
- Add
AKKARADB_ENTITYbeside each persisted struct. - Open one
AkkaraDBper data directory. - Register indexes immediately after opening a table.
- Use
query()for typed filtering andfindBy()for indexed equality lookup. - Use
Ref<T>andSchemaonly where row-id stability or referential actions are actually needed.