Quick Start¶
Building¶
git clone https://github.com/gofractally/arbtrie.git
cd arbtrie
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build/release
cmake --build build/release -j8
Prerequisites¶
Compiler: Clang 18+ is required. GCC is not supported — the codebase uses
Clang-specific flags (-Wno-vla-extension) and ARM NEON code that is Clang-only
in several benchmark files.
macOS (Apple Silicon)
CMake picks up Homebrew LLVM automatically via theif(APPLE) block in
CMakeLists.txt.
Linux (Ubuntu 22.04+ / Debian)
sudo apt-get install -y \
clang-20 \
cmake \
ninja-build \
libsqlite3-dev \
libboost-all-dev \
libcatch2-dev
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER=clang-20 \
-B build/debug
| Dependency | Minimum version | Purpose |
|---|---|---|
| Clang | 18 | Compiler (GCC not supported) |
| CMake | 3.16 | Build system |
| Ninja | any | Build engine (recommended) |
| SQLite3 | 3.x | sqlite-arb-test benchmark (optional) |
| Boost | 1.71 | program_options (benchmarks) |
| Catch2 | 3.x | Unit test framework |
Build Configurations¶
| Directory | Type | Flags | Use |
|---|---|---|---|
build/release |
Release | -O3, LTO |
Performance testing |
build/debug |
Debug | -O0 -g |
Development/debugging |
build/coverage |
Coverage | -O1 -DNDEBUG -fprofile-arcs -ftest-coverage -g |
Coverage reports |
Running Tests¶
Your First Database¶
Create a Database¶
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
// Create or open a database in the given directory
auto db = psitri::database::create("my_database");
The database stores data in memory-mapped files within the specified directory. The create factory method uses default configuration; for custom settings, construct with a runtime_config.
Write Data¶
Sessions are thread-affine -- always create them on the thread that will use them. See API Reference for details.
auto session = db->start_write_session();
// All mutations happen inside transactions
auto tx = session->start_transaction(0); // root index 0
tx.upsert("user:alice", "{"name": "Alice", "balance": 1000}");
tx.upsert("user:bob", "{"name": "Bob", "balance": 2500}");
tx.insert("user:carol", "{"name": "Carol", "balance": 500}");
tx.commit(); // atomic root swap -- visible to all readers instantly
upsertinserts or updatesinsertonly inserts (returnsfalseif key exists)updateonly updates (returnsfalseif key doesn't exist)- If the transaction is destroyed without
commit(), it auto-aborts
Read Data¶
// Read within a transaction
auto tx = session->start_transaction(0);
auto val = tx.get<std::string>("user:alice");
if (val) {
std::cout << "Alice: " << *val << std::endl;
}
Iterate with a Cursor¶
// Create a read-only cursor from a snapshot
auto read_ses = db->start_read_session();
auto cursor = read_ses->create_cursor(0);
// Forward iteration
cursor.seek_begin();
while (!cursor.is_end()) {
std::cout << cursor.key() << std::endl;
cursor.next();
}
// Range scan
cursor.lower_bound("user:");
while (!cursor.is_end() && cursor.key().starts_with("user:")) {
std::cout << cursor.key() << std::endl;
cursor.next();
}
Remove Data¶
auto tx = session->start_transaction(0);
// Single key removal
tx.remove("user:bob");
// Range removal -- O(log n), not O(k)
tx.remove_range("log:2024-01", "log:2024-06");
tx.commit();
Count Keys in a Range¶
auto cursor = read_ses->create_cursor(0);
// O(log n) -- does not scan the keys
uint64_t count = cursor.count_keys("user:", "user:\xFF");
Subtrees¶
auto tx = session->start_transaction(0);
// Create a subtree (independent tree stored as a value)
auto sub = session->create_write_cursor();
sub->upsert("field1", "value1");
sub->upsert("field2", "value2");
// Store the subtree as a value
tx.upsert("my_subtree", sub->root());
// Read back the subtree
auto subtree_cursor = tx.get_subtree_cursor("my_subtree");
tx.commit();
Sync and Durability¶
// Set sync mode per session
session->set_sync(sal::sync_type::mprotect); // default is sync_type::none (no sync)
// Or sync the entire database explicitly
db->sync();
See Write Protection & Durability for details on sync modes.
512 Independent Roots¶
PsiTri supports up to 512 independent top-level roots. Each root is a separate tree with its own key space. Writers to different roots don't conflict:
auto tx0 = session->start_transaction(0); // root 0: user data
auto tx1 = session->start_transaction(1); // root 1: indexes
tx0.upsert("user:alice", "data");
tx1.upsert("idx:age:25:alice", "");
tx0.commit();
tx1.commit();
Multi-Threaded Writers¶
Multiple threads can write to different roots simultaneously with zero contention. The key rule: create the session on the thread that will use it.
auto db = psitri::database::create("my_database");
const int num_writers = 4;
std::vector<std::thread> writers;
for (int w = 0; w < num_writers; ++w)
{
writers.emplace_back([&db, w]()
{
// Each thread creates its own session -- do NOT share sessions across threads
auto session = db->start_write_session();
// Each writer uses a different root (1, 2, 3, 4) -- no locking between them
auto tx = session->start_transaction(w + 1);
for (int i = 0; i < 10000; ++i)
{
std::string key = "key:" + std::to_string(i);
std::string val = "writer_" + std::to_string(w);
tx.upsert(key, val);
}
tx.commit(); // atomic root swap -- only touches root w+1
});
}
for (auto& t : writers)
t.join();
Writers to the same root are serialized by a per-root mutex -- the transaction blocks until the previous writer on that root commits or aborts:
// 4 threads writing to the SAME root -- safe but serialized
for (int w = 0; w < num_writers; ++w)
{
writers.emplace_back([&db, w]()
{
auto session = db->start_write_session();
// start_transaction(1) acquires the per-root mutex for root 1
// -- blocks until the previous writer commits or aborts
auto tx = session->start_transaction(1);
for (int i = 0; i < 1000; ++i)
{
std::string key = "w" + std::to_string(w) + ":key:" + std::to_string(i);
tx.upsert(key, "data");
}
tx.commit(); // releases the per-root mutex
});
}
Multi-Threaded Readers¶
Reader threads can iterate and query concurrently with writers. Readers never block writers, and writers never block readers. Each reader sees a consistent snapshot.
auto db = psitri::database::create("my_database");
std::atomic<bool> writers_done{false};
std::vector<std::thread> threads;
// Writer thread: continuously inserting data
threads.emplace_back([&db, &writers_done]()
{
auto session = db->start_write_session();
for (int round = 0; round < 100; ++round)
{
auto tx = session->start_transaction(0);
for (int i = 0; i < 1000; ++i)
{
std::string key = "r" + std::to_string(round) + ":k" + std::to_string(i);
tx.upsert(key, "value");
}
tx.commit();
}
writers_done.store(true);
});
// Reader threads: scanning the tree while writes are in progress
for (int r = 0; r < 4; ++r)
{
threads.emplace_back([&db, &writers_done]()
{
// Each reader creates its own session on its own thread
auto session = db->start_read_session();
while (!writers_done.load())
{
// Each cursor call takes a snapshot -- sees a consistent view
auto cur = session->create_cursor(0);
cur.seek_begin();
uint64_t count = 0;
while (!cur.is_end())
{
++count;
cur.next();
}
// count is always consistent -- never sees a partial transaction
}
});
}
for (auto& t : threads)
t.join();
Key Points¶
- Sessions are thread-affine: Always call
start_write_session()orstart_read_session()from the thread that will use it. Never create a session on one thread and pass it to another. - Different roots = zero contention: Writers to different roots never block each other.
- Same root = serialized: Only one writer per root at a time.
start_transaction()blocks until the root is available. - Readers never block: Readers take atomic snapshots and see a consistent view. They never interfere with writers or other readers.
- Up to 64 concurrent sessions: The database supports up to 64 threads with active sessions simultaneously.
Next Steps¶
- API Reference -- full type and method documentation
- Architecture Overview -- how PsiTri works under the hood
- Concurrent Writers -- how zero-contention multi-writer works internally
- Bank Transaction Benchmark -- performance comparison with RocksDB, MDBX, and more