Skip to main content

Using LoraDB in Rust

Overview​

The Rust API is the reference surface — every other binding wraps the same lora_database::Database type. Results map to a strongly typed LoraValue enum; errors propagate through Result. The handle is Send + Sync and cheap to clone; the underlying store is published through Arc snapshots, with writes serialized at commit.

How-to~5 min

Embed LoraDB in a Rust project

Add the lora-database crate to Cargo.toml, open a cheap Send + Sync Database handle, run a Cypher query through execute, and persist the graph either with a one-shot snapshot or with a WAL-backed open.

  1. Add the crate
    Run cargo add lora-database, or add lora-database = "0.14" under [dependencies] in Cargo.toml. The crate ships on crates.io with a stable Send + Sync handle, a strongly typed LoraValue enum, and Result-based errors. Examples on this page also pull in anyhow = "1" to keep main short; swap it for your own error type when you wire it into a real binary.
    Jump to section
  2. Open an in-memory handle
    Call Database::in_memory() to get a ready-to-use handle backed by an empty graph. Clones share one inner Arc<Mutex<Store>>, so wrapping in Arc<Database> is the standard way to share across threads and tasks. Auto-commit reads overlap on Arc snapshots, and write commits serialize at the store.
    Jump to section
  3. Run a Cypher query
    Call db.execute("CREATE (:Person {name: 'Ada'})", None)? to write a node, then db.execute("MATCH (p:Person) RETURN p.name", None)? to read it back. The second argument is Option<ExecuteOptions>; pass None for engine defaults. Results come back as a typed QueryResult enum: match on RowArrays for column/row tuples, Rows for column-keyed maps, or Graph for the default node/relationship shape.
    Jump to section
  4. Persist the graph
    For a one-shot dump, call db.save_snapshot_to("graph.bin")? to write a point-in-time file via atomic rename, then later Database::in_memory_from_snapshot("graph.bin")? to boot a fresh handle from it. For continuous durability between snapshots, open with Database::open_with_wal(WalConfig::enabled("./app"))? and reload with Database::recover("graph.bin", WalConfig::enabled("./app"))?, which replays the WAL above the snapshot's fence.
    Jump to section

Installation / Setup​

crates.io

# Cargo.toml
[dependencies]
lora-database = "0.14"
anyhow = "1"

When working inside this repository, use the workspace path dependency instead.

Creating a Client / Connection​

use lora_database::Database;

fn main() -> anyhow::Result<()> {
let db = Database::in_memory();
Ok(())
}

Database::in_memory() returns a ready-to-use handle with an empty graph. Clone it (via Arc) to share across threads — the inner store is shared, not duplicated.

Running Your First Query​

use lora_database::Database;

fn main() -> anyhow::Result<()> {
let db = Database::in_memory();

db.execute("CREATE (:Person {name: 'Ada', born: 1815})", None)?;

let result = db.execute(
"MATCH (p:Person) RETURN p.name AS name",
None,
)?;

println!("{:?}", result);
Ok(())
}

The second argument is Option<ExecuteOptions> — pass None for defaults.

Examples​

Minimal working example​

Already shown above — in_memory → execute → inspect.

Parameterised query​

use std::collections::BTreeMap;
use lora_database::{Database, LoraValue};

fn main() -> anyhow::Result<()> {
let db = Database::in_memory();
db.execute("CREATE (:Person {name: 'Ada', born: 1815})", None)?;

let mut params = BTreeMap::new();
params.insert("name".to_string(), LoraValue::String("Ada".into()));
params.insert("min".to_string(), LoraValue::Int(1800));

let result = db.execute_with_params(
"MATCH (p:Person)
WHERE p.name = $name AND p.born >= $min
RETURN p.name AS name, p.born AS born",
None,
params,
)?;
println!("{:?}", result);
Ok(())
}

Bind every $name the query uses: a missing one fails the query with expected parameter: $name. Bind null for "no value". See Queries → Parameters.

Structured result handling​

use lora_database::{Database, LoraValue, QueryResult};

fn names(db: &Database) -> anyhow::Result<Vec<String>> {
let result = db.execute("MATCH (p:Person) RETURN p.name AS name", None)?;
let QueryResult::RowArrays { columns, rows } = result else {
anyhow::bail!("unexpected result shape");
};
let idx = columns.iter().position(|c| c == "name").unwrap();

let mut out = Vec::with_capacity(rows.len());
for row in rows {
if let LoraValue::String(s) = &row[idx] {
out.push(s.clone());
}
}
Ok(out)
}

See Data Types → Scalars for the full LoraValue variants.

Service-layer abstraction​

A thin wrapper you'd realistically put in your application code:

use std::collections::BTreeMap;
use std::sync::Arc;
use lora_database::{Database, LoraValue};

#[derive(Clone)]
pub struct UserService {
db: Arc<Database>,
}

impl UserService {
pub fn new(db: Arc<Database>) -> Self { Self { db } }

pub fn upsert_user(&self, id: i64, name: &str) -> anyhow::Result<()> {
let mut params = BTreeMap::new();
params.insert("id".into(), LoraValue::Int(id));
params.insert("name".into(), LoraValue::String(name.into()));
self.db.execute_with_params(
"MERGE (u:User {id: $id})
ON CREATE SET u.created = temporal.timestamp()
SET u.name = $name, u.updated = temporal.timestamp()",
None,
params,
)?;
Ok(())
}

pub fn count(&self) -> anyhow::Result<i64> {
let r = self.db.execute("MATCH (u:User) RETURN count(*) AS n", None)?;
extract_int(r, "n")
}
}

// helper omitted for brevity — map the RowArrays result to an i64
# fn extract_int(_r: lora_database::QueryResult, _c: &str) -> anyhow::Result<i64> { Ok(0) }

