5.11 Enumerated anchors — WHERE anchor.key IN [...]
A virtual anchor is normally pinned to ONE value ((q:IntegrityQuery {abn:'…'})). The BATCH form
is an enumerated IN over literals, and it behaves identically — one anchor per value:
MATCH (q:IntegrityQuery)-[:HAS_TAX_RECORD]->(x:TaxRecord)WHERE q.abn IN ['31010545267', '29008423005', '46221314841']RETURN q.abn, x.name, x.taxPayableThis is the shape every screening question takes — these twenty suppliers against that register — and it fetches ONCE for the batch (the producer’s batch contract), not once per value.
Guarantees
- Each value is probed for a REAL node first, exactly as a pinned anchor is; only values with no real node are minted as virtual anchors. A mixed list works.
- Duplicates collapse: one anchor per distinct value.
- An anchor the source answered for is there even when the answer is none.
MATCH (r:GitHubRepository {full_name:'acme/empty'}) OPTIONAL MATCH (r)-[:HAS_ISSUE]->(i) RETURN r.full_name, count(i)answersacme/empty, 0, and anINlist mixing an empty value with a full one keeps the empty one’s row. A value the source answered 404 for, or whose fetch failed, is not minted; the answer carries that warning instead (§9). - An anchor nothing fetched is explained, not answered empty.
MATCH (r:GitHubRepository {full_name:'acme/gears'}) RETURN r.full_nametraverses no join, so nothing asked the source whether the repository exists, and a row built from the query’s own literal would assert it on no evidence. The empty answer is typed as a query defect,ANCHOR_UNFETCHED, and its hint names the label’s joins and theOPTIONAL MATCHthat would answer. A label with stored instances and no match for the pin is a true negative and gets no hint. - It composes with the rest of the WHERE. An
INcombined with other conditions byANDstill seeds (this matters more than it looks: scope conditions are ANDed onto every query, so an enumerated anchor is essentially always inside a compound). - A
nullin the list seeds nothing, and voids nothing.IN ['a', null]seeds'a'— as Cypher’sINis true only for'a'. A boolean member seeds as its token. - A member that is computed is resolved. An expression (
toLower('ACME/WIDGETS')), a value bound by an earlierWITHorUNWIND, or a list-valued variable (WITH ['a','b'] AS ks … IN ks) is evaluated before the fetch, exactly as{key: expression}is, and seeds what it evaluates to. - Alternatives on one key enumerate their union.
k IN ['a'] OR k = 'b'seeds'a'and'b'— the same set written two ways. - A list narrowed by a ranked
LIMITfetches for the anchors theLIMITkeeps. InMATCH (s:Shop) WHERE s.code IN ['s1', …, 's6'] WITH s ORDER BY s.rank LIMIT 3 MATCH (s)-[:HAS_STOCK]->(x)the hop is asked for the three shops the ranking keeps, not the six the list names. That holds when every listed value resolves to a stored node and theLIMITis ranked by anORDER BYover this anchor. Where it does not — a value no stored node holds (a minted anchor has no sort key to rank by), an unrankedLIMIT(the k a probe keeps need not be the k the query keeps), or an anchor label this query is itself fetching — every listed anchor is bound and fetched for, and the graph narrows afterwards: a fuller fetch, never a wrong k.
A parameter list seeds exactly as the literal list does. WHERE q.abn IN $abns with abns
supplied answers the same rows as the same values written inline — supplied parameters are
substituted before the query is read. A parameter nobody supplied is refused, never read as an empty
list.
A fetched node pinned to a value bound upstream is narrowed to it. In
MATCH … WITH x MATCH (a)-[:HAS_FIELD]->(f:Field {key: x})-[:ACCESSED_BY]->(m), the node a join
FETCHES (f, not the anchor) is pinned to x, which an earlier clause bound. Its values are
resolved before the fetch, and only the matching rows are kept — handed to a source that applies IN
filters as one, filtered on receipt otherwise — so the next hop (ACCESSED_BY) is asked for those
keys alone, as it would be for {key: 'C.f3'} written inline. The same holds for WHERE f.key = x
(either operand order), f.key IN xs, and a pin to another node’s property bound earlier
({owner: frame.className}). WHERE f.key = x pins exactly as {key: x} does when the condition is
an AND of terms; under an OR it is a filter, and a comparison between the properties of two
nodes of the same MATCH (a.id = b.ownerId) is a join, never a pin. When an EARLIER MATCH bound
b, the comparison is the inline pin {id: b.ownerId} and pins as that does, either operand order.
What it guarantees and what it does not:
- The same rows as without it. Narrowing never drops a row the query keeps;
OPTIONAL MATCHkeeps its null rows. - It holds back rather than guess. When the earlier part of the query still reads data not yet fetched, when there are more than 5,000 distinct values, when none resolve, or when a value is not a plain scalar, the node is fetched in full and the pin applied afterwards — a fuller fetch, never a wrong answer.
- It narrows what is fetched only where the source can be asked for it. A source keyed by something else (a whole-collection producer) still reads its collection; only what is kept, and every later hop, shrinks. A realm that wants the read itself narrowed declares a producer selected by that property.
An anchor keyed by a value computed from fetched rows is fetched for each value. In
MATCH (me:AssistantUser)-[:HAS_LOG_EVENT]->(e:LogEvent)-[:HAS_FRAME]->(f:JavaStackFrame)WITH DISTINCT f.jarPath AS jarMATCH (j:Jar)-[:HAS_PACKAGE]->(p:JavaPackage) WHERE j.path = jarRETURN jar, count(p)the jar is keyed by every value jar holds once the frames are fetched — however many fetches
before it they are — exactly as (j:Jar {path: 'jars/app.jar'}) keys it. The same holds for
{path: jar} inline, for a key read straight off a node an earlier MATCH bound
(WHERE j.path = f.jarPath), and for a key aggregated or unwound upstream; a computed pin on the
fetched node (… UNWIND causes AS cause … WHERE j.path = jar AND c.calleeOwner = cause) applies on
top, as above.
- Never an empty answer that is not one. If the values can never be read — the earlier part of
the query reads data this query does not fetch — the join is not fetched and the answer carries a
PARTIAL_RESULTwarning naming the anchor, the value its key is computed from, and the join.
What does NOT seed, and why
- A filter that cannot list keys —
STARTS WITH,CONTAINS, a range, a regex on the key. Nothing can be fetched, and the answer SAYS so: aNEEDS_FILTERwarning names the label and the key to pin, so an empty result is never read as “there are none”. The same warning accompanies an anchor that is not pinned at all. - An
ORacross different properties or nodes —WHERE q.abn IN [...] OR q.name = '…'. One branch of such a disjunction must not enumerate the anchor set. (Alternatives on the SAME key are an enumeration, above.) - The mirror form —
WHERE 'X' IN n.someListnarrows a node by list membership; it is a filter, not an enumeration of identities.

