Skip to content

Cypher Reference

GraphForge implements the full openCypher language on the v0.5.0 Rust core. Run queries through forge.execute(query, params=None) — every call returns a PyArrow Table (no CypherValue wrappers).

This guide covers everyday clauses, patterns, expressions, and functions. For install and first-graph steps, start with Quick Start; for programmatic construction, see Graph Construction.

Compliance: See TCK Compliance for the current openCypher TCK corpus gate.



Find nodes and relationships matching a pattern.

-- All nodes
MATCH (n) RETURN n
-- Nodes with label
MATCH (p:Person) RETURN p
-- Nodes with multiple labels
MATCH (e:Person:Employee) RETURN e
-- Nodes with property filter
MATCH (p:Person {name: 'Alice'}) RETURN p
-- Directed relationship
MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
-- Undirected relationship
MATCH (a:Person)-[:KNOWS]-(b:Person) RETURN a, b
-- Multiple relationship types
MATCH (a)-[r:KNOWS|LIKES]->(b) RETURN a, r, b
-- Variable-length paths
MATCH (a:Person)-[:KNOWS*1..3]->(b:Person) RETURN a, b
-- Unbounded variable-length
MATCH (a)-[*]->(b) RETURN a, b
-- Named path
MATCH p = (a:Person)-[:KNOWS*]->(b:Person) RETURN p
-- Multi-hop
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:WORKS_AT]->(c:Company)
RETURN a.name, b.name, c.name

Left-outer-join semantics: returns NULL for missing matches.

MATCH (p:Person)
OPTIONAL MATCH (p)-[:KNOWS]->(friend:Person)
RETURN p.name AS person, friend.name AS friend -- friend.name is NULL if no match
-- Multiple optional matches
MATCH (p:Person)
OPTIONAL MATCH (p)-[:WORKS_AT]->(company:Company)
OPTIONAL MATCH (p)-[:LIVES_IN]->(city:City)
RETURN p.name, company.name, city.name

Filter by any boolean expression.

-- Comparison
MATCH (p:Person) WHERE p.age > 25 RETURN p
-- NULL check
MATCH (p:Person) WHERE p.email IS NULL RETURN p
MATCH (p:Person) WHERE p.email IS NOT NULL RETURN p
-- Boolean operators
MATCH (p:Person) WHERE p.age > 25 AND p.city = 'NYC' RETURN p
MATCH (p:Person) WHERE p.age < 20 OR p.age > 60 RETURN p
MATCH (p:Person) WHERE NOT p.active RETURN p
-- Label predicate
MATCH (n) WHERE n:Person RETURN n
-- String predicates
MATCH (p:Person) WHERE p.name STARTS WITH 'Al' RETURN p
MATCH (p:Person) WHERE p.name ENDS WITH 'ice' RETURN p
MATCH (p:Person) WHERE p.name CONTAINS 'lic' RETURN p
-- Regex
MATCH (p:Person) WHERE p.name =~ 'A.*' RETURN p
-- List membership
MATCH (p:Person) WHERE p.city IN ['NYC', 'Boston', 'London'] RETURN p
-- Pattern predicate (not returning path)
MATCH (p:Person) WHERE (p)-[:KNOWS]->(:Person {name: 'Alice'}) RETURN p
MATCH (p:Person) WHERE NOT (p)-[:KNOWS]->() RETURN p
-- EXISTS subquery
MATCH (p:Person)
WHERE EXISTS { MATCH (p)-[:KNOWS]->(:Person) }
RETURN p.name
-- Property existence (short form)
MATCH (p:Person) WHERE exists(p.email) RETURN p

Project variables, properties, expressions, and aggregations.

