Skip to content

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, and run
  • Transactional control via begin_tx and VelrTx
  • Savepoints via savepoint, savepoint_named, rollback_to, and release_savepoint
  • Streaming result tables via ExecTables, ExecTablesTx, and TableResult
  • EXPLAIN / EXPLAIN ANALYZE via explain and explain_analyze
  • Arrow binding via bind_arrow and bind_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] is Send + !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 database
  • Some(":memory:") → in-memory database
  • Some("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:

  • Null
  • Bool(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

let tx = db.begin_tx()?;

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

tx.commit()?;
// or
tx.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

  • ExecTables
  • ExecTablesTx
  • TableResult
  • RowIter
  • CellRef

Explain types

  • ExplainTrace
  • ExplainPlan
  • ExplainStep
  • ExplainStatement
  • ExplainPlanMeta
  • ExplainStepMeta
  • ExplainStatementMeta