Skip to content

3.4 Predicate pushdown — scope the fetch at the source

MATCH (p:Person {name:'Grace Hopper'})-[:HAS_GITHUB]->(g)-[:RAISED]->(i:GitHubIssue)
WHERE i.html_url CONTAINS 'acme/app'
RETURN i.title

Means: “Grace’s issues in the acme/app repo.”

How it executes (with pushdown):

  • Probe + resolve the bridge as before → g.login = 'devone'.
  • Fetch — pushed down. Instead of fetching all of devone’s issues and discarding the ones not in acme/app, the engine renders the WHERE i.html_url CONTAINS 'acme/app' predicate into the source’s native filter: the GitHub search becomes is:issue author:devone repo:acme/app. One scoped search returns only the matching issues.
  • Materialize / run / roll back as usual. The same WHERE still runs in the graph too, so correctness never depends on pushdown — it only changes cost and coverage.

How it executes without a pushdown rule declared: the engine fetches devone’s issues broadly (paged, capped) and the graph-side WHERE drops non-matches. Correct, but wasteful, and it can hit the source’s result cap before your matches (see §3.5).

Declared: a pushdown: rule on the issuesByAuthor producer mapping the html_url predicate to a repo:{value} qualifier (with a valuePattern regex to extract owner/repo from a URL).

A captured realm’s handler producer declares pushdown as property and argument pairs. The handler receives every value an = or IN allows as a list of strings under that argument, and applies it however its source spells a filter. The graph still runs the WHERE, so the guarantee above holds unchanged (hosted profile).

If your source’s query is JSON, not text — an Odoo domain, an Elasticsearch bool.filter, a GraphQL where: — declare argPath + clause instead of qualifier. The clause is placed in the producer’s own args, addressed the way keyArg is, with a trailing - meaning “append here”:

pushdown:
- property: status
op: IN
argPath: domain.-
clause: ["status", "in", "{values}"]

"{values}" becomes the real member list, so the source gets JSON rather than text shaped like it. Appending is what keeps an existing keyArg: "domain.0.2" pointing at the triple it always did.

Why it is worth declaring, beyond saving a page. What the source absorbs decides what else the engine may do. With every predicate on the target absorbed, a LIMIT can stop the page walk (n rows fetched are n rows answered; SKIP s LIMIT n stops it at s + n, since the skipped rows must be read to be skipped), and a count() can be asked of the source instead of fetched — one call for the whole question rather than one per anchor. Neither is safe while the graph still has filtering left to do, so an undeclared filter costs more than its own page.

A read is reused for the narrower questions inside it. Within a producer’s cache window, the engine does not re-read the source for a question that a cached complete read for the same key already contains: the unfiltered read of an account’s cases serves WHERE c.status = 'open' over the same account, and a LIMIT 50 read serves LIMIT 10 in the same declared order, each with no call and reported as cached. It never works the other way — the filtered read does not serve the unfiltered one, and a truncated read serves nothing but itself — so in a family of views, run the broadest first (VIRTUAL_CYPHER §5.13).

When there is nothing to declare, say so: pushdown: []. A thread endpoint that returns the whole conversation, a list keyed by status and nothing else — some sources apply no filter at all, and every WHERE on their target is the graph’s by construction. An absent pushdown: key cannot express that; it reads as “not declared yet”, so the realm is warned at load and every read past a handful of records carries advice to declare a rule that cannot exist. A declared empty list is the author’s statement that the source filters nothing: the load warning goes, the read’s cost note says the source applies no filters, and EXPLAIN names the residual without telling you to declare it. The note itself stays, because it is what marks the read as complete and its empty as exact.