Cypher Aliases
gql() speaks ISO GQL. Common Cypher spellings are accepted as aliases where they are
unambiguous and mean exactly the same: an alias is translated to its GQL form while the query is
parsed, so both spellings give the same plan and the same rows. An alias never has a meaning of
its own. This table lists every accepted alias; Cypher functions without any GQL counterpart are
the compatibility functions below, and Cypher's map projections and
comprehensions, COUNT { }, COLLECT { }, SET n += .., MERGE and the regular-expression
match =~ the compatibility forms
(Map projections and comprehensions,
Regular-expression match,
COUNT and COLLECT, SET and REMOVE, MERGE);
any other Cypher
spelling is either not supported yet or refused.
| Cypher spelling | GQL form | Notes |
|---|---|---|
SKIP n | OFFSET n | |
a != b | a <> b | |
s1 + s2 on two strings | s1 || s2 | also on two lists; + of a string and a number is an error (no implicit conversion: write s + toString(n) or s || CAST(n AS STRING)) |
(n:A:B) | (n:A&B) | plain names only; mixing :A:B with |, & or ! is an error (write it with &) |
-[:A|:B]-> | -[:A|B]-> | a colon after the | of a label disjunction |
n:A in an expression | n IS LABELED A | |
--> | -> | abbreviated edge pointing right |
<-- | <- | abbreviated edge pointing left |
-- | - | abbreviated edge, either direction (the -- rule) |
<--> | <-> | either direction |
elementId(x) | element_id(x) | the id as a string |
toUpper(s) | upper(s) | |
toLower(s) | lower(s) | |
size(s) on a string | char_length(s) | size(list) is the item count, as cardinality(list) |
collect(x) | collect_list(x) | also collect(DISTINCT x) |
percentileCont(x, p), percentileDisc(x, p) | percentile_cont(x, p), percentile_disc(x, p) | ISO's binary set functions, value first (Aggregation) |
length(p) | path_length(p) | the number of edges of a path |
log(x) (one argument) | ln(x) | the natural logarithm; the two-argument log(base, x) is GQL's general logarithm |
-[e:T*]-> | -[e:T]->{1,} | Cypher's lower bound is 1: not GQL's postfix *, which is {0,} |
-[e:T*n]-> | -[e:T]->{n} | |
-[e:T*m..n]-> | -[e:T]->{m,n} | |
-[e:T*..n]-> | -[e:T]->{1,n} | lower bound 1 again |
-[e:T*m..]-> | -[e:T]->{m,} | |
-[e:T*m..n]-> with m > n, -[e:T*..0]-> | no match | the empty interval matches nothing (no row; OPTIONAL MATCH gives its NULL row); the GQL {3,1} is a syntax error |
a < b <= c, a chained comparison | a < b AND b <= c | any mix of =, <>, <, <=, >, >=; the middle operand is evaluated once per comparison |
WHERE (a)-[:T]->(b), a pattern predicate | WHERE EXISTS { (a)-[:T]->(b) } | where a condition stands (WHERE, FILTER, under NOT / AND / OR / XOR, a comprehension's WHERE, a searched CASE WHEN; elsewhere 42001); it may name only variables bound before it (EXISTS) |
UNWIND list AS x | FOR x IN list | one row per item; WITH ORDINALITY / WITH OFFSET only after FOR |
CREATE <pattern> | INSERT <pattern> | the same clause: once per row, nodes with label conjunctions (:A:B as :A&B), directed edges with one label (Writing with GQL); CREATE INDEX / CONSTRAINT / GRAPH are refused catalog commands |
CALL { WITH a, b ... }, the importing WITH | CALL (a, b) { ... } | the first WITH of the body, when it lists plain variables only, is the variable scope clause (CALL subqueries) |
SET n:A:B, REMOVE n:A:B | SET n:A, n:B, REMOVE n:A, n:B | the labels in order; SET n IS A & B likewise (Writing with GQL) |
SET (n).k = v, REMOVE (n).k | SET n.k = v, REMOVE n.k | a parenthesized variable as the target |
localtime(x), localdatetime(x), datetime(x) | local_time(x), local_datetime(x), zoned_datetime(x) | the temporal constructors (Dates and times) |
Each pair below returns the same rows:
g.gql("MATCH (n:person) RETURN n.name AS name ORDER BY name SKIP 2").to_list() // #{"name": "peter"}, #{"name": "vadas"}
g.gql("MATCH (n:person) RETURN n.name AS name ORDER BY name OFFSET 2").to_list() // #{"name": "peter"}, #{"name": "vadas"}
g.gql("MATCH (n:person) WHERE n.age != 29 RETURN n.name AS name ORDER BY name").to_list() // josh, peter, vadas
g.gql("MATCH (n:person) WHERE n.age <> 29 RETURN n.name AS name ORDER BY name").to_list() // josh, peter, vadas
g.gql("MATCH (p {name: 'marko'})-->(x) RETURN x.name AS x ORDER BY x").to_list() // josh, lop, vadas
g.gql("MATCH (p {name: 'marko'})->(x) RETURN x.name AS x ORDER BY x").to_list() // josh, lop, vadas
g.gql("MATCH (n) WHERE n:software RETURN n.name AS name ORDER BY name").to_list() // lop, ripple
g.gql("MATCH (n) WHERE n IS LABELED software RETURN n.name AS name ORDER BY name").to_list() // lop, ripple
g.gql("MATCH (n:person) RETURN elementId(n) AS id, toUpper(n.name) AS up, size(n.name) AS len, n.name + '?' AS q ORDER BY id LIMIT 1").to_list()
// #{"id": "1", "len": 5, "q": "marko?", "up": "MARKO"}
g.gql("MATCH (n:person) RETURN element_id(n) AS id, upper(n.name) AS up, char_length(n.name) AS len, n.name || '?' AS q ORDER BY id LIMIT 1").to_list()
// #{"id": "1", "len": 5, "q": "marko?", "up": "MARKO"}
g.gql("MATCH (p:person)-[:created]->(s) RETURN p.name AS name, collect(s.name) AS sw ORDER BY name").to_list()
// #{"name": "josh", "sw": ["ripple", "lop"]}, #{"name": "marko", "sw": ["lop"]}, #{"name": "peter", "sw": ["lop"]}
g.gql("MATCH (p:person)-[:created]->(s) RETURN p.name AS name, collect_list(s.name) AS sw ORDER BY name").to_list()
// #{"name": "josh", "sw": ["ripple", "lop"]}, #{"name": "marko", "sw": ["lop"]}, #{"name": "peter", "sw": ["lop"]}
g.gql("MATCH p = (a {name: 'marko'})-[*1..2]->(b) RETURN b.name AS b, length(p) AS len ORDER BY len, b").to_list()
// josh 1, lop 1, vadas 1, lop 2, ripple 2
g.gql("MATCH p = (a {name: 'marko'})-[]->{1,2}(b) RETURN b.name AS b, path_length(p) AS len ORDER BY len, b").to_list()
// josh 1, lop 1, vadas 1, lop 2, ripple 2
g.gql("MATCH (n:person {name: 'marko'}) RETURN log(100) AS l").to_list() // #{"l": 4.605170185988092}
g.gql("MATCH (n:person {name: 'marko'}) RETURN ln(100) AS l").to_list() // #{"l": 4.605170185988092}
g.gql("MATCH (p:person) WHERE (p)-[:created]->() RETURN p.name AS name ORDER BY name").to_list() // josh, marko, peter
g.gql("MATCH (p:person) WHERE EXISTS { (p)-[:created]->() } RETURN p.name AS name ORDER BY name").to_list() // josh, marko, peter
g.gql("MATCH (p:person {name: 'marko'}) UNWIND [p.age, p.age + 1] AS a RETURN a ORDER BY a").to_list() // #{"a": 29}, #{"a": 30}
g.gql("MATCH (p:person {name: 'marko'}) FOR a IN [p.age, p.age + 1] RETURN a ORDER BY a").to_list() // #{"a": 29}, #{"a": 30}
g.gql("MATCH (p:person) CALL { WITH p MATCH (p)-[:knows]->(f) RETURN count(f) AS n } RETURN p.name AS name, n ORDER BY name").to_list() // josh 0, marko 2, peter 0, vadas 0
g.gql("MATCH (p:person) CALL (p) { MATCH (p)-[:knows]->(f) RETURN count(f) AS n } RETURN p.name AS name, n ORDER BY name").to_list() // josh 0, marko 2, peter 0, vadas 0
g.gql("CREATE (c:city:capital {name: 'Rome'}) RETURN labels(c) AS l").to_list() // #{"l": ["city", "capital"]}
g.gql("INSERT (c:city&capital {name: 'Rome'}) RETURN labels(c) AS l").to_list() // #{"l": ["city", "capital"]}
g.gql("MATCH (p:person {name: 'marko'}) SET p:admin:staff RETURN labels(p) AS l").to_list() // #{"l": ["person", "admin", "staff"]}
g.gql("MATCH (p:person {name: 'marko'}) SET p:admin, p:staff RETURN labels(p) AS l").to_list() // #{"l": ["person", "admin", "staff"]}
g.gql("MATCH (n:person) WHERE 27 <= n.age < 32 RETURN n.name AS name ORDER BY name").to_list() // marko, vadas
g.gql("MATCH (n:person) WHERE 27 <= n.age AND n.age < 32 RETURN n.name AS name ORDER BY name").to_list() // marko, vadas
g.gql("MATCH (a {name: 'marko'})-[*2..1]->(b) RETURN b.name AS b").to_list() // [] (the empty interval)
The importing WITH passes the variables on unchanged; a first WITH that renames, computes or
filters (WITH p AS q, WITH p WHERE ..) is an ordinary WITH of a body that imports
nothing, so reading p there is an error (write CALL (p) { .. }). A CALL { }
without a scope clause and without that WITH imports nothing, as in Neo4j Cypher 25.
Mind the Cypher *: -[*]-> starts at one edge, GQL's -[]->* at zero, so the GQL form
also returns marko himself:
g.gql("MATCH (a {name: 'marko'})-[*]->(b) RETURN b.name AS b ORDER BY b").to_list() // josh, lop, lop, ripple, vadas
g.gql("MATCH (a {name: 'marko'})-[]->*(b) RETURN b.name AS b ORDER BY b").to_list() // josh, lop, lop, marko, ripple, vadas
The Cypher clause WITH
WITH is Cypher; GQL writes the same with its linear statements (LET, FILTER, and
RETURN ... NEXT, UNION and NEXT). gql() accepts WITH with Cypher's
meaning: it passes on only its items, may take DISTINCT, aggregates, ORDER BY, SKIP /
OFFSET, LIMIT and a WHERE (applied last), and a later clause continues from its columns
(The Supported Subset). A WITH that only passes values on is the same as
LET plus a narrower scope, and its WHERE the same as FILTER:
g.gql("MATCH (p:person) WITH p, p.age / 10 AS d WHERE d = 3 RETURN p.name AS name ORDER BY name").to_list() // #{"name": "josh"}, #{"name": "peter"}
g.gql("MATCH (p:person) LET d = p.age / 10 FILTER d = 3 RETURN p.name AS name ORDER BY name").to_list() // #{"name": "josh"}, #{"name": "peter"}
g.gql("MATCH (p:person) RETURN p, p.age / 10 AS d NEXT FILTER d = 3 RETURN p.name AS name ORDER BY name").to_list() // #{"name": "josh"}, #{"name": "peter"}
Spellings that are GQL as well
These look like Cypher but are GQL too, so they are not aliases: : before a label expression
(GQL also has IS), ASC / DESC (also ASCENDING / DESCENDING), string literals in
'...' and "...", // and /* */ comments, $name parameters, back-quoted names.
The functions abs, floor, ceil, sqrt, exp, log10, left, right, ltrim, rtrim,
btrim, trim(s), nullif (Cypher nullIf; names are case-insensitive) and coalesce have the
same name in both languages and are GQL functions. Where Neo4j's version differs, GQL's meaning
wins: trim(s), ltrim(s) and rtrim(s) remove spaces only (Cypher removes any
whitespace), and floor(3) / ceil(3) of an integer stay integers (Neo4j returns floats that
compare equal). ceiling(x) is GQL's second spelling of ceil(x).
Compatibility functions
Some Cypher functions have no GQL counterpart at all, so they cannot be aliases. gql() accepts
a fixed list of them, because Cypher users and LLMs write them all the time, with their
openCypher / Neo4j Cypher 25 meaning: a query using them means the same as in Neo4j, except
where Semantics says otherwise. They never get
a meaning of their own, and no vendor function joins them (apoc.*, text.* are
refused).
| Function | Family | GQL nearest form |
|---|---|---|
toInteger(x), toFloat(x), toBoolean(x), toString(x) | conversions | CAST(x AS INTEGER | FLOAT | BOOLEAN | STRING), but stricter (below) |
toIntegerOrNull(x), toFloatOrNull(x), toBooleanOrNull(x), toStringOrNull(x) | conversions | none |
substring(s, start [, length]) | strings | left(s, n), right(s, n) |
replace(s, search, replacement [, limit]), split(s, delimiter), reverse(x), isEmpty(x) | strings, lists | none |
round(x [, precision [, mode]]), sign(x) | numbers | none |
head(list), last(list), tail(list), range(start, end [, step]) | lists | trim(list, n) drops items from the end |
keys(x), properties(x) | elements | x.key reads one property |
startNode(e), endNode(e) | elements | the predicates n IS SOURCE OF e, n IS DESTINATION OF e |
nodes(p), relationships(p) | paths | GQL's elements(p) (not supported yet) |
all(x IN l WHERE c), any(..), none(..), single(..) | list predicates | none; a list comprehension counted with size() (below) |
reduce(acc = init, x IN l | expr) | lists | none; FOR x IN l and an aggregate fold the rows of a query, not a list per row |
date.truncate(unit, x) and the other X.truncate(), duration.between(a, b), duration.inMonths() / inDays() / inSeconds(), X.statement(), X.transaction(), datetime.fromepoch(), datetime.fromepochmillis(), toString(temporal) | dates and times | the constructors date(), local_time(), ... and duration() (Dates and times) |
CAST is not an alias of toInteger(). Both convert with the same rules, but they treat a
text that does not convert differently: GQL's CAST fails the query with 22018, Cypher's
toInteger / toFloat / toBoolean give NULL, and the ...OrNull forms also give NULL
for a value of the wrong kind. toInteger and toFloat also read Cypher literals ('0x1F',
'1_000'), CAST only decimal text. Use CAST to catch dirty data, toInteger to skip it
(Semantics).
g.gql("MATCH (n:person {name: 'marko'}) RETURN toInteger('42') AS a, CAST('42' AS INTEGER) AS b").to_list() // #{"a": 42, "b": 42}
g.gql("MATCH (n:person {name: 'marko'}) RETURN toInteger('forty') AS a").to_list() // #{"a": ()}
g.gql("MATCH (n:person {name: 'marko'}) RETURN CAST('forty' AS INTEGER) AS b") // error: invalid character value for cast (22018)
g.gql("MATCH (a)-[e:knows]-(b) WHERE startNode(e) = a RETURN a.name AS a, b.name AS b ORDER BY b").to_list() // #{"a": "marko", "b": "josh"}, #{"a": "marko", "b": "vadas"}
g.gql("MATCH (a)-[e:knows]-(b) WHERE a IS SOURCE OF e RETURN a.name AS a, b.name AS b ORDER BY b").to_list() // #{"a": "marko", "b": "josh"}, #{"a": "marko", "b": "vadas"}
List predicates and reduce
all(x IN l WHERE c), any(..), none(..) and single(..) test the items of a list, and
reduce(acc = init, x IN l | expr) folds them: acc starts at init and takes the value of
expr for each item in list order. x (and acc) exist only inside the call and must be new
names, like a list comprehension's variable. The
WHERE is required; an aggregate may stand in the list and in init, not in the condition or
after |. Their NULL rules are the three-valued ones of the openCypher TCK, which Neo4j
Cypher 25 follows:
- a
NULLlist givesNULL(reducetoo), a value that is not a list is error22G03; - a decided answer wins and stops at once:
allis FALSE at the first FALSE item,anyTRUE andnoneFALSE at the first TRUE one,singleFALSE at the second TRUE one (later items are not evaluated); - otherwise an item whose condition is
NULLmakes the answerNULL; - otherwise
allandnoneare TRUE,anyFALSE,singlewhether exactly one item was TRUE; so for an empty listall/noneare TRUE,any/singleFALSE, andreducegivesinit.
A list comprehension counted with size() gives the same answer when no condition is NULL
(it drops a NULL condition instead): all is size([x IN l WHERE c]) = size(l), any is
> 0, none is = 0, single is = 1. Over a pattern comprehension the predicates test the
matches of a pattern per row, the Gremlin where(__..) / not(__..):
g.gql("RETURN all(x IN [1, NULL] WHERE x > 0) AS a, any(x IN [1, NULL] WHERE x > 0) AS b, single(x IN [1, 2] WHERE x > 1) AS c, none(x IN [] WHERE x > 1) AS d").to_list() // #{"a": (), "b": true, "c": true, "d": true}
g.gql("MATCH (p:person) WHERE any(a IN [(p)-[:knows]->(f) | f.age] WHERE a > 30) RETURN p.name AS name").to_list() // #{"name": "marko"}
g.V().hasLabel("person").where(__.out("knows").has("age", P.gt(30))).values("name").to_list() // "marko"
g.gql("MATCH (p:person) WHERE none(w IN [(p)-[:created]->(s) | s] WHERE w.name = 'lop') RETURN p.name AS name").to_list() // #{"name": "vadas"}
g.V().hasLabel("person").not(__.out("created").has("name", "lop")).values("name").to_list() // "vadas"
g.gql("MATCH (p:person) RETURN p.name AS name, reduce(total = 0, w IN [(p)-[e]->() | e.weight] | total + w) AS weight ORDER BY name").to_list()
// josh 1.4, marko 1.9, peter 0.2, vadas 0 (reduce starts at 0)
g.V().hasLabel("person").order().by("name").project("name", "weight").by("name").by(__.out_e().values("weight").fold().sum(Scope.local)).to_list()
// josh 1.4, marko 1.9, peter 0.2, vadas without "weight" (the sum of no values is no value)
g.gql("MATCH (p:person) RETURN reduce(s = '', n IN collect_list(p.name) | s || n) AS names").to_list() // one string of the four names
Regular-expression match
GQL has no regular-expression operator, so Cypher's s =~ pattern is a compatibility form with
Neo4j's meaning: TRUE when the pattern matches the whole string (Java's String.matches),
so 'marko' =~ 'mar' is FALSE and 'marko' =~ 'mar.*' TRUE; an alternation is anchored as a
whole ('josh' =~ 'mark|jos' is FALSE). It binds like STARTS WITH, and NOT s =~ p negates
it. A NULL or non-string operand gives NULL, as for STARTS WITH.
The pattern is written in the syntax of the Rust
regex crate, not Java's: no look-around, no
back-references, matching linear in the text; inline flags such as (?i) work. A pattern that
is no valid expression is error 2201B: a literal or $parameter pattern before the query runs
(it is compiled once), a pattern computed per row when a row reaches it
(Semantics). On a property with a literal or
$parameter pattern it becomes has(key, TextP.regex("^(?:pattern)$"))
(Plans).
g.gql("MATCH (n:person) WHERE n.name =~ 'm.*' RETURN n.name AS name").to_list() // #{"name": "marko"}
g.V().hasLabel("person").has("name", TextP.regex("^(?:m.*)$")).values("name").to_list() // "marko"
g.gql("MATCH (n:person) WHERE n.name =~ '(?i)J.*' RETURN n.name AS name").to_list() // #{"name": "josh"}
g.gql("MATCH (n:person) RETURN n.name AS name, n.name =~ '.*a.*' AS a ORDER BY name").to_list() // josh false, marko true, peter false, vadas true
g.gql("MATCH (n:person) WHERE n.name =~ 'm(' RETURN n.name AS name") // error: invalid regular expression (2201B)
Map projections and comprehensions
Three Cypher expression forms have no GQL counterpart with the same meaning, so they are not
aliases either. gql() accepts them as compatibility forms, like the compatibility
functions: with their openCypher / Neo4j Cypher 25 meaning, because they are how Cypher users and
LLMs ask for nested, JSON-shaped answers (The Supported Subset).
| Cypher form | GQL nearest form | Difference |
|---|---|---|
map projection p {.name, .age, .*, f, k: expr} | the record constructor {name: p.name, age: p.age, f: f, k: expr} (or RECORD {..}) | for a NULL p the projection is NULL, the record {name: NULL, ...}; .* has no record form (properties(p) is the map of all properties) |
list comprehension [x IN list WHERE cond | expr] | none; FOR x IN list FILTER cond RETURN collect_list(expr) gives one list per query, not per row | |
pattern comprehension [(p)-[:R]->(f) WHERE cond | expr] | none; OPTIONAL MATCH (p)-[:R]->(f) WHERE cond + collect_list(expr) groups the rows instead | the comprehension keeps the row and gives [] when nothing matches |
The projection and the record give the same map whenever the variable is not NULL:
g.gql("MATCH (p:person) RETURN p {.name, .age} AS m ORDER BY p.name LIMIT 1").to_list() // #{"m": #{"age": 32, "name": "josh"}}
g.gql("MATCH (p:person) RETURN {name: p.name, age: p.age} AS m ORDER BY p.name LIMIT 1").to_list() // #{"m": #{"age": 32, "name": "josh"}}
g.gql("MATCH (p:person) RETURN p.name AS name, [(p)-[:created]->(s) | s.name] AS sw ORDER BY name").to_list()
// josh [ripple, lop] (in any order), marko [lop], peter [lop], vadas []
g.gql("MATCH (p:person) OPTIONAL MATCH (p)-[:created]->(s) RETURN p.name AS name, collect_list(s.name) AS sw ORDER BY name").to_list()
// josh [ripple, lop], marko [lop], peter [lop], vadas []
Cypher's list predicates all(), any(), none(), single() and reduce() bind a variable
the same way; they are compatibility functions.
COUNT and COLLECT
Cypher's COUNT { } and COLLECT { } subqueries have no GQL counterpart either; gql()
accepts them as compatibility forms with their Neo4j Cypher 25 meaning
(COUNT and COLLECT). They take the bodies of EXISTS: a pattern,
MATCH clauses, or a whole query ending in RETURN (one item for COLLECT).
| Cypher form | GQL nearest form | Difference |
|---|---|---|
COUNT { <pattern or query> } | CALL (p) { OPTIONAL MATCH .. RETURN count(x) AS n }, or OPTIONAL MATCH + count() | the subquery keeps the row and stands in any expression; COUNT { } > 0 is EXISTS { } |
COLLECT { <query> RETURN x } | CALL (p) { OPTIONAL MATCH .. RETURN collect_list(x) AS l } | COLLECT keeps NULL items, collect_list() skips them |
g.gql("MATCH (p:person) RETURN p.name AS name, COUNT { (p)-[:created]->() } AS n ORDER BY name").to_list() // josh 2, marko 1, peter 1, vadas 0
g.gql("MATCH (p:person) CALL (p) { OPTIONAL MATCH (p)-[:created]->(s) RETURN count(s) AS n } RETURN p.name AS name, n ORDER BY name").to_list() // josh 2, marko 1, peter 1, vadas 0
g.gql("MATCH (p:person) RETURN p.name AS name, COLLECT { MATCH (p)-[:created]->(s) RETURN s.name ORDER BY s.name } AS sw ORDER BY name").to_list()
// josh [lop, ripple], marko [lop], peter [lop], vadas []
g.gql("MATCH (p:person) CALL (p) { OPTIONAL MATCH (p)-[:created]->(s) WITH s ORDER BY s.name RETURN collect_list(s.name) AS sw } RETURN p.name AS name, sw ORDER BY name").to_list()
// josh [lop, ripple], marko [lop], peter [lop], vadas []
One difference from Neo4j: a COUNT { } body whose final RETURN only projects is counted
without evaluating its items (Semantics).
SET and REMOVE
GQL's SET items are a property (SET n.k = v), all properties from a braced map
(SET n = {k: v}) and a label (SET n:A / SET n IS A); REMOVE takes a property or a label.
Two Cypher forms have no GQL counterpart and are accepted as compatibility forms with
Cypher's meaning:
SET n += mapmerges the map into the properties: the keys of the map are set (aNULLvalue removes its key), the other properties stay.SET n = exprwith an expression that is not a braced map: a$parameteror any map value replaces every property, and a node or an edge stands for its properties, soSET n = mcopies them (SET n = nchanges nothing). WriteSET n = properties(m)for the same as a plain map.
A NULL on the right of = or += is an error (22004) rather than an empty map; write
coalesce($map, {}) (Semantics). This is our choice for the
compatibility form, where Neo4j removes every property: standard GQL has no such form, since its
SET n = {..} takes a braced map only.
g.gql("MATCH (p:person {name: 'marko'}) SET p += {city: 'Rome'} RETURN properties(p) AS props").to_list() // #{"props": #{"age": 29, "city": "Rome", "name": "marko"}}
g.gql("MATCH (p:person {name: 'marko'}) SET p = $props RETURN properties(p) AS props", #{props: #{name: "marko", city: "Rome"}}).to_list() // #{"props": #{"city": "Rome", "name": "marko"}}
g.gql("MATCH (p:person {name: 'marko'}), (q:person {name: 'josh'}) SET p = q RETURN properties(p) AS props").to_list() // #{"props": #{"age": 32, "name": "josh"}}
g.gql("MATCH (p:person {name: 'marko'}) SET p += $more", #{more: ()}) // error: the right side of SET p += .. is NULL, not a map
MERGE
Cypher's MERGE ("find or create") has no ISO GQL counterpart. gql() accepts it as a
compatibility form with Neo4j Cypher 25's meaning, and with it its ON CREATE SET and
ON MATCH SET actions:
| Cypher | In gql() | Notes |
|---|---|---|
MERGE <pattern> | supported as a compatibility form | one path pattern per clause; per row, every match is a row, or the whole pattern is created; each row sees the rows before it (Writing with GQL) |
ON CREATE SET <items>, ON MATCH SET <items> | supported as a compatibility form | the items of SET, several actions in any order, applied in the order written |
MERGE p = <pattern> | supported | the path found or created |
MERGE (a)-[:T]-(b) without a direction | supported | matches either direction, creates from left to right |
Plain GQL writes the same with a CALL body that inserts only when nothing exists:
g.gql("MERGE (c:city {name: 'Berlin'}) RETURN c.name AS name").to_list() // #{"name": "Berlin"}
g.gql("CALL { FILTER NOT EXISTS { MATCH (:city {name: 'Berlin'}) } INSERT (:city {name: 'Berlin'}) } MATCH (c:city {name: 'Berlin'}) RETURN c.name AS name").to_list() // #{"name": "Berlin"}
The differences from Neo4j are on Semantics.
Dates and times
The temporal constructors have an ISO GQL name and a Cypher name; the Cypher names are aliases (the same function, the same value). The qualified Cypher functions have no ISO spelling: they are compatibility functions with Neo4j's meaning (Dates and times).
| Cypher | ISO GQL | Notes |
|---|---|---|
date(x) | date(x) | the same name |
localtime(x) | local_time(x) | a time of day without an offset |
localdatetime(x) | local_datetime(x) | |
datetime(x) | zoned_datetime(x) | an instant with its UTC offset (and its zone name) |
time(x) | zoned_time(x) | a time of day with its UTC offset |
duration(x) | duration(x) | the same name |
date.truncate(), localtime.truncate(), time.truncate(), localdatetime.truncate(), datetime.truncate() | none | compatibility functions |
duration.between(), duration.inMonths(), duration.inDays(), duration.inSeconds() | none | compatibility functions; ISO's duration_between() is an unknown function that suggests duration.between() |
X.statement(), X.transaction(); datetime.fromepoch(), datetime.fromepochmillis() | none | compatibility functions; the clocks give the time the query started |
| X.realtime() | none | refused: a clock that moves while the query runs |
g.gql("RETURN datetime('2015-07-21T21:40+01:00') = zoned_datetime('2015-07-21T21:40+01:00') AS a, localtime('12:00') = local_time('12:00') AS b, localdatetime('2015-07-21T21:40') = local_datetime('2015-07-21T21:40') AS c").to_list()
// #{"a": true, "b": true, "c": true}
g.gql("RETURN zoned_time('12:00+01:00') = time('12:00+01:00') AS t").to_list() // #{"t": true}
Cypher spellings that are not aliases
| Cypher | In gql() |
|---|---|
id(n) | refused: an integer in Neo4j, Graphersal ids are strings; use element_id(n) |
nodes(p), relationships(p) | supported as compatibility functions, not aliases: GQL has no such functions |
x ^ y | supported as a compatibility operator: GQL has no operator for it; it means power(x, y) (always a float, the same errors), binding tighter than * and looser than a unary minus |
rand(), randomUUID() | refused: gql() functions are deterministic |
time(), time.*(); date.realtime() and the other X.realtime() | time() not supported yet (no time-with-offset type: use localtime() or datetime()); X.realtime() refused, like rand() (Dates and times) |
| map projections, list and pattern comprehensions | supported as compatibility forms, not aliases |
any() / all() / none() / single(), reduce() | supported as compatibility functions, not aliases: GQL has no such functions |
COUNT { }, COLLECT { } | supported as compatibility forms, not aliases |
CALL { } IN TRANSACTIONS | refused: a write form; the CALL itself is supported |
exists(n.p) (Cypher 4) | refused: EXISTS takes a pattern; test a property with n.p IS NOT NULL |
GROUP BY (not Cypher) | ISO GQL, accepted after the RETURN items (optional: grouping is implicit); after WITH an error that explains implicit grouping |
MERGE | supported as a compatibility form, with ON CREATE SET / ON MATCH SET |
FOREACH | not supported yet (Writing with GQL); CREATE is an alias of INSERT; SET and REMOVE are supported, += as a compatibility form |
DELETE, DETACH DELETE | the same in GQL, supported (Writing with GQL); GQL also writes the default explicitly as NODETACH DELETE |
SET n[key] = v, SET n:$(expr) (Cypher 25 dynamic keys and labels) | not supported; name the key and the label in the query |
CALL proc(..) YIELD .. | supported for saved queries written in GQL: CALL folder.name(..) with YIELD, YIELD * and the standalone forms of Cypher (Calling saved queries) |
CALL db.*(), dbms.*, apoc.*, gds.*, SHOW ... | refused: vendor extensions |
a--1 (minus minus one) | a comment starts at -- (GQL wins): RETURN n.age--1 AS x returns the column n.age; write n.age - -1 |