Handle errors​

Every execute call returns Result. Distinguish query errors from connection-layer errors (not currently surfaced in the in-memory binding, but relevant when embedding):

use lora_database::Database;

fn main() {
let db = Database::in_memory();

match db.execute("BAD QUERY", None) {
Ok(_) => println!("ok"),
Err(e) => {
// engine-level parse / semantic / runtime error
eprintln!("query failed: {e}");
}
}
}

Common causes: parse errors, unknown variables, unknown functions. See Troubleshooting → Parse errors and Semantic errors.

Concurrency​

use std::sync::Arc;
use lora_database::Database;

fn main() -> anyhow::Result<()> {
let db = Arc::new(Database::in_memory());

let h1 = {
let db = Arc::clone(&db);
std::thread::spawn(move || -> anyhow::Result<()> {
db.execute("CREATE (:X)", None)?;
Ok(())
})
};
let h2 = {
let db = Arc::clone(&db);
std::thread::spawn(move || -> anyhow::Result<()> {
db.execute("MATCH (x) RETURN count(*)", None)?;
Ok(())
})
};
h1.join().unwrap()?;
h2.join().unwrap()?;
Ok(())
}

Auto-commit reads can overlap on Arc snapshots; write commits serialize.

Persisting your graph​

LoraDB can save the in-memory graph to a single file and restore it later. Snapshots are a point-in-time dump — simple and atomic on rename — and Rust also exposes the WAL-backed open / recover path when you need continuous durability between snapshots.

use lora_database::{Database, SnapshotMeta};

let db = Database::in_memory();
db.execute("CREATE (:Person {name: 'Ada'})", None)?;

// Save everything to disk.
let meta: SnapshotMeta = db.save_snapshot_to("graph.bin")?;
println!(
"{} nodes, {} relationships",
meta.node_count, meta.relationship_count,
);

// Boot a fresh Database from the saved file.
let db2 = Database::in_memory_from_snapshot("graph.bin")?;

// Or overlay a snapshot onto an existing handle.
db.load_snapshot_from("graph.bin")?;

Both save and load process the whole graph. Existing readers can keep using their pinned snapshots, but a crash between manual saves loses every mutation since the last save unless you opened the database with WAL.

WAL-backed open / recover:

use lora_database::{Database, WalConfig};

let db = Database::open_with_wal(WalConfig::enabled("./app"))?;
db.execute("CREATE (:Person {name: 'Ada'})", None)?;

// Later, reload a snapshot and replay WAL above its fence.
let recovered = Database::recover("graph.bin", WalConfig::enabled("./app"))?;

See the canonical Snapshots guide for the full metadata shape, file format, atomic-rename guarantees, and boundaries. For the recovery model, sync modes, and checkpoint semantics, see WAL and checkpoints.

Common Patterns​

Bulk insert from a Vec​

use lora_database::{Database, LoraValue};
use std::collections::BTreeMap;

let db = Database::in_memory();
let rows: Vec<LoraValue> = (0..1000u64).map(|i| {
let mut m: BTreeMap<String, LoraValue> = BTreeMap::new();
m.insert("id".into(), LoraValue::Int(i as i64));
m.insert("name".into(), LoraValue::String(format!("user-{i}")));
LoraValue::Map(m)
}).collect();

let mut params: BTreeMap<String, LoraValue> = BTreeMap::new();
params.insert("rows".into(), LoraValue::List(rows));

db.execute_with_params(
"UNWIND $rows AS row CREATE (:User {id: row.id, name: row.name})",
None,
params,
)?;

See UNWIND.

Share a Database across threads or tasks​

Wrap in Arc and clone freely. Clones share a single graph; auto-commit reads can overlap on snapshots, while write commits and explicit read-write transactions serialize.

Result format selection​

execute returns Result<QueryResult>. QueryResult has variants for different output shapes:

pub enum QueryResult {
RowArrays { columns: Vec<String>, rows: Vec<Vec<LoraValue>> },
Rows { columns: Vec<String>, rows: Vec<BTreeMap<String, LoraValue>> },
Graph { /* nodes, relationships */ },
Combined { /* rows + graph */ },
}

Control which shape you get via ExecuteOptions::format. The engine default is Graph. See Result formats for how each shape looks and when to pick which.

LoraValue at a glance​

  • Null, Bool, Int(i64), Float(f64), String
  • List(Vec<LoraValue>), Map(BTreeMap<String, LoraValue>)
  • Node(u64), Relationship(u64), Path { nodes, rels }
  • Temporal types: Date, Time, LocalTime, DateTime, LocalDateTime, Duration
  • Point { x, y, z?, srid }

Node and relationship variants hold only an ID. Use ExecuteOptions { format: ResultFormat::Rows, hydrate: true } (or re-materialise with MATCH (n) … RETURN n {.*}) for full maps with labels and properties.

Error Handling​

Everything is Result. Errors fall into three buckets:

BucketTypical causeHow to handle
ParseMissing paren, bad syntaxFix the query string
SemanticUnknown variable, unknown function, wrong arityAdjust names or fix version
RuntimeDeleteNodeWithRelationships, integer overflow, integer division by zeroAdjust query; see Troubleshooting

Pattern:

if let Err(e) = db.execute("BAD QUERY", None) {
tracing::error!(error = %e, "query failed");
}

Performance / Best Practices​

  • One graph per Database. Auto-commit reads can overlap on snapshots; write commits and explicit read-write transactions serialize.
  • Clone the handle, not the data. Arc<Database> gives every thread / task a cheap clone; the inner Arc<Mutex<Store>> is shared.
  • Timeouts are cooperative. Use execute_with_timeout where available, and still cap variable-length traversals and parameter sizes.
  • Release build for benchmarks. Debug builds are ~10× slower for most query shapes.

See also​