Skip to content

Graph Queries Tutorial

Graph Queries Tutorial

Difficulty: Intermediate Prerequisites: Basic HeliosDB SQL knowledge Module: src/graph/

HeliosDB includes a property-graph layer for modeling and traversing relationships. This tutorial covers the data model, API usage, traversal algorithms, and planned SQL integration.


1. Concepts

HeliosDB’s graph layer uses a property-graph model:

  • Nodes are identified by a 64-bit unsigned integer (compatible with row IDs in the relational engine)
  • Edges are directed, typed (labeled), and optionally weighted
  • Adjacency lists are stored in concurrent DashMap structures for both outgoing and incoming edges

This design is ideal for social networks, knowledge graphs, dependency trees, and recommendation engines.


2. Creating a Graph

The graph store is created via the Rust API:

use heliosdb_lite::graph::{GraphStore, Edge};
// Create an in-memory graph
let graph = GraphStore::in_memory();

3. Adding Nodes and Edges

Edges implicitly create nodes. Each edge has a source, destination, label, and optional weight:

// Unweighted edges (default weight = 1.0)
graph.add_edge(Edge::new(1, 2, "FOLLOWS"));
graph.add_edge(Edge::new(1, 3, "FOLLOWS"));
graph.add_edge(Edge::new(2, 3, "KNOWS"));
// Weighted edge
graph.add_edge(Edge::with_weight(3, 4, "REPORTS_TO", 0.5));

Removing Edges

// Remove all edges between nodes 1 and 2 (regardless of label)
let removed = graph.remove_edges(1, 2);
assert_eq!(removed, 1);

Querying Adjacency

// Outgoing edges from node 1
let out = graph.out_edges(1);
// out = [Edge { from: 1, to: 3, label: "FOLLOWS", weight: 1.0 }]
// Incoming edges to node 3
let inc = graph.in_edges(3);
// inc contains edges from 1 and 2
// Degree queries
let out_deg = graph.out_degree(1); // 1 (after removing edge to 2)
let in_deg = graph.in_degree(3); // 2

4. Traversal: BFS

Breadth-first search visits nodes layer by layer from a source node:

use heliosdb_lite::graph::traverse::bfs;
let result = bfs(&graph, 1, u32::MAX);
// result.order: nodes in BFS visit order
// result.depth: HashMap<NodeId, u32> with hop distance from source
for node in &result.order {
let d = result.depth[node];
println!("Node {} is {} hops from source", node, d);
}

Bounded BFS

Limit traversal depth to find nodes within N hops:

// Only visit nodes within 2 hops
let nearby = bfs(&graph, 1, 2);

5. Traversal: Dijkstra Shortest Path

Find the shortest (minimum-weight) path between two nodes:

use heliosdb_lite::graph::traverse::dijkstra;
let result = dijkstra(&graph, 1);
// result.dist: HashMap<NodeId, f32> with total cost from source
// result.prev: HashMap<NodeId, NodeId> predecessor map for path reconstruction
if let Some(&cost) = result.dist.get(&4) {
println!("Shortest path cost from 1 to 4: {}", cost);
}

Reconstructing the Path

use heliosdb_lite::graph::traverse::shortest_path;
match shortest_path(&graph, 1, 4) {
Ok(path) => {
println!("Path: {:?}, cost: {}", path.nodes, path.cost);
// e.g., Path: [1, 3, 4], cost: 1.5
}
Err(e) => println!("No path: {}", e),
}

Dijkstra requires non-negative edge weights. A NegativeWeight error is returned if a negative weight is encountered.


A* extends Dijkstra with a heuristic function for faster goal-directed search:

use heliosdb_lite::graph::traverse::a_star;
// Heuristic: estimated cost from node to target
let heuristic = |node: u64| -> f32 {
match node {
3 => 0.5, // Node 3 is close to target
_ => 1.0, // Default estimate
}
};
match a_star(&graph, 1, 4, heuristic) {
Ok(path) => println!("A* path: {:?}, cost: {}", path.nodes, path.cost),
Err(e) => println!("No path: {}", e),
}

The heuristic must be admissible (never overestimate) for A* to find optimal paths.


7. Planned SQL Functions

The graph module declares four SQL functions for future integration:

-- BFS traversal: returns reachable node IDs ordered by depth
SELECT * FROM graph_bfs(1, 10);
-- Dijkstra shortest path between two nodes
SELECT * FROM graph_shortest_path(1, 4);
-- A* search with a heuristic column
SELECT * FROM graph_a_star(1, 4);
-- Immediate out-neighbours of a node
SELECT * FROM graph_neighbors(1);

Note: These SQL functions are declared but not yet wired to the executor. The wiring is deferred until persistent EdgeStore backends (NativeBackend, RocksDB) are implemented. Currently, graph operations are available through the Rust API only.


8. Example: Social Network

use heliosdb_lite::graph::{GraphStore, Edge};
use heliosdb_lite::graph::traverse::{bfs, shortest_path};
let g = GraphStore::in_memory();
// Build a social network
g.add_edge(Edge::new(1, 2, "FOLLOWS")); // Alice -> Bob
g.add_edge(Edge::new(1, 3, "FOLLOWS")); // Alice -> Carol
g.add_edge(Edge::new(2, 4, "FOLLOWS")); // Bob -> Dave
g.add_edge(Edge::new(3, 4, "FOLLOWS")); // Carol -> Dave
g.add_edge(Edge::new(4, 5, "FOLLOWS")); // Dave -> Eve
// Find all users within 2 hops of Alice
let nearby = bfs(&g, 1, 2);
// nearby.order = [1, 2, 3, 4] (Eve is 3 hops away)
// Shortest path from Alice to Eve
let path = shortest_path(&g, 1, 5).unwrap();
// path.nodes = [1, 2, 4, 5] or [1, 3, 4, 5], cost = 3.0

9. Troubleshooting

IssueCauseFix
NoPath errorNo directed path existsCheck edge direction; edges are one-way
NegativeWeight errorEdge has weight < 0Dijkstra requires non-negative weights; use BFS for unweighted traversal
Node not in resultsNode has no edgesNodes only exist when they have at least one edge

Next Steps

  • Combine graph traversal with relational queries by using node IDs as foreign keys
  • Use weighted edges for recommendation scoring
  • See tests/graph_basic_helix.rs for comprehensive test examples