GraphReader
GraphReader<'snap> is the primary entry point for all graph operations.
It wraps an MVCC snapshot and provides typed, zero-copy access to graph data.
Creating a GraphReader
#![allow(unused)] fn main() { use parallax_graph::GraphReader; use parallax_store::StorageEngine; let engine = StorageEngine::open(config)?; let snap = engine.snapshot(); // O(1), no lock after this let graph = GraphReader::new(&snap); // graph can now be used freely; snap must not be dropped while graph is in scope }
Full API Reference
#![allow(unused)] fn main() { impl<'snap> GraphReader<'snap> { pub fn new(snapshot: &'snap Snapshot) -> Self; // ── Entity finding ──────────────────────────────────────────────── /// Find entities by type. Uses type index. pub fn find(&self, entity_type: &str) -> EntityFinder<'snap>; /// Find entities by class. Uses class index. pub fn find_by_class(&self, class: &str) -> EntityFinder<'snap>; /// Find all entities. Full scan. pub fn find_all(&self) -> EntityFinder<'snap>; /// O(1) point lookup by EntityId. pub fn get_entity(&self, id: EntityId) -> Option<&'snap Entity>; /// O(1) point lookup by RelationshipId. pub fn get_relationship(&self, id: RelationshipId) -> Option<&'snap Relationship>; // ── Traversal ───────────────────────────────────────────────────── /// Start a traversal from a specific entity. pub fn traverse(&self, start: EntityId) -> TraversalBuilder<'snap>; // ── Path finding ────────────────────────────────────────────────── /// Find the shortest path between two entities. pub fn shortest_path(&self, from: EntityId, to: EntityId) -> ShortestPathBuilder<'snap>; // ── Blast radius ────────────────────────────────────────────────── /// Compute the blast radius from a target entity. pub fn blast_radius(&self, origin: EntityId) -> BlastRadiusBuilder<'snap>; // ── Coverage analysis ───────────────────────────────────────────── /// Find entities missing a qualifying neighbor via the given verb. pub fn coverage_gap(&self, verb: &str) -> CoverageGapBuilder<'snap>; } }
Lifetime Annotation
The 'snap lifetime on GraphReader<'snap> means:
- Every
&'snap Entityreference returned by the reader borrows from the snapshot - The snapshot must outlive the reader and all references derived from it
- The Rust borrow checker enforces this at compile time — no runtime cost
This design guarantees:
- Zero-copy reads: Entity data is never cloned from the snapshot
- No use-after-free: You cannot hold a reference to freed snapshot data
- No stale reads: You always read from the snapshot you created the reader with
Collecting vs Cloning
Since GraphReader returns references with snapshot lifetimes, you must
either use the data within the snapshot's scope or clone it:
#![allow(unused)] fn main() { // Use within scope — zero allocation let snap = engine.snapshot(); let graph = GraphReader::new(&snap); let count = graph.find("host").count(); // no allocation let first_host = graph.find("host").collect().first().map(|e| e.display_name.as_str()); // Clone if you need owned data past the snapshot let hosts: Vec<Entity> = graph .find("host") .collect() .into_iter() .cloned() // Clone here when crossing async boundaries .collect(); drop(snap); // now safe to drop }
In Server Handlers
#![allow(unused)] fn main() { async fn list_hosts(State(state): State<AppState>) -> Json<Value> { let hosts = { let engine = state.engine.lock().unwrap(); let snap = engine.snapshot(); let graph = GraphReader::new(&snap); // Collect into owned Vec before dropping snap + lock graph.find("host") .collect() .into_iter() .cloned() .collect::<Vec<Entity>>() }; // engine lock + snap released here Json(json!({ "entities": hosts.iter().map(entity_to_json).collect::<Vec<_>>() })) } }