Examples¶
Complete, runnable examples are in the examples/ directory. Build them with:
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build/release
cmake --build build/release --target example-basic_crud example-cursor_iteration \
example-range_operations example-snapshots example-subtrees
Basic CRUD¶
Create a database, insert/get/update/remove keys using transactions.
examples/basic_crud.cpp
/// basic_crud.cpp — Create a database, insert/get/update/remove keys
#include <filesystem>
#include <iostream>
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
#include <psitri/write_session_impl.hpp>
#include <psitri/read_session_impl.hpp>
int main()
{
// Create (or open) a database in a temporary directory
auto dir = std::filesystem::temp_directory_path() / "psitri_basic_crud";
std::filesystem::remove_all(dir);
auto db = psitri::database::open(dir);
// All writes go through a write session (one per thread)
auto ws = db->start_write_session();
// --- Transactions ---
// start_transaction acquires root 0 and commits atomically
{
auto tx = ws->start_transaction(0);
tx.upsert("alice", "engineer");
tx.upsert("bob", "designer");
tx.upsert("carol", "manager");
tx.commit(); // atomic — all three keys become visible at once
}
// --- Point lookups ---
{
auto tx = ws->start_transaction(0);
if (auto val = tx.get<std::string>("alice"))
std::cout << "alice = " << *val << "\n";
if (auto val = tx.get<std::string>("dave"))
std::cout << "dave = " << *val << "\n";
else
std::cout << "dave not found\n";
tx.abort(); // read-only, nothing to commit
}
// --- Update and remove ---
{
auto tx = ws->start_transaction(0);
tx.update("alice", "senior engineer");
tx.remove("bob");
// upsert always succeeds (insert or update)
tx.upsert("carol", "updated value");
tx.commit();
}
// --- Verify final state ---
{
auto tx = ws->start_transaction(0);
auto alice = tx.get<std::string>("alice");
auto bob = tx.get<std::string>("bob");
auto carol = tx.get<std::string>("carol");
std::cout << "alice = " << alice.value_or("(gone)") << "\n";
std::cout << "bob = " << bob.value_or("(gone)") << "\n";
std::cout << "carol = " << carol.value_or("(gone)") << "\n";
tx.abort();
}
std::filesystem::remove_all(dir);
return 0;
}
Expected output:
alice = engineer
dave not found
insert carol again: no
alice = senior engineer
bob = (gone)
carol = manager
Cursor Iteration¶
Iterate keys forward, backward, and by prefix using read-only cursors.
examples/cursor_iteration.cpp
/// cursor_iteration.cpp — Iterate keys forward, backward, and by prefix
#include <filesystem>
#include <iostream>
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
#include <psitri/write_session_impl.hpp>
#include <psitri/read_session_impl.hpp>
int main()
{
auto dir = std::filesystem::temp_directory_path() / "psitri_cursor";
std::filesystem::remove_all(dir);
auto db = psitri::database::open(dir);
auto ws = db->start_write_session();
// Populate some data
{
auto tx = ws->start_transaction(0);
tx.upsert("fruit/apple", "red");
tx.upsert("fruit/banana", "yellow");
tx.upsert("fruit/cherry", "red");
tx.upsert("fruit/date", "brown");
tx.upsert("veggie/carrot", "orange");
tx.upsert("veggie/pea", "green");
tx.commit();
}
// Read-only cursor from a read session
auto rs = db->start_read_session();
auto cursor = rs->create_cursor(0);
// --- Forward iteration over all keys ---
std::cout << "=== All keys (forward) ===\n";
cursor.seek_begin();
while (!cursor.is_end())
{
auto val = cursor.value<std::string>();
std::cout << " " << cursor.key() << " = " << val.value_or("?") << "\n";
cursor.next();
}
// --- Reverse iteration ---
std::cout << "\n=== All keys (reverse) ===\n";
cursor.seek_last();
while (!cursor.is_rend())
{
std::cout << " " << cursor.key() << "\n";
cursor.prev();
}
// --- Prefix scan ---
std::cout << "\n=== Keys with prefix 'fruit/' ===\n";
cursor.lower_bound("fruit/");
while (!cursor.is_end() && cursor.key().starts_with("fruit/"))
{
std::cout << " " << cursor.key() << "\n";
cursor.next();
}
// --- lower_bound seek ---
std::cout << "\n=== lower_bound('fruit/c') ===\n";
cursor.lower_bound("fruit/c");
if (!cursor.is_end())
std::cout << " first key >= 'fruit/c': " << cursor.key() << "\n";
std::filesystem::remove_all(dir);
return 0;
}
Expected output:
=== All keys (forward) ===
fruit/apple = red
fruit/banana = yellow
fruit/cherry = red
fruit/date = brown
veggie/carrot = orange
veggie/pea = green
=== All keys (reverse) ===
veggie/pea
veggie/carrot
fruit/date
fruit/cherry
fruit/banana
fruit/apple
=== Keys with prefix 'fruit/' ===
fruit/apple
fruit/banana
fruit/cherry
fruit/date
=== lower_bound('fruit/c') ===
first key >= 'fruit/c': fruit/cherry
Range Operations¶
O(log n) range counting and range deletion — no leaf scanning required.
examples/range_operations.cpp
/// range_operations.cpp — O(log n) range counting and range deletion
#include <filesystem>
#include <iostream>
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
#include <psitri/write_session_impl.hpp>
#include <psitri/read_session_impl.hpp>
int main()
{
auto dir = std::filesystem::temp_directory_path() / "psitri_ranges";
std::filesystem::remove_all(dir);
auto db = psitri::database::open(dir);
auto ws = db->start_write_session();
// Insert 10,000 keys: "key/00000" .. "key/09999"
{
auto tx = ws->start_transaction(0);
for (int i = 0; i < 10000; ++i)
{
char key[16], val[16];
snprintf(key, sizeof(key), "key/%05d", i);
snprintf(val, sizeof(val), "val-%d", i);
tx.upsert(key, val);
}
tx.commit();
}
// --- O(log n) range count ---
// count_keys uses descendant counters in inner nodes — no leaf scanning
auto rs = db->start_read_session();
auto cursor = rs->create_cursor(0);
uint64_t total = cursor.count_keys();
uint64_t range = cursor.count_keys("key/01000", "key/02000");
std::cout << "Total keys: " << total << "\n";
std::cout << "Keys in [01000, 02000): " << range << "\n";
// --- O(log n) range deletion ---
{
auto tx = ws->start_transaction(0);
uint64_t removed = tx.remove_range("key/05000", "key/06000");
std::cout << "Removed in [05000, 06000): " << removed << "\n";
tx.commit();
}
// Verify the count dropped
cursor.refresh(0);
std::cout << "Total after removal: " << cursor.count_keys() << "\n";
std::filesystem::remove_all(dir);
return 0;
}
Expected output:
Total keys: 10000
Keys in [01000, 02000): 1000
Removed in [05000, 06000): 1000
Total after removal: 9000
Snapshots¶
Zero-cost snapshots via copy-on-write — O(1) to create, readers see a consistent point-in-time view while writers proceed independently.
examples/snapshots.cpp
/// snapshots.cpp — Zero-cost snapshots via copy-on-write
#include <filesystem>
#include <iostream>
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
#include <psitri/write_session_impl.hpp>
#include <psitri/read_session_impl.hpp>
int main()
{
auto dir = std::filesystem::temp_directory_path() / "psitri_snapshots";
std::filesystem::remove_all(dir);
auto db = psitri::database::open(dir);
auto ws = db->start_write_session();
// Write initial data
{
auto tx = ws->start_transaction(0);
tx.upsert("balance/alice", "1000");
tx.upsert("balance/bob", "500");
tx.commit();
}
// Take a snapshot — O(1), just increments a reference count on the root
auto rs = db->start_read_session();
auto snapshot = rs->create_cursor(0);
// Mutate the live tree
{
auto tx = ws->start_transaction(0);
tx.upsert("balance/alice", "800");
tx.upsert("balance/bob", "700");
tx.upsert("balance/carol", "300");
tx.commit();
}
// Snapshot still sees the old state (copy-on-write)
std::cout << "=== Snapshot (before mutation) ===\n";
snapshot.seek_begin();
while (!snapshot.is_end())
{
auto val = snapshot.value<std::string>();
std::cout << " " << snapshot.key() << " = " << val.value_or("?") << "\n";
snapshot.next();
}
// Current state sees the new values
std::cout << "\n=== Current state (after mutation) ===\n";
auto current = rs->create_cursor(0);
current.seek_begin();
while (!current.is_end())
{
auto val = current.value<std::string>();
std::cout << " " << current.key() << " = " << val.value_or("?") << "\n";
current.next();
}
// Snapshot is released automatically when the cursor goes out of scope
// — the COW tree nodes shared between snapshot and current are freed
// only when the last reference is released
std::filesystem::remove_all(dir);
return 0;
}
Expected output:
=== Snapshot (before mutation) ===
balance/alice = 1000
balance/bob = 500
=== Current state (after mutation) ===
balance/alice = 800
balance/bob = 700
balance/carol = 300
Subtrees¶
Store entire trees as values — composable, hierarchical data with O(1) subtree operations.
examples/subtrees.cpp
/// subtrees.cpp — Composable trees: store entire trees as values
#include <filesystem>
#include <iostream>
#include <psitri/database.hpp>
#include <psitri/transaction.hpp>
#include <psitri/write_session_impl.hpp>
#include <psitri/read_session_impl.hpp>
int main()
{
auto dir = std::filesystem::temp_directory_path() / "psitri_subtrees";
std::filesystem::remove_all(dir);
auto db = psitri::database::open(dir);
auto ws = db->start_write_session();
// --- Build a subtree for user metadata ---
auto meta_cursor = ws->create_write_cursor();
meta_cursor->upsert("schema_version", "3");
meta_cursor->upsert("created_by", "admin");
meta_cursor->upsert("engine", "psitri");
auto meta_root = meta_cursor->root();
// --- Build the main tree and embed the subtree ---
{
auto tx = ws->start_transaction(0);
tx.upsert("users/alice", "engineer");
tx.upsert("users/bob", "designer");
tx.upsert("metadata", std::move(meta_root)); // subtree as a value
tx.commit();
}
// --- Read back the subtree ---
{
auto tx = ws->start_transaction(0);
if (tx.is_subtree("metadata"))
{
std::cout << "=== metadata subtree ===\n";
auto sub_cursor = tx.get_subtree_cursor("metadata");
auto rc = sub_cursor.read_cursor();
rc.seek_begin();
while (!rc.is_end())
{
auto val = rc.value<std::string>();
std::cout << " " << rc.key() << " = " << val.value_or("?") << "\n";
rc.next();
}
}
// Regular keys work alongside subtree keys
std::cout << "\n=== top-level keys ===\n";
auto rc = tx.read_cursor();
rc.seek_begin();
while (!rc.is_end())
{
if (rc.is_subtree())
std::cout << " " << rc.key() << " = [subtree]\n";
else
{
auto val = rc.value<std::string>();
std::cout << " " << rc.key() << " = " << val.value_or("?") << "\n";
}
rc.next();
}
tx.abort();
}
std::filesystem::remove_all(dir);
return 0;
}
Expected output: