Expressions¶
GQL supports a rich expression language for conditional logic, type conversion, subqueries and list operations.
CASE Expressions¶
Searched CASE¶
Evaluate conditions in order and return the first matching result:
MATCH (p:Person)
RETURN p.name,
CASE
WHEN p.age < 18 THEN 'minor'
WHEN p.age < 65 THEN 'adult'
ELSE 'senior'
END AS category
Simple CASE¶
Compare an expression against specific values:
MATCH (p:Person)
RETURN p.name,
CASE p.status
WHEN 'A' THEN 'Active'
WHEN 'I' THEN 'Inactive'
WHEN 'P' THEN 'Pending'
ELSE 'Unknown'
END AS status_label
NULLIF¶
Returns null if both arguments are equal, otherwise returns the first argument. Useful for avoiding division by zero:
COALESCE¶
Returns the first non-null value from the argument list:
-- Display name fallback chain
MATCH (p:Person)
RETURN COALESCE(p.nickname, p.firstName, p.email) AS display_name
CAST¶
Convert a value to a different type with CAST(expr AS type):
-- Numeric conversions
RETURN CAST('42' AS INT) -- 42
RETURN CAST(42 AS FLOAT) -- 42.0
RETURN CAST('1.50' AS DECIMAL) -- 1.50, exact with its scale
RETURN CAST(2.345 AS DECIMAL(10, 2)) -- 2.35, rounded half away from zero
RETURN CAST(123.4 AS DEC(4, 2)) -- fails with 22003: more integer digits than DEC(4, 2) holds
RETURN CAST(42 AS STRING) -- '42'
RETURN CAST('true' AS BOOLEAN) -- true
-- Temporal conversions
RETURN CAST('2024-01-15' AS DATE)
RETURN CAST('14:30:00' AS TIME)
RETURN CAST('2024-01-15T14:30:00Z' AS DATETIME)
RETURN CAST('P1Y2M' AS DURATION)
-- Zoned temporal conversions
RETURN CAST('2024-01-15T14:30:00+05:30' AS ZONED DATETIME)
RETURN CAST('14:30:00+05:30' AS ZONED TIME)
-- Wrap a scalar in a list
RETURN CAST(42 AS LIST) -- [42]
Supported target types: INT/INTEGER, FLOAT, DECIMAL/DEC (bare, or with a precision from 1 to 38 and a scale up to it: DECIMAL(10, 2), DEC(5)), STRING, BOOLEAN, DATE, TIME, DATETIME, DURATION, ZONED DATETIME, ZONED TIME, LIST, LIST<type>.
A cast of text to an integer, a float or a decimal trims the spaces around it and reads the number it spells; to an integer it is truncated toward zero (CAST(' 4.7 ' AS INT) is 4). Text that spells no number fails with 22018 (CAST('Alix' AS INT)), as SQL does, where toInteger('Alix') is NULL. A float outside the integer range, an infinity or NaN cast to an integer fails with 22003, and so does a value with more integer digits than a declared decimal holds (CAST(12345.6 AS DECIMAL(5, 2))). A value that cannot convert otherwise, such as a date cast to an integer, is NULL.
Dynamic Parameters¶
Use $param placeholders in queries and provide values from the host language:
// Node.js
const result = await db.execute(
"MATCH (p:Person) WHERE p.name = $name RETURN p",
{ name: "Alix" }
);
A list or map parameter nests at most 128 levels deep, the limit of a stored value. A deeper one is refused with an error before the query runs, in every binding.
LET ... IN ... END¶
Bind local variables within an expression. The variables are scoped to the expression only.
MATCH (p:Person)
RETURN p.name,
LET base = p.salary,
bonus = base * 0.1
IN base + bonus
END AS total_compensation
List Comprehensions¶
Transform and filter lists in a single expression:
-- Filter: keep only even numbers
RETURN [x IN [1, 2, 3, 4, 5] WHERE x % 2 = 0] AS evens
-- [2, 4]
-- Transform: double each value
RETURN [x IN [1, 2, 3] | x * 2] AS doubled
-- [2, 4, 6]
-- Filter and transform
RETURN [x IN [1, 2, 3, 4, 5] WHERE x > 2 | x * 10] AS result
-- [30, 40, 50]
-- With graph data
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name,
[f IN collect(friend) WHERE f.age > 30 | f.name] AS older_friends
List Predicates¶
Test whether list elements satisfy a condition:
-- all(): every element must match
RETURN all(x IN [2, 4, 6] WHERE x % 2 = 0) -- true
-- any(): at least one element must match
RETURN any(x IN [1, 2, 3] WHERE x > 2) -- true
-- none(): no element must match
RETURN none(x IN [1, 2, 3] WHERE x > 5) -- true
-- single(): exactly one element must match
RETURN single(x IN [1, 2, 3] WHERE x = 2) -- true
-- Practical example: check path properties
MATCH path = (a:Person)-[:KNOWS*]->(b:Person)
WHERE a.name = 'Alix'
AND all(n IN nodes(path) WHERE n.active = true)
RETURN path
reduce()¶
Fold a list into a single value using an accumulator:
-- Sum a list
RETURN reduce(acc = 0, x IN [1, 2, 3, 4, 5] | acc + x) AS total
-- 15
-- Build a string
RETURN reduce(s = '', name IN ['Alix', 'Gus', 'Harm'] | s + name + ', ') AS names
-- Compute from graph data
MATCH (p:Person)
WITH collect(p.salary) AS salaries
RETURN reduce(total = 0, s IN salaries | total + s) AS payroll
Subquery Expressions¶
EXISTS¶
Check whether a pattern or subquery has any results:
-- Pattern exists
MATCH (p:Person)
WHERE EXISTS { MATCH (p)-[:MANAGES]->(:Team) }
RETURN p.name AS manager
-- Subquery with filtering
MATCH (p:Person)
WHERE EXISTS {
MATCH (p)-[:ORDERED]->(o:Order)
WHERE o.total > 1000
}
RETURN p.name AS high_value_customer
COUNT¶
Count the number of results from a subquery:
VALUE¶
Return a single scalar value from a subquery:
MATCH (p:Person)
RETURN p.name,
VALUE {
MATCH (p)-[:KNOWS]->(friend)
RETURN count(friend)
} AS friend_count
Index Access¶
Access list elements by index (0-based):
RETURN [10, 20, 30, 40][0] -- 10
RETURN [10, 20, 30, 40][2] -- 30
RETURN [10, 20, 30, 40][-1] -- 40, a negative index counts from the end
-- Slices: from the first bound up to, not including, the second
RETURN [10, 20, 30, 40][1..3] -- [20, 30]
RETURN [10, 20, 30, 40][-2..] -- [30, 40]
RETURN [10, 20, 30, 40][..-2] -- [10, 20]
RETURN 'Grafeo'[-3..] -- 'feo', strings slice the same way
-- With graph data
MATCH (p:Person)
RETURN p.tags[0] AS first_tag
Map Literals¶
Create and access map (dictionary) values:
-- Map literal
RETURN {name: 'Alix', age: 30, active: true} AS person
-- Access map properties
LET person = {name: 'Alix', age: 30}
RETURN person.name, person.age
-- Maps in mutations
MATCH (p:Person {name: 'Alix'})
SET p += {city: 'Amsterdam', role: 'engineer'}
A property can hold a map. Dotted access reads its keys, also nested, the same as a subscript; a missing key, or a value that is not a map, gives null. A column without an alias is named as written:
INSERT (:Doc {id: 'd1', meta: {route: 'directory', score: 0.5, source: {kind: 'file'}}})
MATCH (d:Doc)
WHERE d.meta.route = 'directory'
RETURN d.meta.score, d.meta.source.kind, d.meta['route']
-- columns: d.meta.score, d.meta.source.kind, d.meta['route']
Dotted access reads a key of a map value (a property, a variable, a parameter, a map literal or properties(n)), and the property of a node or edge that an expression returns, such as startNode(r).name. It also reads the result of an aggregate in the same clause: in head(collect(n)).name, collect(n.name)[0] and {names: collect(n.name)} the aggregate is computed first, also inside LET ... IN ... END.
SESSION_USER¶
Returns the current session user name. In embedded databases, this returns 'default'.
Introspection Functions¶
Query session state and database metadata:
-- Current schema name
RETURN CURRENT_SCHEMA
-- Current graph name
RETURN CURRENT_GRAPH
-- Database metadata (version, node count, edge count)
RETURN info()
-- Schema information (node types, edge types, indexes, constraints)
RETURN schema()
String Concatenation¶
The || operator concatenates strings:
LIKE Pattern Matching¶
Match strings against patterns using % (any characters) and _ (single character):
MATCH (p:Person)
WHERE p.name LIKE 'A%'
RETURN p.name
MATCH (p:Person)
WHERE p.email LIKE '%@example.com'
RETURN p.name, p.email
IN Operator¶
Test membership in a list: