lance-csr
A Cypher graph query engine for Lance datasets, built on Apache DataFusion, with a CSR (compressed sparse row) adjacency index for fast multi-hop traversal. It interprets Lance datasets or Arrow tables as property graphs. The lance-csr Python package wraps this crate.
Features
- Cypher query parsing and AST construction
- Graph configuration for mapping Lance tables to nodes and relationships
- Semantic validation with typed
GraphErrordiagnostics - Pluggable execution strategies (DataFusion planner by default, Lance Native placeholder)
CypherEngine: register datasets once, optionally build the CSR, and serve many queries- Lance datasets read lazily and pinned to a version until
refresh() - Async query execution that returns Arrow
RecordBatchresults or a stream - JSON-serializable parameter binding for reusable query templates
- Logical plan debugging via
CypherQuery::explain
Quick start
use HashMap;
use ;
let config = builder
.node_table
.rel_table
.build?;
let sources = from;
let engine = new.await?;
let batch = engine
.execute
.await?;
examples/embedded_query.rs is a complete program that writes two Lance datasets, queries them, and picks up a
new write with refresh():
The Rust guide covers the API, and the rest of the
documentation covers the graph schema, supported Cypher and the CSR index.
Building the crate needs protoc for Lance's dependencies.
For one query over in-memory batches, CypherQuery works without an engine:
use HashMap;
use Arc;
use ;
use ;
use ;
let config = builder
.node_table
.rel_table
.build?;
let schema = new;
let batch = try_new?;
let mut tables = new;
tables.insert;
let query = new?
.with_config
.with_parameter;
let runtime = new?;
// Use default DataFusion-based execution
let result = runtime.block_on?;
The query expects a HashMap<String, RecordBatch> keyed by the labels and relationship types referenced in the Cypher text. Each record batch should expose the columns configured through GraphConfig (ID fields, property fields, etc.). Relationship mappings also expect a batch keyed by the relationship type (for example KNOWS) that contains the configured source/target ID columns and any optional property columns.
Configuring Graph Mappings
Graph mappings are declared with GraphConfig::builder():
use ;
let config = builder
.node_table
.rel_table
.build?;
For finer control, build NodeMapping and RelationshipMapping instances explicitly:
let person = new
.with_properties
.with_filter;
let knows = new
.with_properties;
let config = builder
.with_node_mapping
.with_relationship_mapping
.build?;
For Rust callers migrating away from direct mapping-field access, use get_node_mapping(label) and
get_relationship_mapping(type_name) for case-insensitive lookups. iter_node_mappings() and
iter_relationship_mappings() yield read-only (normalized_key, mapping) pairs; iteration order is
unspecified. Build mappings through GraphConfigBuilder so keys are normalized. Public fields remain
available for compatibility until a breaking release; replacing direct writes with builder calls avoids
that future migration. The serialized configuration and Python API are unchanged.
Executing Cypher Queries
CypherQuery::newparses Cypher text into the internal AST.with_configattaches the graph configuration used for validation and execution.with_parameter/with_parametersbind JSON-serializable values that can be referenced as$paramin the Cypher text.executeis asynchronous and returns an ArrowRecordBatch. PassNoneto use the default DataFusion planner.ExecutionStrategy::LanceNativeis reserved for future native execution support and currently errors.explainis asynchronous and returns a formatted string containing the graph logical plan alongside the DataFusion logical and physical plans.
All queries use the DataFusion planner for optimization and execution.
A builder (CypherQueryBuilder) is also available for constructing queries programmatically without parsing text.
Supported Cypher
See Supported Cypher in the documentation for the clauses, patterns, expressions and functions the engine runs.
Crate Layout
ast– Cypher AST definitions.parser– Nom-based Cypher parser.semantic– Lightweight semantic checks on the AST.logical_plan– Builders for graph logical plans.datafusion_planner– DataFusion-based execution planning.config– Graph configuration types and builders.engine–CypherEngine: cached snapshot, CSR adjacencies,refresh().query– High levelCypherQueryAPI and runtime.csr_index,csr_expand– The CSR adjacency index and the traversal operators that use it.error–GraphErrorand result helpers.namespace– Namespace helpers (re-exported fromlance-csr-catalog).source_catalog– Catalog helpers for looking up table metadata (re-exported fromlance-csr-catalog).
lance-csr re-exports the catalog and namespace types from the lance-csr-catalog crate for
API compatibility. You can depend on lance-csr-catalog directly if you only need catalog or
namespace utilities.
Error Handling
Most APIs return Result<T, GraphError>. Errors include parsing failures, missing mappings, and execution issues surfaced from DataFusion.
Constructing query ASTs directly
Prefer CypherQuery::new or the query builder when you do not need direct AST access. Pagination fields
now use Option<ast::RowCount>: wrap literal values with RowCount::Literal(n), or use
RowCount::Parameter(name) before parameter substitution. AST struct literals also set
union_branches: Vec::new() for a single query. UnionBranch holds each following query and its all
flag; each branch has its own variable scope. Exhaustive matches on LogicalOperator must handle the
new SingleRow and QueryUnion variants. These are Rust source compatibility changes; Python method
signatures are unchanged by UNION.
Named ordinary paths add PathPattern.variable; set it to None in existing unnamed-path struct
literals, or Some(name) for p = (a)-[:R]->(b). Shortest-path names still use
ShortestPathSpec.variable. LogicalOperator::PathMatch retains ordered node and relationship
records, so visitors must traverse its input and path pattern instead of treating a path value as a
length. The public nodes(path) result remains a list of integer keys; the Cypher guide describes the
separate Arrow record returned by a path value.
Read-only subqueries add ReadingClause::Call(Box<CypherQuery>) and
LogicalOperator::{Apply, CallInput}. Update exhaustive matches and visitors to recurse into the CALL
query, including its UNION branches. A leading importing WITH declares the outer variables available
in that query; each CALL body otherwise has its own scope.
List folds add ValueExpression::{Reduce, ListPredicate, LocalVariable}. Update exhaustive matches
and visitors to traverse the input and body while respecting iterator and accumulator scope. Prefer
parsing Cypher text to construct these expressions: the parser marks local variable references so they
cannot accidentally bind to graph variables with the same name. These path, CALL and fold additions
also require updates from Rust consumers that construct ASTs or exhaustively match public enums.
Testing
Benchmarks
All published numbers were measured through the Python package; see the
benchmarks page. Criterion micro-benchmarks live in the unpublished
lance-csr-benches crate.
Python Bindings
See the Python package docs for setup and development:
- Python package README:
python/README.md - Runnable examples (from repo root):
examples/README.md
License
Apache-2.0. See LICENSE and NOTICE.