3.2 A bridge plus a downstream hop (Person → GitHub identity → issues)
MATCH (p:Person {name:'Grace Hopper'})-[:HAS_GITHUB]->(g:GitHubIdentity)-[:RAISED]->(i:GitHubIssue)RETURN i.title, i.html_urlMeans: “What GitHub issues has Grace raised?” — but the graph holds no GitHub data at all. Two virtual hops: first resolve which GitHub account is Grace, then fetch that account’s issues.
How it executes:
- Probe — bind
p(Grace), read her email. - Resolve the bridge (
HAS_GITHUBis an identity bridge — §5.2). Run Grace’sresolve:chain: is there already aGitHubIdentitylinked to her (existingBridge)? a learnedgithubLoginon her node (learnedHandle)? otherwise look her email up via thegithubUsersByEmailproducer (canonicalEmail). Say it resolves to logindevone. The bridge(p)-[:HAS_GITHUB]->(:GitHubIdentity {login:'devone'})is persisted (write-through) so the next query skips this step. - Now
gis a real, bound node. Its key islogin = 'devone'. - Fetch the downstream join — call the
issuesByAuthorproducer with['devone']. It searches GitHub forauthor:devone, returns the issue records. - Materialize each as
(:GitHubIssue:Virtual)linked(g)-[:RAISED]->(i). - Run / roll back —
RETURNreads the issues; the issues roll back (the bridge stays, as a warm cache).
Declared: GitHubIdentity with a resolve: bridge join from Person; GitHubIssue with a
plain virtualJoins entry { anchorLabel: GitHubIdentity, relationship: RAISED, keyField: login, producer: issuesByAuthor }.
Why two declarations, not one. The bridge (who-is-Grace) and the data (her issues) have different lifecycles — the identity is stable and worth persisting; the issues are volatile and roll back. Splitting them lets the engine cache the expensive identity resolution and re-fetch only the cheap, changing part.