-- Return variable
MATCH (p:Person) RETURN p
-- Return property
MATCH (p:Person) RETURN p.name, p.age
-- Alias
MATCH (p:Person) RETURN p.name AS person, p.age AS age
-- DISTINCT
MATCH (p:Person) RETURN DISTINCT p.city
-- Aggregation (implicit GROUP BY on non-aggregated columns)
MATCH (p:Person) RETURN p.city AS city, count(*) AS total
-- All properties (map)
MATCH (p:Person) RETURN p {.*}
-- Arithmetic in return
MATCH (p:Person) RETURN p.name, p.salary * 1.1 AS after_raise
-- CASE expression
MATCH (p:Person)
RETURN p.name,
CASE
WHEN p.age < 18 THEN 'Minor'
WHEN p.age < 65 THEN 'Adult'
ELSE 'Senior'
END AS category
-- Return * (all variables in scope)
MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN *
-- Sort ascending (default)
MATCH (p:Person) RETURN p.name ORDER BY p.age
-- Sort descending
MATCH (p:Person) RETURN p.name ORDER BY p.age DESC
-- Multiple sort keys
MATCH (p:Person) RETURN p.name ORDER BY p.city ASC, p.age DESC
-- Limit results
MATCH (p:Person) RETURN p.name ORDER BY p.age LIMIT 10
-- Pagination
MATCH (p:Person) RETURN p.name ORDER BY p.name SKIP 20 LIMIT 10

Create nodes and relationships.

-- Single node
CREATE (p:Person {name: 'Alice', age: 30})
-- Multiple labels
CREATE (e:Person:Employee {name: 'Charlie'})
-- Multiple nodes in one clause
CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
-- Relationship between existing nodes
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)
-- Create inline
CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})
-- CREATE ... RETURN
CREATE (p:Person {name: 'Alice'})
RETURN p.name AS name, id(p) AS node_id

Find or create — safe for idempotent upserts.

-- Merge node (create if not exists, match if exists)
MERGE (p:Person {email: 'alice@example.com'})
-- ON CREATE and ON MATCH callbacks
MERGE (p:Person {email: 'alice@example.com'})
ON CREATE SET p.created = 2024, p.name = 'Alice'
ON MATCH SET p.last_seen = 2024
-- Merge relationship (both endpoints must already exist)
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
MERGE (a)-[:KNOWS]->(b)
-- Merge with RETURN
MERGE (p:Person {email: 'alice@example.com'})
RETURN p

Expand a list into rows.

UNWIND [1, 2, 3] AS x RETURN x * 2 AS doubled
-- Common pattern: batch-create from list
UNWIND ['Alice', 'Bob', 'Charlie'] AS name
CREATE (:Person {name: name})
-- Expand collected list
MATCH (p:Person)
WITH collect(p.name) AS names
UNWIND names AS name
RETURN name

Add or update properties and labels.

-- Set property
MATCH (p:Person {name: 'Alice'}) SET p.age = 31
-- Multiple properties
MATCH (p:Person {name: 'Alice'})
SET p.age = 31, p.city = 'Boston', p.active = true
-- Set from map
MATCH (p:Person {name: 'Alice'})
SET p += {age: 31, city: 'Boston'}
-- Overwrite all properties
MATCH (p:Person {name: 'Alice'})
SET p = {name: 'Alice', age: 31}
-- Add label
MATCH (p:Person {name: 'Alice'}) SET p:Manager
-- SET ... RETURN
MATCH (p:Person {name: 'Alice'})
SET p.age = 31
RETURN p.name AS name, p.age AS new_age

Remove properties and labels.

-- Remove property
MATCH (p:Person {name: 'Alice'}) REMOVE p.temp
-- Remove multiple
MATCH (p:Person {name: 'Alice'}) REMOVE p.temp, p.draft
-- Remove label
MATCH (p:Person {name: 'Alice'}) REMOVE p:Manager
-- Delete a relationship
MATCH (a)-[r:KNOWS]->(b) WHERE a.name = 'Alice' DELETE r
-- Delete a node (no relationships may exist)
MATCH (p:Person {name: 'Alice'}) DELETE p
-- DETACH DELETE: remove node and all its relationships
MATCH (p:Person {name: 'Alice'}) DETACH DELETE p
-- Delete everything
MATCH (n) DETACH DELETE n

Pass results between query stages, filter aggregations, limit scope.

