GQL Conformance

This page lists which parts of ISO/IEC 39075 (GQL) gql() supports, in the style of the conformance appendices that GQL implementations publish. It is maintained by hand together with the code; when a construct lands, its row moves here in the same change.

GQL names its optional features with identifiers such as G036 (graph pattern features), GQ.. (query features), GE.. (expressions), GF.. (functions), GA.. (additional behaviour), GB.. (lexical), GL.. (literals), GH.. (other). This page cites only those identifiers and short descriptive names of our own; it does not reproduce the standard. An identifier marked (provisional) could not be checked against the standard text yet and may be corrected.

How far the Cypher compatibility forms agree with Neo4j's behaviour is measured against the openCypher TCK: TCK Compliance.

Status: supported, partial (what is missing is named), not yet (planned), no (refused for good; see Refused Constructs).

Core language

The mandatory core of a GQL query, by construct.

ConstructStatusNotes
MATCH with node and edge patterns, comma-separated patterns, several MATCH clausessupportedtrail (DIFFERENT EDGES) match mode per clause
WHERE after MATCH, inline WHERE and property maps in element patternssupported
RETURN, RETURN *, RETURN DISTINCT, ASsupportedRETURN is mandatory unless the last clause writes; FINISH ends a query without a result
ORDER BY ... ASC / DESCsupportedNULL last ascending, first descending
label expressions &, |, !, ( )supported
property reference x.keysupported
comparison, AND / OR / NOT, IS [NOT] NULLsupportedthree-valued logic
arithmetic + - * / %, string concatenation ||supported
simple and searched CASE, COALESCE, NULLIFsupported
UPPER, LOWER, CHAR_LENGTH / CHARACTER_LENGTH, LEFT, RIGHT, TRIM(s)supportedTRIM removes spaces by default; NORMALIZE not yet
BYTE_LENGTH / OCTET_LENGTH of a character stringsupportedthe number of UTF-8 bytes; Graphersal has no byte strings
list trim TRIM(list, n)supporteddrops the last n items
parameters $namesupportedvalues only, never text
aggregate functions COUNT, SUM, AVG, MIN, MAX, COLLECT_LIST, with DISTINCTsupportedimplicit grouping by the items without an aggregate; Cypher collect alias
binary set functions PERCENTILE_CONT, PERCENTILE_DISCsupportedvalue first, then the percentile (0.0 to 1.0, 22003 outside); Cypher percentileCont / percentileDisc aliases (stage 3f)
OPTIONAL MATCHsupportedalso as the first clause
EXISTS predicate: EXISTS { MATCH ... }, EXISTS { <pattern> }, EXISTS ( ... ) (provisional: whether the standard makes it core or an optional feature, and its identifier, are still to be checked)supportedanywhere an expression stands, nested too; TRUE or FALSE, never NULL; the body is a pattern, MATCH clauses, or a whole query ending in RETURN (EXISTS); Cypher pattern predicates as aliases
Cypher WITHsupportednot GQL; accepted with Cypher's meaning (Cypher Aliases)
record constructor {k: expr, ...} / RECORD {k: expr, ...} and list value constructor [a, b]supportedRECORD is contextual: a variable named record stays usable, back-quoted before a projection that starts with key: or is empty (Expressions)
Cypher map projections p {.k, .*, x, k: expr}, list comprehensions [x IN l WHERE c | e], pattern comprehensions [(a)-->(b) WHERE c | e]supportednot GQL; accepted as compatibility forms with Cypher's meaning (Cypher Aliases); a pattern comprehension is a subquery; all() / any() / none() / single() / reduce() likewise, three-valued as the openCypher TCK (Cypher Aliases)
Cypher COUNT { } and COLLECT { } subqueriessupportednot GQL; compatibility forms with Cypher's meaning (Cypher Aliases); bodies as for EXISTS, COLLECT returns one item; one deviation below
a query without MATCH (RETURN 1)supporteda query may start with FOR, LET, WITH, FILTER or RETURN; it starts from one empty row
data-modifying statements: INSERTsupportednodes with label conjunctions and properties, directed edges with one label, once per row; Cypher CREATE as an alias (Writing with GQL)
data-modifying statements: SET, REMOVEsupportedproperty items n.k = v, all properties n = {..}, label items n:A / n IS A; once per row, items left to right, NULL removes; Cypher's += and SET n = <expression> as compatibility forms (Writing with GQL)
data-modifying statements: DELETEsupportedDETACH DELETE and [NODETACH] DELETE of nodes, edges, paths and lists of them, once per row; NODETACH checked per clause (G1001); reading a deleted element afterwards is an error (Writing with GQL, Semantics)
Cypher MERGE with ON CREATE SET / ON MATCH SETsupportednot GQL; a compatibility form with Cypher's meaning (Cypher Aliases): one pattern, per row every match a row or the whole pattern created, rows see earlier rows' creations, NULL in the pattern 22004 (Writing with GQL, Semantics)
catalog statements (CREATE GRAPH, CREATE GRAPH TYPE, ...), session and transaction commandsnothe host application manages graphs, schemas and transactions
value typespartialthe GQL types Graphersal has (integers, floats, strings, booleans, lists, records/maps, dates and times, NULL); path values; no byte strings

