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 guarded by a mutex.

Installation / Setup

crates.io

While pre-release, consume the crate as a workspace path or git dependency rather than from crates.io:

# Cargo.toml
[dependencies]
lora-database = { path = "../../crates/lora-database" }
anyhow = "1"
# or, once published:
# lora-database = "0.1"

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_memoryexecute → 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(())
}

Missing parameters resolve to null. Always bind every $name used in the query. 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 = timestamp()
SET u.name = $name, u.updated = 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 labels, 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(())
}

Calls serialise on the inner mutex; no data races, but no parallel execution either.

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 serialise against every query on the handle — the snapshot holds the same mutex as execute. A crash between saves loses every mutation since the last save.

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. Calls serialise on the internal mutex — the clones share a single graph.

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 label, unknown function, wrong arityAdjust names or fix version
RuntimeDeleteNodeWithRelationships, division by zero (returns null, doesn't error), integer overflow (debug only)Adjust query; see Troubleshooting

Pattern:

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

Performance / Best Practices

  • One mutex, one graph. Multiple execute() calls on the same Database serialise.
  • Clone the handle, not the data. Arc<Database> gives every thread / task a cheap clone; the inner Arc<Mutex<Store>> is shared.
  • No query timeout. A pathological query will hold the lock indefinitely. Cap variable-length traversals, and ensure parameter sizes are reasonable.
  • Release build for benchmarks. Debug builds are ~10× slower for most query shapes.

See also