-- Chain match with filter
MATCH (p:Person)-[:KNOWS]->(friend)
WITH p, count(friend) AS friends
WHERE friends > 5
RETURN p.name AS person, friends
-- Rename and continue
MATCH (p:Person)
WITH p.name AS name, p.age AS age
WHERE age > 25
RETURN name, age
-- Aggregate then match
MATCH (p:Person)
WITH p.city AS city, count(*) AS pop
WHERE pop > 100
MATCH (p2:Person {city: city})
RETURN p2.name, city, pop
ORDER BY pop DESC
-- WITH DISTINCT
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WITH DISTINCT friend
RETURN friend.name AS mutual_friend
-- UNION (removes duplicates)
MATCH (p:Person) RETURN p.name AS name
UNION
MATCH (c:Company) RETURN c.name AS name
-- UNION ALL (keeps duplicates)
MATCH (p:Person) RETURN p.name AS name
UNION ALL
MATCH (p2:Person) RETURN p2.name AS name

Pattern Meaning
(n) Any node
(n:Person) Node with label Person
(n:Person:Employee) Node with both labels
(n:Person {age: 30}) Node with label and property
(:Person) Anonymous node with label
({name: 'Alice'}) Anonymous node with property
Pattern Meaning
-[r]-> Any directed relationship
-[:KNOWS]-> Specific type
-[r:KNOWS {since: 2020}]-> Type with property
-[:KNOWS|LIKES]-> Multiple types (OR)
-[*]-> Variable-length (any)
-[*2]-> Exactly 2 hops
-[*1..3]-> 1 to 3 hops
-[*..5]-> Up to 5 hops
-[*3..]-> 3 or more hops
-- Bind path to variable
MATCH p = (a:Person)-[:KNOWS*]->(b:Person)
RETURN length(p) AS hops, nodes(p) AS path_nodes
-- Shortest path
MATCH p = shortestPath((a:Person {name: 'Alice'})-[:KNOWS*]->(b:Person {name: 'Bob'}))
RETURN p

RETURN 5 + 3 -- 8
RETURN 5 - 3 -- 2
RETURN 5 * 3 -- 15
RETURN 5 / 2 -- 2.5
RETURN 5 % 2 -- 1
RETURN 2 ^ 10 -- 1024.0
RETURN 5 = 5 -- true
RETURN 5 <> 6 -- true
RETURN 5 < 6 -- true
RETURN 5 <= 5 -- true
RETURN 6 > 5 -- true
RETURN 6 >= 6 -- true
RETURN true AND false -- false
RETURN true OR false -- true
RETURN NOT true -- false
RETURN true XOR true -- false
-- NULL propagation
RETURN null AND true -- null
RETURN null OR true -- true
RETURN NOT null -- null
RETURN 'hello' + ' world' -- 'hello world'
RETURN 'Alice' STARTS WITH 'Al' -- true
RETURN 'Alice' ENDS WITH 'ice' -- true
RETURN 'Alice' CONTAINS 'lic' -- true
RETURN 'Alice' =~ 'A.*' -- true (regex)
RETURN [1, 2, 3] + [4, 5] -- [1, 2, 3, 4, 5]
RETURN 3 IN [1, 2, 3] -- true
RETURN [1, 2, 3][0] -- 1
RETURN [1, 2, 3][1..3] -- [2, 3]
-- NULL propagates through most operations
RETURN null + 5 -- null
RETURN null = null -- null (use IS NULL instead)
-- Safe operators
MATCH (p:Person) WHERE p.age IS NULL RETURN p
MATCH (p:Person) WHERE p.age IS NOT NULL RETURN p
-- Simple form
RETURN CASE p.status
WHEN 'active' THEN 'Active User'
WHEN 'inactive' THEN 'Inactive User'
ELSE 'Unknown'
END
-- Generic form
RETURN CASE
WHEN p.age < 18 THEN 'Minor'
WHEN p.age < 65 THEN 'Adult'
ELSE 'Senior'
END

