Rust Driver¶
A lightweight, embedded Rust API for querying, mutating, and inspecting the Velr graph database.
The Rust driver provides:
- Connection management via
Velr::open - Executing Cypher queries via
exec,exec_one, andrun - Transactional control via
begin_txandVelrTx - Savepoints via
savepoint,savepoint_named,rollback_to, andrelease_savepoint - Streaming result tables via
ExecTables,ExecTablesTx, andTableResult - EXPLAIN / EXPLAIN ANALYZE via
explainandexplain_analyze - Arrow binding via
bind_arrowandbind_arrow_chunks(feature:arrow-ipc) - Arrow IPC export via
TableResult::to_arrow_ipc_file()(feature:arrow-ipc)
Threading model¶
Velr uses a connection-affine model.
-
[
Velr] isSend+!Sync -
You may move a connection to another thread.
-
A single connection should not be used concurrently from multiple threads.
-
Handle types are thread-affine:
-
[
ExecTables] - [
ExecTablesTx] - [
TableResult] - [
RowIter] - [
VelrTx] - [
VelrSavepoint] - [
ExplainTrace]
A common pattern is to open one connection per thread when parallelism is needed.
Opening a Database¶
use velr::Velr;
fn main() -> velr::Result<()> {
// In-memory database
let db = Velr::open(None)?;
// Or on-disk
let db = Velr::open(Some("my.db"))?;
Ok(())
}
Path semantics:
None→ in-memory databaseSome(":memory:")→ in-memory databaseSome("path")→ file-backed database
Executing Queries¶
Queries may produce zero or more result tables.
run: Execute and discard results¶
Use this when you want to execute a query and do not need to consume any returned rows.
use velr::Velr;
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
db.run("CREATE (:User {name:'Alice'})")?;
Ok(())
}
exec: Stream result tables¶
Use this when a query may return one or more tables.
use velr::{CellRef, Velr};
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
db.run("CREATE (:User {name:'Alice'}), (:User {name:'Bob'})")?;
let mut stream = db.exec("MATCH (u:User) RETURN u.name AS name")?;
while let Some(mut table) = stream.next_table()? {
table.for_each_row(|row| {
if let CellRef::Text(name) = row[0] {
println!("User: {}", std::str::from_utf8(name).unwrap());
}
Ok(())
})?;
}
Ok(())
}
exec_one: Expect exactly one result table¶
Use this when the query is expected to produce exactly one table.
use velr::{CellRef, Velr};
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
db.run("CREATE (:User {name:'Alice', age:30})")?;
let mut table = db.exec_one(
"MATCH (u:User) RETURN u.name AS name, u.age AS age"
)?;
table.for_each_row(|row| {
let name = match row[0] {
CellRef::Text(t) => std::str::from_utf8(t).unwrap(),
_ => "<invalid>",
};
let age = match row[1] {
CellRef::Integer(v) => v,
_ => -1,
};
println!("{name} ({age})");
Ok(())
})?;
Ok(())
}
Reading Results¶
A TableResult provides:
column_names()column_count()rows()for_each_row(...)collect(...)
Example:
use velr::{CellRef, Velr};
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
db.run("CREATE (:Movie {title:'Inception', released:2010})")?;
let mut table = db.exec_one(
"MATCH (m:Movie) RETURN m.title AS title, m.released AS year"
)?;
println!("{:?}", table.column_names());
table.for_each_row(|row| {
let title = match row[0] {
CellRef::Text(t) => std::str::from_utf8(t).unwrap(),
_ => "<invalid>",
};
let year = match row[1] {
CellRef::Integer(y) => y,
_ => -1,
};
println!("{title} ({year})");
Ok(())
})?;
Ok(())
}
CellRef¶
Rows are exposed as slices of CellRef<'_>.
Supported cell types:
NullBool(bool)Integer(i64)Float(f64)Text(&[u8])Json(&[u8])
For text cells, you can also use CellRef::as_str_utf8():
use velr::CellRef;
fn print_cell(cell: CellRef<'_>) {
if let Some(Ok(s)) = cell.as_str_utf8() {
println!("{s}");
}
}
Lifetime of row values¶
Text and Json cells borrow bytes from the current row buffer.
Those borrowed bytes remain valid until:
- the next call to
RowIter::next()on the same iterator, or - the iterator is dropped
In normal usage, treat them as valid only inside the row callback.
Transactions¶
Velr supports explicit transactions via VelrTx.
Opening a transaction¶
Running queries inside a transaction¶
use velr::{CellRef, Velr};
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
let tx = db.begin_tx()?;
tx.run("CREATE (:Person {name:'Neo'})")?;
let mut table = tx.exec_one(
"MATCH (p:Person) RETURN count(p) AS c"
)?;
table.for_each_row(|row| {
let count = match row[0] {
CellRef::Integer(v) => v,
_ => -1,
};
println!("count = {count}");
Ok(())
})?;
tx.commit()?;
Ok(())
}
Commit / rollback¶
Both commit() and rollback() consume the transaction handle.
If a transaction is dropped without being committed or rolled back explicitly, it is rolled back automatically.
Savepoints¶
Velr supports two kinds of savepoints inside a transaction:
- Scoped savepoints via
savepoint(), which return a guard - Named savepoints via
savepoint_named(name), which remain active in the transaction until released or the transaction ends
Calling rollback_to(name) rolls back to the named savepoint, discards any newer named savepoints, and keeps the target savepoint active.
Scoped savepoint¶
Use a scoped savepoint when you want a local rollback point managed by a handle.
let tx = db.begin_tx()?;
let sp = tx.savepoint()?;
tx.run("CREATE (:Temp {k:'x'})")?;
// roll back only to the savepoint
sp.rollback()?;
tx.commit()?;
If a VelrSavepoint is dropped without an explicit release() or rollback(), it is rolled back and released automatically.
Named savepoint¶
Use a named savepoint when you want to roll back to a previously-created marker, including one created before newer savepoints.
let tx = db.begin_tx()?;
tx.savepoint_named("before_write1")?;
tx.run("CREATE (:Temp {k:'a'})")?;
tx.savepoint_named("before_write2")?;
tx.run("CREATE (:Temp {k:'b'})")?;
tx.rollback_to("before_write1")?;
tx.run("CREATE (:Temp {k:'c'})")?;
tx.release_savepoint("before_write1")?;
tx.commit()?;
Releasing a named savepoint¶
let tx = db.begin_tx()?;
tx.savepoint_named("before_write1")?;
tx.savepoint_named("before_write2")?;
tx.release_savepoint("before_write2")?;
tx.release_savepoint("before_write1")?;
tx.commit()?;
release_savepoint(name) currently releases the most recent active named savepoint.
EXPLAIN and EXPLAIN ANALYZE¶
The Rust driver exposes structured explain traces on both connections and transactions.
Basic EXPLAIN¶
use velr::Velr;
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
let trace = db.explain("MATCH (n) RETURN n")?;
println!("plans: {}", trace.plan_count()?);
println!("{}", trace.to_compact_string()?);
Ok(())
}
EXPLAIN ANALYZE¶
use velr::Velr;
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
let trace = db.explain_analyze("MATCH (n) RETURN n")?;
let snapshot = trace.snapshot()?;
for plan in snapshot {
println!("plan {}", plan.meta.plan_id);
for step in plan.steps {
println!(" step {}: {}", step.meta.step_no, step.meta.title);
for stmt in step.statements {
println!(" {} [{}]", stmt.meta.stmt_id, stmt.meta.kind);
}
}
}
Ok(())
}
Explain APIs¶
ExplainTrace provides:
plan_count()plan_meta(...)step_count(...)step_meta(...)statement_count(...)statement_meta(...)sqlite_plan_count(...)sqlite_plan_detail(...)sqlite_plan_details(...)snapshot()compact_len()to_compact_bytes()to_compact_string()write_compact(...)
Both Velr and VelrTx provide:
explain(query)explain_analyze(query)
Binding External Data with BIND()¶
Arrow bindings are available with the arrow-ipc feature enabled.
Binding Arrow arrays¶
#[cfg(feature = "arrow-ipc")]
fn main() -> velr::Result<()> {
use arrow2::array::{Array, BooleanArray, Utf8Array};
use velr::Velr;
let db = Velr::open(None)?;
let col_names = vec!["name".to_string(), "active".to_string()];
let arrays: Vec<Box<dyn Array>> = vec![
Box::new(Utf8Array::<i64>::from(vec![Some("Alice"), Some("Bob")])),
Box::new(BooleanArray::from(vec![Some(true), Some(false)])),
];
db.bind_arrow("_users", col_names, arrays)?;
db.run(
"UNWIND BIND('_users') AS r
CREATE (:User {name: r.name, active: r.active})"
)?;
Ok(())
}
Binding chunked Arrow columns¶
#[cfg(feature = "arrow-ipc")]
fn main() -> velr::Result<()> {
use arrow2::array::{Array, PrimitiveArray};
use velr::Velr;
let db = Velr::open(None)?;
let col_names = vec!["value".to_string()];
let chunks_per_col: Vec<Vec<Box<dyn Array>>> = vec![
vec![
Box::new(PrimitiveArray::<i64>::from([Some(1), Some(2)])),
Box::new(PrimitiveArray::<i64>::from([Some(3)])),
]
];
db.bind_arrow_chunks("_chunked", col_names, chunks_per_col)?;
Ok(())
}
Arrow binding inside a transaction¶
#[cfg(feature = "arrow-ipc")]
fn main() -> velr::Result<()> {
use arrow2::array::{Array, Utf8Array};
use velr::Velr;
let db = Velr::open(None)?;
let tx = db.begin_tx()?;
let col_names = vec!["name".to_string()];
let arrays: Vec<Box<dyn Array>> = vec![
Box::new(Utf8Array::<i64>::from(vec![Some("Alice"), Some("Bob")])),
];
tx.bind_arrow("_people", col_names, arrays)?;
tx.run(
"UNWIND BIND('_people') AS r
CREATE (:Person {name: r.name})"
)?;
tx.commit()?;
Ok(())
}
Notes¶
bind_arrow(...)binds one Arrow array per column.bind_arrow_chunks(...)binds chunked Arrow data per column.- For chunked binds, all columns must have the same total row count.
Exporting Results as Arrow IPC¶
With the arrow-ipc feature enabled, a TableResult can be exported as an Arrow IPC file in memory.
#[cfg(feature = "arrow-ipc")]
fn main() -> velr::Result<()> {
use velr::Velr;
let db = Velr::open(None)?;
db.run("CREATE (:Movie {title:'Inception', released:2010})")?;
let mut table = db.exec_one(
"MATCH (m:Movie) RETURN m.title AS title, m.released AS year"
)?;
let ipc_bytes = table.to_arrow_ipc_file()?;
println!("arrow ipc bytes: {}", ipc_bytes.len());
Ok(())
}
Complete Example¶
use velr::{CellRef, Velr};
fn main() -> velr::Result<()> {
let db = Velr::open(None)?;
db.run("CREATE (:Movie {title:'Inception', released:2010})")?;
let mut table = db.exec_one(
"MATCH (m:Movie) RETURN m.title AS title, m.released AS year"
)?;
table.for_each_row(|row| {
let title = match row[0] {
CellRef::Text(t) => std::str::from_utf8(t).unwrap(),
_ => "<??>",
};
let year = match row[1] {
CellRef::Integer(y) => y,
_ => -1,
};
println!("{title} ({year})");
Ok(())
})?;
Ok(())
}
API Summary¶
Velr¶
open(path: Option<&str>)exec(query)exec_one(query)run(query)explain(query)explain_analyze(query)begin_tx()bind_arrow(logical, col_names, arrays)(feature:arrow-ipc)bind_arrow_chunks(logical, col_names, chunks_per_col)(feature:arrow-ipc)
VelrTx¶
exec(query)exec_one(query)run(query)explain(query)explain_analyze(query)commit()rollback()savepoint()savepoint_named(name)rollback_to(name)release_savepoint(name)bind_arrow(logical, col_names, arrays)(feature:arrow-ipc)bind_arrow_chunks(logical, col_names, chunks_per_col)(feature:arrow-ipc)
VelrSavepoint¶
release()rollback()
Result types¶
ExecTablesExecTablesTxTableResultRowIterCellRef
Explain types¶
ExplainTraceExplainPlanExplainStepExplainStatementExplainPlanMetaExplainStepMetaExplainStatementMeta