Repository navigation
Graph API
The Graph API (mgraphdb::graph::Graph) is the public surface that ties the
four storage layers into one ergonomic interface. "Everything is a node": an
edge is simply a property whose value is PropValue::Edge, owned by its start
node and pointing at its end node.
| Layer | Role |
|---|---|
| Node Store | one 12-byte record per node |
| Property/Edge Store | one 16-byte record per property or edge |
| String Store | deduplicated backing for strings too long to inline |
live adjacency (Vec<Vec<PropId>>) |
O(1) edge/property insert + iterate; exported to a CSR Index |
While the graph is mutated, adjacency is kept in a live Vec<Vec<PropId>> for
O(1) inserts and contiguous iteration. build_index() exports it to an immutable
CSR IndexStore for persistence; open() rebuilds the live adjacency from that
index.
set_str routes by length automatically:
-
≤ 14 bytes → stored inline in the 16-byte property record (
InlineStr). -
> 14 bytes → interned into the String Store; the record holds a
StringRef(StrId).
string_value resolves either form back to a String transparently, so callers
never see the distinction.
| Method | Purpose |
|---|---|
new() / with_compression(c)
|
empty graph (optionally compressed strings) |
add_node() / add_typed_node(type_id)
|
create a node (optionally typed by a class node) |
set_property(node, &PropValue) |
attach a typed property |
set_str(node, &str) |
attach a string, auto-routing long values |
add_edge(from, to) |
directed edge; sets HAS_OUT on from, HAS_IN on to
|
neighbors(node) / degree(node)
|
records owned by a node (props + edges) |
out_edges(node) |
end nodes of a node's outgoing edges |
properties(node) |
(PropId, PropValue) pairs, edges excluded |
value(pid) / string_value(pid)
|
decode a record; resolve a string transparently |
build_index() |
export live adjacency to a CSR IndexStore
|
save(dir) / open(dir)
|
persist / load all four segments |
use mgraphdb::graph::Graph;
use mgraphdb::prop_store::PropValue;
let mut g = Graph::new();
let person = g.add_node(); // a class node, used as a type
let alice = g.add_typed_node(person);
let bob = g.add_typed_node(person);
g.set_str(alice, "Alice")?; // inline (≤ 14 bytes)
g.set_property(alice, &PropValue::I64(30))?;
g.set_str(bob, "Bob")?;
g.add_edge(alice, bob)?; // Alice → Bob
assert_eq!(g.out_edges(alice)?, vec![bob]);
assert_eq!(g.properties(alice)?.len(), 2); // name + age (edge excluded)
g.save("my_graph_dir")?; // writes nodes/props/strings/index
let reloaded = Graph::open("my_graph_dir")?;The demo binary (cargo run) builds and traverses a small social graph end to
end.
cargo bench --bench graph — indicative figures on a developer laptop
(reduced sample size), graphs of N nodes with ~4 outgoing edges each:
| Benchmark | Size | Time | Throughput |
|---|---|---|---|
graph_build |
1 000 nodes | ~0.21 ms | ~23 M elem/s |
graph_build |
10 000 nodes | ~2.4 ms | ~21 M elem/s |
graph_traverse/out_edges_all |
10 000 nodes | ~0.75 ms | ~13 M edge/s (decodes each edge) |
graph_traverse/degree_all |
10 000 nodes | ~4.4 µs | ~2.3 G elem/s (slice length only) |
graph_index/build_index |
~50 000 records | ~0.21 ms | ~249 M elem/s |
graph_persist/save |
~50 000 records | ~0.92 ms | — |
graph_persist/open |
~50 000 records | ~0.99 ms | — |
Numbers are first-cut and meant for relative comparison as the design evolves, not as headline specifications.
- The live adjacency makes the Graph API write-friendly; the persisted CSR index makes reload read-friendly. This pairs the two layouts discussed on the Persistent Layer page.
- Tombstoned nodes still occupy a slot (their adjacency is retained); compaction is a future concern.
- A node out of range is rejected with an error rather than panicking.
See the Requirements page (REQ-01–03, REQ-22, REQ-60–63) for the numbered, traceable form of this design.
(c) Stephan Zehrer (2025)
Planning & process
Design & architecture
Scratch