Function Example Result
toLower(s) toLower('HELLO') 'hello'
toUpper(s) toUpper('hello') 'HELLO'
trim(s) trim(' hi ') 'hi'
ltrim(s) ltrim(' hi') 'hi'
rtrim(s) rtrim('hi ') 'hi'
replace(s, f, r) replace('aaa', 'a', 'b') 'bbb'
substring(s, start, len) substring('hello', 1, 3) 'ell'
left(s, n) left('hello', 2) 'he'
right(s, n) right('hello', 2) 'lo'
split(s, delim) split('a,b,c', ',') ['a','b','c']
reverse(s) reverse('hello') 'olleh'
size(s) size('hello') 5
toString(x) toString(42) '42'
Function Description
abs(n) Absolute value
ceil(n) Round up
floor(n) Round down
round(n) Round to nearest
sqrt(n) Square root
pow(base, exp) Power
exp(n) e^n
log(n) Natural log
log10(n) Base-10 log
sin(n), cos(n), tan(n) Trig (radians)
asin(n), acos(n), atan(n), atan2(y,x) Inverse trig
pi() π (3.14159…)
e() e (2.71828…)
rand() Random 0.0–1.0
sign(n) -1, 0, or 1
Function Example Result
head(list) head([1,2,3]) 1
tail(list) tail([1,2,3]) [2,3]
last(list) last([1,2,3]) 3
size(list) size([1,2,3]) 3
range(start, end) range(1,5) [1,2,3,4,5]
range(start, end, step) range(0,10,2) [0,2,4,6,8,10]
reverse(list) reverse([1,2,3]) [3,2,1]
sort(list) sort([3,1,2]) [1,2,3]
keys(map) keys({a:1,b:2}) ['a','b']
-- Filter a list
RETURN [x IN range(1,10) WHERE x % 2 = 0] -- [2,4,6,8,10]
-- Transform a list
RETURN [x IN range(1,5) | x * x] -- [1,4,9,16,25]
-- Filter + transform
RETURN [x IN range(1,10) WHERE x > 5 | x * 2] -- [12,14,16,18,20]
-- all() — every element satisfies predicate
RETURN all(x IN [2,4,6] WHERE x % 2 = 0) -- true
-- any() — at least one element satisfies predicate
RETURN any(x IN [1,2,3] WHERE x > 2) -- true
-- none() — no element satisfies predicate
RETURN none(x IN [1,2,3] WHERE x > 5) -- true
-- single() — exactly one element satisfies predicate
RETURN single(x IN [1,2,3] WHERE x = 2) -- true
-- reduce() — fold list to single value
RETURN reduce(acc = 0, x IN [1,2,3,4,5] | acc + x) -- 15
-- Count rows
MATCH (p:Person) RETURN count(*) AS total
-- Count non-null
MATCH (p:Person) RETURN count(p.age) AS with_age
-- Count distinct
MATCH (p:Person) RETURN count(DISTINCT p.city) AS cities
-- Numeric aggregations
MATCH (p:Person) RETURN sum(p.salary), avg(p.age), min(p.age), max(p.age)
-- Collect into list
MATCH (p:Person) RETURN collect(p.name) AS names
-- Collect distinct
MATCH (p:Person) RETURN collect(DISTINCT p.city) AS cities
-- Standard deviation
MATCH (p:Person) RETURN stDev(p.age), stDevP(p.age)
-- Percentile
MATCH (p:Person) RETURN percentileDisc(p.age, 0.5) AS median
-- Node identity
RETURN id(n)
-- Labels
RETURN labels(n) -- list of labels
RETURN 'Person' IN labels(n) -- true/false
-- Relationship type
RETURN type(r)
-- Properties (as map)
RETURN properties(n)
-- Keys (property names)
RETURN keys(n)
-- Path functions
MATCH p = (a)-[*]->(b)
RETURN nodes(p), relationships(p), length(p)
-- Endpoints
MATCH (a)-[r]->(b)
RETURN startNode(r), endNode(r)
-- Degree
MATCH (n:Person)
RETURN size((n)-[:KNOWS]->()) AS out_degree
-- exists() — property exists
MATCH (p:Person) WHERE exists(p.email) RETURN p
-- isEmpty()
RETURN isEmpty([]) -- true
RETURN isEmpty('') -- true
RETURN isEmpty(null) -- true
-- Null coalescing
RETURN coalesce(null, null, 'default') -- 'default'
RETURN coalesce(p.nickname, p.name) -- first non-null
RETURN toInteger('42') -- 42
RETURN toInteger(3.7) -- 3
RETURN toFloat('3.14') -- 3.14
RETURN toString(42) -- '42'
RETURN toBoolean('true') -- true
RETURN toBoolean(0) -- false

GraphForge implements full openCypher temporal precision including nanoseconds and IANA timezone names.

