Expand description
The typed graph surface, where a traversal that does not make sense does not
compile (11 section 6).
A node type is a struct with an id and a label, an edge type is a struct that says which node type it goes from and which it goes to, and a walk is a chain of method calls. There is no query language, and the argument for not having one is this file: what a Cypher engine finds out at run time, this finds out at compile time.
use yo::{Edge, Node, Yo};
#[derive(Yo, Debug, PartialEq)]
struct Person {
#[yo(id)]
id: u64,
#[yo(index)]
city: String,
}
#[derive(Yo, Debug, PartialEq)]
struct Follows {
since: i64,
}
impl Node for Person {
const LABEL: &'static str = "Person";
}
impl Edge for Follows {
type From = Person;
type To = Person;
const LABEL: &'static str = "FOLLOWS";
}
let db = yo::open(yo::MEMORY)?;
let g = db.graph("social")?;
let ada = g.add(&Person { id: 1, city: "london".to_owned() })?;
let grace = g.add(&Person { id: 2, city: "london".to_owned() })?;
let edsger = g.add(&Person { id: 3, city: "austin".to_owned() })?;
g.link(ada, grace, &Follows { since: 2024 })?;
g.link(grace, edsger, &Follows { since: 2026 })?;
// Who does the person I follow follow.
let two = g.walk(ada).out::<Follows>()?.out::<Follows>()?.nodes()?;
assert_eq!(two, vec![Person { id: 3, city: "austin".to_owned() }]);
// And a walk can start at an index rather than at an id.
assert_eq!(g.find(Person::CITY, "london")?.len(), 2);§What does not compile
An edge carries where it goes from and where it goes to in its type, so
out::<Follows>() on a walk that is standing on a Company is a type error
and not an empty result.
let acme = g.add(&Company { id: 100 }).unwrap();
// Follows starts at a Person, so there is no such hop from a Company.
g.walk(acme).out::<Follows>().unwrap();Linking is the same. An edge’s ends are its own types, so putting a company
on the wrong end of a Follows is a type error at the call site.
let acme = g.add(&Company { id: 100 }).unwrap();
g.link(ada, acme, &Follows { since: 2026 }).unwrap();§An Id is not the id you wrote
Graph::add hands back an Id<Person>, which is a handle into this graph
and not the 1 in the struct. The adjacency plane is keyed by a dense u64
and that is what makes a hop a probe and a sequential read, so the id in the
struct is looked up once on the way in and never again. Everything a walk
touches is already dense.
That is the trade and it is worth stating plainly. An entry point costs a hash lookup, and a hop costs nothing extra at all. A graph engine that keyed its adjacency by whatever the user’s id happened to be would pay that lookup on every hop of every walk instead.
Because the id is a handle, Graph::id_of is how you get back to one from
the id you wrote, and it is the only call that costs the lookup.
§One store, several node types
Every node type shares one document collection and one adjacency plane, and
the label keeps them apart. That costs four bytes a node, which is the label
beside each dense id, and it buys the thing that matters: a two hop walk that
crosses from Person to Company is one plane and one contiguous run, not a
join between two stores.
An index is declared per path rather than per type, so two node types that
both index $.name share one index and Graph::find filters the answer by
label. That over-reads when two types share a path and share values, and it
is written down here rather than found later. Two types that index the same
path for different kinds is a conflict and is refused.
Structs§
- EdgeId
- One edge in a graph, which is what
Graph::linkanswers with. - Graph
- A graph.
- Hop
- One step across an edge: where it went, and which edge it was.
- Id
- A node in a graph.
- Walk
- A walk standing on a set of nodes of one type.