Optional features

IDFeatureStatusNotes
G002different-edges match modepartialthe semantics is the default and only mode; writing DIFFERENT EDGES is not accepted yet
G004path variablessupportedp = (a)-->(b), path_length(p) (Cypher length); Cypher's nodes(p) / relationships(p) as compatibility functions
G005path search prefix in a path patternnot yetANY, ALL, SHORTEST prefixes
G010explicit WALK keywordnot yet
G011advanced path modes: TRAILnot yettrail is the semantics of every MATCH; the keyword is not accepted yet
G012advanced path modes: SIMPLEnot yet
G013advanced path modes: ACYCLICnot yet
G016any path searchnot yet
G017all shortest path searchnot yetGremlin repeat()
G018any shortest path searchnot yetGremlin repeat()
G019counted shortest path searchnot yet
G035quantified paths (provisional)partiala parenthesized path pattern with {m,n}, {n}, {m,}, {,n}, *, + (Quantified path patterns); body variables are group variables (lists), the outer nodes merge with the body's first and last node, trail per body edge; one chain per body, no quantifier, path variable, path mode or search prefix inside a body, a body variable is a new name (each refused with 0A000)
G036quantified edgessupported{m,n}, {n}, {m,}, {,n}, *, + on full and abbreviated edges; Cypher [*m..n] and its forms are aliases; trail per clause
G037questioned paths (provisional)supported((..))?: the body once or not at all; its variables are single elements, NULL when it did not match (Semantics)
G043complete full edge patternspartialpointing left, right, either direction (-[e]-, <-[e]->); the undirected forms (~[e]~ and its variants) are refused, Graphersal edges are directed
G044basic abbreviated edge patternssupported->, <-, - (Cypher -->, <--, --)
G045complete abbreviated edge patternspartial<-> supported; ~, <~, ~> refused
G050parenthesized path pattern: WHERE clause (provisional)partial((x)-[e]->(y) WHERE <condition>) with or without a quantifier, run in every iteration; it reads the body's variables, parameters and values (a condition reading an outer variable is not yet supported); a parenthesized path pattern without a quantifier matches once and keeps singletons
G074label expression: wildcard labelsupported%
G100ELEMENT_ID functionsupportedthe id as a string; Cypher elementId alias
G110IS DIRECTED predicate (provisional)not yetevery edge is directed
G111IS LABELED predicatesupportedCypher x:A alias
G112IS SOURCE OF / IS DESTINATION OF predicates (provisional)supportedn IS [NOT] SOURCE OF e, n IS [NOT] DESTINATION OF e: the stored direction of the edge; Cypher's startNode(e) / endNode(e) as compatibility functions
G113ALL_DIFFERENT predicate (provisional)not yet
G114SAME predicate (provisional)not yet
G115PROPERTY_EXISTS predicate (provisional)not yetx.key IS NOT NULL tests a property
GA03explicit ordering of nullsnot yetNULLS FIRST / NULLS LAST
GA04universal comparison (provisional)partial= / <> between values of different kinds are FALSE / TRUE, the orderings NULL (Neo4j's reading, see Differences from the standard); there is no total order across kinds outside ORDER BY
GA05cast specificationpartialCAST(x AS INTEGER | FLOAT | STRING | BOOLEAN) and their synonyms (INT, BIGINT, DOUBLE, VARCHAR, BOOL, ...); the other target types (DATE, LIST<..>, DECIMAL, ...) not yet; strict: unconvertible text is 22018
GA07ordering by discarded binding variables (provisional)supportedORDER BY may read variables that RETURN does not return (not after RETURN DISTINCT)
GB02double minus sign commentssupported-- ..., with the edge rule of Writing GQL in Rhai Strings
GB03double solidus commentssupported// ...
GD01updatable graphs (provisional)partialINSERT (Cypher CREATE), SET, REMOVE, DELETE; Cypher's MERGE (no ISO counterpart) as a compatibility form (Writing with GQL); the Cypher forms pass every in-scope case of the openCypher TCK's write clauses (TCK Compliance)
GD02graph label set changes (provisional)supportedSET n:A / SET n IS A and REMOVE n:A add and remove node labels; an edge's single label is not changed by them (Writing with GQL)
GD03DELETE statement: subquery support (provisional)supporteda DELETE item may hold a subquery, as in DETACH DELETE COLLECT { MATCH (s)<-[e]-() RETURN e }; a CALL { } body may end with a DELETE (Writing with GQL)
GD04DELETE statement: simple expression support (provisional)supporteda DELETE item is an expression: a variable, a path, a list (collect(n)), a map member, a function result; NULL is skipped, another value is 42002 / 22G03 (Semantics)
GE07Boolean XORsupported
GF01enhanced numeric functionssupportedABS, FLOOR, CEIL / CEILING, SQRT, MOD; an integer argument keeps its type
GF02trigonometric functions (provisional)not yetGremlin math() after gql()
GF03logarithmic functionssupportedEXP, LN, LOG10, LOG(base, x) (provisional), POWER; an argument outside the domain is an error (2201E, 2201F), never NaN
GF04enhanced path functionspartialPATH_LENGTH; ELEMENTS(p) (provisional) not yet (Cypher's nodes(p) / relationships(p) instead)
GF05multi-character trim functionssupportedBTRIM, LTRIM, RTRIM with an optional set of characters (default: the space)
GF06explicit TRIM functionsupportedTRIM([LEADING | TRAILING | BOTH] [c] FROM s); c is one character (22027 otherwise), the space by default
GF12CARDINALITY functionsupported
GF13SIZE functionsupportedalso on strings as the Cypher alias of CHAR_LENGTH
GH02undirected edge patternsnoGraphersal edges are directed; -[e]- matches either direction
GL01hexadecimal literalssupported0x1F
GL02octal literalssupported0o17
GL03binary literalssupported0b101
GQ01USE graph clausenogql() runs on the graph of its traversal source
GQ02composite query: OTHERWISE (provisional)not yet
GQ03composite query: UNIONsupportedUNION, UNION DISTINCT, UNION ALL; operands return the same column names in any order; one kind per chain; ORDER BY / OFFSET / LIMIT per operand (UNION and NEXT)
GQ04composite query: EXCEPT DISTINCT (provisional)not yet
GQ06composite query: INTERSECT DISTINCT (provisional)not yet
GQ08FILTER statementsupportedFILTER [WHERE] <condition>
GQ09LET statementsupported
GQ10FOR statement: list value supportsupportedFOR x IN <list> (Cypher UNWIND <list> AS x), also as the first clause; a NULL list gives no row, a value that is not a list one row (Semantics)
GQ11FOR statement: WITH ORDINALITY (provisional)supportedthe position from 1
GQ24FOR statement: WITH OFFSET (provisional)supportedthe position from 0
GQ12ORDER BY and page statement: OFFSET clausepartialOFFSET / SKIP after RETURN and in WITH; the standalone statement form not yet
GQ13ORDER BY and page statement: LIMIT clausepartialLIMIT after RETURN and in WITH; the standalone statement form not yet
GQ14complex expressions in sort keys (provisional)supportedORDER BY element_id(e), ORDER BY n.age * 2
GQ15GROUP BY clause (provisional)supportedafter the RETURN items, GROUP BY () included; the keys must be the items without an aggregate (grouping is implicit, so it is optional); a key may also repeat an item's expression (GROUP BY p.name), beyond ISO's variable references
GQ18scalar subqueries (provisional)not yet
GQ20advanced linear composition with NEXTpartialRETURN .. NEXT .., also after a UNION and inside subquery bodies; NEXT YIELD not yet
GQ21OPTIONAL: multiple MATCH statements (provisional)not yetone OPTIONAL MATCH per optional part is supported; the block form OPTIONAL { MATCH ... MATCH ... } is not
GP01inline procedure (provisional)supported[OPTIONAL] CALL { <query> }, run once per incoming row, the body ending in RETURN (CALL subqueries) or, as a unit subquery, in a write (Writing with GQL); IN TRANSACTIONS refused
GP02inline procedure with implicit nested variable scope (provisional)partialCALL { } without a scope clause is accepted but imports nothing (Neo4j Cypher 25's reading; the ISO scoping is still to be checked); import with CALL (*) or Cypher's importing WITH
GP03inline procedure with explicit nested variable scope (provisional)supportedCALL (a, b) { }, CALL (*) { }, CALL () { }
GP04named procedure calls (provisional)partial[OPTIONAL] CALL folder.name(args) [YIELD ..] calls a saved query written in GQL of the graph's catalog, once per row, standalone or inside a query (Calling saved queries); arguments are constants or parameters; a saved query in Rhai and a call inside EXISTS / COUNT / COLLECT not yet; vendor procedures (CALL db.*, dbms.*, apoc.*, gds.*) are refused
GV39temporal types: date, local datetime and local time (provisional)partialdate(), local_time(), local_datetime() (Cypher localtime(), localdatetime()) from ISO 8601 text, a map of fields or another temporal value, components (d.year), arithmetic with durations, comparison (Dates and times); values hold nanoseconds; the literals DATE '..', TIME '..' / LOCAL TIME '..', TIMESTAMP '..' / LOCAL DATETIME '..' and the clocks CURRENT_DATE, LOCAL_TIME, LOCAL_TIMESTAMP (Literals); CAST(x AS DATE) not yet
GV40temporal types: zoned datetime and zoned time (provisional)partialzoned_datetime() (Cypher datetime()) keeps a UTC offset and the zone name it was built with, its arithmetic following the zone; zoned_time() (Cypher time()) keeps a time of day and its offset (Dates and times); the literals TIME '10:00+01:00', DATETIME '..+01:00' (also ZONED TIME '..', ZONED DATETIME '..') and the clocks CURRENT_TIME, CURRENT_TIMESTAMP (Literals); CAST(x AS ZONED TIME) not yet
GV41temporal types: duration (provisional)partialduration() from text or a map, the literal DURATION '..', + / - / * / /, components; ISO DURATION_BETWEEN not yet (the Cypher compatibility function duration.between() instead); < on two durations is NULL

Every feature not listed here is not supported. Cypher spellings that GQL does not have, and the openCypher functions accepted without a GQL counterpart (toInteger, substring, split, round, keys, nodes, the temporal date.truncate(), duration.between(), ...), are on Cypher Aliases.

Implementation-defined choices

Where the standard leaves a choice to the implementation, gql() makes this one (Neo4j's, where it has one):

TopicChoice
NULL in ORDER BYlast ascending, first descending
order of values of different typesmap, vertex, edge, list, date/time, UUID, string, boolean, number, vector, NULL
row order without ORDER BYunspecified
item order of a pattern comprehensionunspecified: the order the matches are found, which depends on the plan
item order of COLLECT { }the order of the body's rows: its ORDER BY when it has one, unspecified otherwise
UNION operands with the same column names in another orderaccepted, matched by name; the result has the first operand's column order (provisional)
order of the rows of a UNIONunspecified, also when the operands are sorted: sort after NEXT
a CALL body that imports nothingstill run once per incoming row; its rows are not cached
the RETURN items of a projecting COUNT { } bodynot evaluated: the rows are counted, an expression that would fail raises no error (a deviation, Semantics)
a comprehension variable named like another variable of the queryan error (42002) for a name in scope or one a LET / WITH value in scope still reads (stricter than Neo4j only there); a name no longer in scope may be used again (Semantics)
DISTINCT, grouping keys, DISTINCT in aggregatesGQL equality: 1 and 1.0 are one value, NULLs form one group
aggregates of no rows (no grouping keys)one row: count 0, sum 0, collect_list [], avg / min / max NULL (Neo4j's values, which the openCypher TCK confirms; the ISO text is still to be checked)
min / max over values of different kindsthe ORDER BY order, never an error
sum of integersan integer; overflow is an error (22003)
unbounded quantifiersno own limit: trail ends them; repeat.max_loops and traversal.max_traversers are errors, never a shortened result (quantified edges and quantified path patterns alike)
outer nodes next to a quantified path patternmerged with the body's first node of the first iteration and last node of the last iteration (Neo4j Cypher 25's reading)
order of a group variable's listthe iterations in the order the text reads, also when the plan walks the pattern from its far end
WITH ... LIMIT ... WHEREthe WHERE filters after ORDER BY / OFFSET / LIMIT (Cypher's order; provisional)
name of an unaliased columnthe expression as written (Neo4j's names, such as count(*), which the openCypher TCK confirms; the ISO text is still to be checked)
element idsstrings, the same as Gremlin's id(); vertex and edge ids are separate spaces
integer divisiontruncates toward zero; % takes the sign of the dividend
temporal precisionnanoseconds
default time zoneUTC: a datetime without an offset or zone, and the clocks without a zone
the current date and timeone instant per query, taken when it starts; X.realtime() is refused
ORDER BY of durationsby length, a month counted as 30 days (< on two durations is NULL)
CAST(temporal AS STRING)the engine's ISO 8601 text ('12:00:00'); Cypher's toString() writes Neo4j's ('12:00')
integer overflowan error (22003), never a wrap
text and numbersno implicit conversion: 'a' + 1 is an error (22G03); convert with CAST(x AS STRING)
CAST of a float to INTEGER, of text with surrounding spacestruncated toward zero; the spaces are skipped (provisional)
CAST to STRING of a float, a boolean, a list or a mapJava's float text ('1.0', '1.0E7'), 'true' / 'false', Neo4j's list and map text (provisional)
CAST(x AS BOOLEAN) of a number; of 'unknown'22G03; NULL (provisional)
default character of TRIM, BTRIM, LTRIM, RTRIMthe space U+0020 only (provisional)
BTRIM / LTRIM / RTRIM with charactersany character of the set is removed (Neo4j's semantics; provisional)
functions and NULLa NULL argument gives NULL, before the kinds are checked
strings in functionspositions and lengths count characters (Unicode scalar values)
GQLSTATUS codes42001 for syntax errors; the others provisional (Errors)
order of the writes of one INSERTper row: its nodes in text order, then its edges in text order
property values of INSERTNULL is not stored; maps and lists of any kinds are stored as given (a difference from Neo4j)

Differences from the standard

Where ISO GQL and Neo4j disagree and the openCypher TCK measures the Neo4j behaviour, gql() follows Neo4j in these places (user decision, stage 3f):

TopicISO GQLgql()
= / <> / IN / simple CASE between values of different kindsnot comparable= FALSE, <> TRUE (1 = '1', [1] = 1, 1 IN ['1']); the orderings < .. >= stay NULL (Semantics)
< <= > >= on two listsnot comparablelexicographic, three-valued
a float divided by zero22012IEEE 754: Infinity, -Infinity or NaN; an integer divided by zero stays 22012
NaN in a comparison(no NaN arises: every division by zero is 22012)never equal (NaN = NaN FALSE), every ordering against a number FALSE

The deliberate differences from Neo4j are listed on Semantics.