RETURN date('2024-01-15')
RETURN date({year: 2024, month: 1, day: 15})
RETURN date() -- current date
RETURN time('14:30:00.000000789') -- with nanoseconds
RETURN time({hour: 14, minute: 30, second: 0, nanosecond: 789})
RETURN datetime('2024-01-15T14:30:00[Europe/London]') -- IANA timezone
RETURN datetime('2024-01-15T14:30:00+01:00') -- offset
RETURN datetime() -- current datetime
RETURN localDatetime('2024-01-15T14:30:00')
RETURN duration('P1Y2M3DT4H5M6.789S')
RETURN duration({years: 1, months: 2, days: 3})
RETURN date('2024-01-15').year -- 2024
RETURN date('2024-01-15').month -- 1
RETURN date('2024-01-15').day -- 15
RETURN date('2024-01-15').weekday -- 1 (Monday)
RETURN date('2024-01-15').week -- 3 (ISO week)
RETURN date('2024-01-15').ordinalDay -- 15
RETURN time('14:30:00.000000789').hour -- 14
RETURN time('14:30:00.000000789').nanosecond -- 789
RETURN datetime('2024-01-15T14:30:00[Europe/Stockholm]').timezone
-- 'Europe/Stockholm'
RETURN duration('P1Y2M3DT4H5M6S').years -- 1
RETURN duration('P1Y2M3DT4H5M6S').months -- 2
RETURN duration('PT0.000000789S').nanoseconds -- 789
RETURN date('2024-01-01') + duration('P1M') -- 2024-02-01
RETURN date('2024-01-01') - duration('P1Y') -- 2023-01-01
RETURN duration('P1Y') + duration('P6M') -- P1Y6M
-- Between
RETURN duration.between(date('2020-01-01'), date('2024-01-01'))
RETURN duration.inMonths(date('2020-01-01'), date('2024-01-01'))
RETURN duration.inDays(datetime('2020-01-01T00:00'), datetime('2020-01-15T12:00'))
RETURN duration.inSeconds(time('08:00'), time('16:30'))

GraphForge supports years outside Python’s native range (1–9999):

RETURN localdatetime('+999999999-12-31T23:59:59').year -- 999999999
RETURN localdatetime('-000001-01-01T00:00').year -- -1
RETURN date.truncate('month', date('2024-07-15')) -- 2024-07-01
RETURN datetime.truncate('day', datetime()) -- current day at midnight
RETURN time.truncate('hour', time('14:37:00')) -- 14:00:00

Bind values as parameters to avoid string interpolation:

# Python
table = forge.execute(
"MATCH (p:Person {name: $name}) WHERE p.age > $min_age RETURN p",
{"name": "Alice", "min_age": 25},
)
-- In Cypher, $name and $min_age are the parameter syntax
MATCH (p:Person {name: $name})
WHERE p.age > $min_age
RETURN p.name, p.age

Parameters work with all value types: strings, integers, floats, booleans, null, lists, maps.


MATCH (me:Person {name: 'Alice'})-[:KNOWS]->(friend)-[:KNOWS]->(foaf:Person)
WHERE NOT (me)-[:KNOWS]->(foaf) AND me <> foaf
RETURN DISTINCT foaf.name AS recommendation
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name AS person, count(friend) AS connections
ORDER BY connections DESC
LIMIT 10
MERGE (p:Person {email: 'alice@example.com'})
ON CREATE SET p.name = 'Alice', p.created = 2024
ON MATCH SET p.last_seen = 2024
RETURN p
UNWIND [
{name: 'Alice', age: 30},
{name: 'Bob', age: 25},
{name: 'Carol', age: 35}
] AS row
CREATE (:Person {name: row.name, age: row.age})
MATCH (p:Person)
RETURN p.name,
CASE WHEN p.age IS NULL THEN 'unknown'
WHEN p.age < 18 THEN 'minor'
ELSE 'adult'
END AS category
-- Nodes that have at least one outgoing relationship
MATCH (p:Person)
WHERE EXISTS { MATCH (p)-[:KNOWS]->() }
RETURN p.name
-- Nodes that have no relationships
MATCH (p:Person)
WHERE NOT EXISTS { MATCH (p)-[]-() }
RETURN p.name AS isolated