Skip to content

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_url

Means: “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_GITHUB is an identity bridge — §5.2). Run Grace’s resolve: chain: is there already a GitHubIdentity linked to her (existingBridge)? a learned githubLogin on her node (learnedHandle)? otherwise look her email up via the githubUsersByEmail producer (canonicalEmail). Say it resolves to login devone. The bridge (p)-[:HAS_GITHUB]->(:GitHubIdentity {login:'devone'}) is persisted (write-through) so the next query skips this step.
  • Now g is a real, bound node. Its key is login = 'devone'.
  • Fetch the downstream join — call the issuesByAuthor producer with ['devone']. It searches GitHub for author:devone, returns the issue records.
  • Materialize each as (:GitHubIssue:Virtual) linked (g)-[:RAISED]->(i).
  • Run / roll back — RETURN reads 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.