Skip to content

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:

=== metadata subtree ===
  created_by = admin
  engine = psitri
  schema_version = 3

=== top-level keys ===
  metadata = [subtree]
  users/alice = engineer
  users/bob = designer