# Syntax
The lexical layer every statement obeys, whichever language it is written in: the notation
this reference uses for syntax, and the rules for identifiers, literals, operators, comments
and null.
Cypher, GQL and SQL share one parser, so these rules are the same in all three. Where a rule
differs between them, the difference is called out.
## Syntax conventions
Every synopsis in this reference is written the same way:
MATCH pattern [, pattern ] ... [ WHERE condition ]
Nothing else in a synopsis is notation. A comma, a parenthesis or an arrow is part of the
statement, and you type it as shown.
**bold**
: A keyword. Type it as it appears; case does not matter.
*italic*
: A placeholder you supply. Each one is defined under *Parameters* on the same page.
`[ ]`
: The enclosed part is optional.
`{ ... | ... }`
: Braces around alternatives separated by a vertical bar: choose exactly one.
`...`
: The item before it may be repeated.
`[, ... ]`
: The item before it may be repeated, separated by commas.
Two or more synopsis lines
: Alternative forms of the same statement. Use one of them.
`where` *placeholder* `is one of:`
: Expands a placeholder that is too large to sit inline, on the lines that follow.
Brackets and braces are never literal. Some statements really do contain a bracket: a list
literal, a map, an edge pattern. For those, the synopsis says so in prose, or shows the
bracket in a different position, as in `-[` *variable* `]->`.
## Identifiers
An identifier names a graph, a label, a variable, a property key, a column, an index or a
constraint.
### Case folding
An unquoted identifier is folded to **lower case**, exactly as PostgreSQL folds an unquoted
SQL identifier. So `MOVIE`, `Movie` and `movie` all name the same label:
```cypher
MATCH (n:MOVIE {title: 'The Matrix'}) RETURN n.title AS title;
```
```
title
--------------
"The Matrix"
```
A double-quoted identifier is taken as written. `"MOVIE"` is a different name from `movie`.
No label is called `MOVIE`, so the pattern matches nothing rather than failing:
```cypher
MATCH (n:"MOVIE" {title: 'The Matrix'}) RETURN n.title AS title;
```
```
title
-------
(0 rows)
```
Quoting works for variables too, and a quoted variable has to be quoted at every mention:
```cypher
MATCH ("Film":movie {title: 'The Matrix'}) RETURN "Film".title AS title;
```
```
title
--------------
"The Matrix"
```
Property keys fold the same way. `{MixedKey: 1}` stores the key `mixedkey`; `{"MixedKey": 1}`
stores `MixedKey`. The two are different keys and a vertex can carry both, so quote a key
consistently or not at all.
Backticks are not a quoting character. `` `movie` `` is a syntax error, not a quoted
identifier:
```cypher
MATCH (n:`movie`) RETURN n.title;
```
```
ERROR: syntax error at or near "`"
LINE 1: MATCH (n:`movie`) RETURN n.title;
^
```
### Characters
An unquoted identifier starts with a letter or an underscore and continues with letters,
digits, underscores and dollar signs. "Letter" includes non-ASCII letters, so `café` is a
valid identifier. It may not start with a digit. `1abc` is read as a number with trailing
junk. A double-quoted identifier may contain any character except a null byte, and a double
quote inside one is written twice (`"say ""hi"""`).
The length limit is PostgreSQL's: 63 bytes, and a longer name is truncated to that. The
limit counts bytes, not characters, so a name of multi-byte letters runs out sooner.
### Reserved words
Some keywords are **reserved**: the grammar reads the word as a keyword and not as a name.
`both`, `let`, `finish`, `insert`, `offset` and `xor` are all reserved. Some are inherited
from PostgreSQL, some added for a graph clause. Which positions each one still
works in varies, so [Keywords](../appendix/keywords.md) carries the position-by-position
table. A pattern variable is a position none of them survives:
```cypher
MATCH (both:movie {title: 'The Matrix'}) RETURN both.title;
```
```
ERROR: syntax error at or near "both"
LINE 1: MATCH (both:movie {title: 'The Matrix'}) RETURN both.title;
^
```
Quoting lifts the restriction, because a quoted identifier is never read as a keyword:
```cypher
MATCH ("both":movie {title: 'The Matrix'}) RETURN "both".title AS title;
```
```
title
--------------
"The Matrix"
```
A reserved word is still allowed as a `RETURN` alias and as a property key, since neither
position can be confused with a keyword:
```cypher
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS both;
```
```
both
--------------
"The Matrix"
```
## Literals
### Strings
A string literal is enclosed in **single** quotes. Double quotes make an identifier, not a
string, so `"plain"` is a variable reference and fails if no such variable is in scope.
A single quote inside a literal is written twice. Backslash escapes follow JSON's rules,
because a Cypher string becomes a `jsonb` string:
| Escape | Meaning |
| --- | --- |
| `''` | A single quote. |
| `\"` | A double quote. |
| `\\` | A backslash. |
| `\/` | A forward slash. |
| `\b` `\f` `\n` `\r` `\t` | Backspace, form feed, line feed, carriage return, tab. |
| `\u` followed by four hex digits | The character with that code point. |
Any other backslash sequence is rejected: `'a\qb'` raises `Escape sequence "\q" is invalid`.
```cypher
RETURN 'plain', 'it''s', 'a\tb', 'a\\tb', 'say \" hi';
```
```
?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------+-------------
"plain" | "it's" | "a\tb" | "a\\tb" | "say \" hi"
```
The output is the `jsonb` rendering of each value, which re-escapes what it prints: the third
column holds three characters, `a`, a tab and `b`.
### Numbers
A number literal is written as in SQL, and PostgreSQL's non-decimal and separated forms are
accepted: hexadecimal `0x`, octal `0o`, binary `0b`, and underscores as digit separators.
```cypher
RETURN 1, -1, 1.5, 1e3, 1E-3, 0x1F, 0o17, 0b101, 1_000;
```
```
?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------+----------+----------+----------+----------+----------
1 | -1 | 1.5 | 1000 | 0.001 | 31 | 15 | 5 | 1000
```
A number written without a decimal point is a JSON integer, and dividing two of them
truncates: `7 / 2` is `3`. Write `7.0 / 2` for `3.5`.
### Booleans, null, lists and maps
`true`, `false` and `null` are keywords, in any case. A list is written in square brackets
and a map in braces. Both nest, and both may mix value types, because both become `jsonb`.
```cypher
RETURN true, false, null, [1, 'two', true, null], {a: 1, b: [2], c: {d: 3}};
```
```
?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+------------------------+-----------------------------------
t | f | | [1, "two", true, null] | {"a": 1, "b": [2], "c": {"d": 3}}
```
A map key is an identifier and folds like one. `[]` and `{}` are the empty list and the empty
map. See [Data types](types.md) for how a literal relates to the stored `jsonb` value.
## Operators
### Precedence
Operators bind in this order, loosest first. Two operators on the same row have equal
precedence and associate as the row says.
| Precedence | Operators | Associativity |
| --- | --- | --- |
| 1 (loosest) | `UNION` `EXCEPT` | left |
| 2 | `INTERSECT` | left |
| 3 | `OR` `XOR` | left |
| 4 | `AND` | left |
| 5 | `NOT` | right |
| 6 | `IS NULL` `IS NOT NULL` `IS [NOT] UNKNOWN` | none |
| 7 | `<` `<=` `>` `>=` `=` `<>` `!=` | none |
| 8 | `IN` `BETWEEN` | none |
| 9 | any other operator, including `\|\|` and the `jsonb` operators | left |
| 10 | `+` `-` (binary) | left |
| 11 | `*` `/` `%` | left |
| 12 | `^` | left |
| 13 | `-` (unary) | right |
| 14 | `STARTS WITH` `ENDS WITH` `CONTAINS` `=~` | none |
| 15 | `[` `]` (subscript), `(` `)` (call) | left |
| 16 | `::` | left |
| 17 (tightest) | `.` | left |
The table lists the operators a Cypher expression can contain. SQL adds a few more at levels
of its own: `AT TIME ZONE`, `COLLATE`, `LIKE`, `ILIKE` and `SIMILAR TO`. Cypher's expression
grammar accepts none of them.
Three things in the table differ from a common expectation.
`^` is **left**-associative and binds looser than unary minus, which is PostgreSQL's rule
rather than arithmetic's. So `2 ^ 3 ^ 2` is `(2 ^ 3) ^ 2` and `-2 ^ 2` is `(-2) ^ 2`:
```cypher
RETURN 2 ^ 3 ^ 2 AS pow, -2 ^ 2 AS neg;
```
```
pow | neg
-----+-----
64 | 4
```
The string-matching operators bind **tighter** than everything except a subscript, a call, a
cast and a property read. They bind tighter than concatenation and tighter than arithmetic.
So the right-hand operand of `||` below is the whole `STARTS WITH` test, not just `'c'`:
```cypher
RETURN 'ab' || 'c' STARTS WITH 'a';
```
```
?column?
-----------
"abfalse"
```
Parenthesise when you mean the other reading. `('ab' || 'c') STARTS WITH 'a'` is `true`.
Comparisons do not chain. `1 < 2 < 3` is a syntax error rather than two comparisons.
### Logical operators
`AND`, `OR`, `NOT` and `XOR` take booleans and return a boolean, over three values. See
[Null and three-valued logic](#null-and-three-valued-logic) for the truth tables. `XOR` is
AgensGraph's addition to SQL's set, and it sits at `OR`'s precedence.
### Comparison and predicates
`=`, `<>` (or `!=`), `<`, `<=`, `>` and `>=` compare two values. Comparing values of
different `jsonb` types does not fail. It orders them by `jsonb`'s own type order.
`IN` tests membership of a list. `IS NULL` and `IS NOT NULL` test any value for null without
comparing it. `IS UNKNOWN` and `IS NOT UNKNOWN` are the standard's spelling of that test on a
boolean, and are accepted here:
```cypher
RETURN 2 IN [1, 2, 3] AS present, null IS NULL AS nul, null IS UNKNOWN AS unk;
```
```
present | nul | unk
---------+-----+-----
t | t | t
```
### String matching
Four operators match strings. `LIKE`, `ILIKE` and `SIMILAR TO` are **SQL only**. They are
not part of Cypher's expression grammar, and they are a syntax error there. PostgreSQL's
regular expression operators (`~`, `~*`, `!~`, `!~*`) do work in Cypher.
| Operator | Test |
| --- | --- |
| *string* `STARTS WITH` *prefix* | The string begins with the prefix. |
| *string* `ENDS WITH` *suffix* | The string ends with the suffix. |
| *string* `CONTAINS` *substring* | The substring occurs anywhere in the string. |
| *string* `=~` *pattern* | The whole string matches the POSIX regular expression. |
```cypher
RETURN 'Matrix' STARTS WITH 'Mat' AS sw, 'Matrix' ENDS WITH 'rix' AS ew,
'Matrix' CONTAINS 'atr' AS c, 'Matrix' =~ '^M.*x$' AS re;
```
```
sw | ew | c | re
----+----+---+----
t | t | t | t
```
None of the four can be answered from an index. A predicate that has to be sargable needs
`=` or a range. [Data types](types.md) covers the index-visible forms of a property.
### Arithmetic
`+`, `-`, `*`, `/`, `%` and `^` work on numbers, and unary `+` and `-` negate. On a property
they run against `jsonb` rather than a SQL numeric type, which gives `+` two extra jobs: it
concatenates two strings, and it appends to a list.
```cypher
MATCH (p:person {name: 'Keanu Reeves'})
RETURN p.born + 1 AS plus, p.born % 7 AS mod, -p.born AS neg,
p.name + '!' AS concat, [1, 2] + 3 AS appended;
```
```
plus | mod | neg | concat | appended
------+-----+-------+-----------------+-----------
1965 | 4 | -1964 | "Keanu Reeves!" | [1, 2, 3]
```
`+` on two maps is refused, with `invalid expression: {"a": 1} + {"b": 2}`. Merge maps with
the `jsonb` concatenation operator instead:
```cypher
RETURN {a: 1} || {b: 2} AS merged;
```
```
merged
------------------
{"a": 1, "b": 2}
```
To merge into an element rather than into a value, use `SET n += ` *map*.
### jsonb operators
A property is `jsonb`, so PostgreSQL's `jsonb` operators apply to one directly. These are the
ones worth knowing:
| Operator | Result |
| --- | --- |
| *json* `->` *key* / *index* | The member as `jsonb`. |
| *json* `->>` *key* / *index* | The member as `text`. |
| *json* `#>` *path* | The value at a text-array path, as `jsonb`. |
| *json* `#>>` *path* | The value at a text-array path, as `text`. |
| *json* `?` *key* | Whether the key is present. |
| *json* `@>` *json* | Whether the left value contains the right. |
| *json* `<@` *json* | Whether the left value is contained in the right. |
| *json* `\|\|` *json* | Concatenation: two objects merge, other combinations make an array. |
```cypher
MATCH (m:movie {title: 'The Matrix'})
RETURN properties(m) -> 'title' AS as_json, properties(m) ->> 'title' AS as_text,
properties(m) ? 'genre' AS has_genre, properties(m) @> '{"genre": "Action"}' AS is_action;
```
```
as_json | as_text | has_genre | is_action
--------------+--------------+-----------+-----------
"The Matrix" | "The Matrix" | t | t
```
`->>` returned `text`, but a Cypher `RETURN` boxes a scalar column back to `jsonb`, so the
quotes are still there. To get a plain string out, wrap the query in SQL. See
[Data types](types.md#what-a-return-hands-back).
Two rules govern the rest of PostgreSQL's `jsonb` operator set inside Cypher, and both follow
from Cypher owning the arithmetic spellings.
`-` between two `jsonb` values is subtraction, so `jsonb`'s key-deletion operator is not
reachable from Cypher: `properties(m) - 'plot'` raises `invalid expression`. Delete a key with
a SQL wrapper, or with `REMOVE`.
A bare string literal beside a native `jsonb` operator is parsed as a whole JSON document
rather than as a JSON string. So `m.title || '!'` fails on `Token "!" is invalid`. Give the
operator a typed operand, as in `m.title || to_jsonb('!'::text)`. Or use `+`, which takes the
Cypher reading.
## Comments
A comment runs from `--` to the end of the line, or from `/*` to a matching `*/`. Block
comments nest, so a commented-out region containing a comment stays balanced. `//` is not a
comment and is a syntax error.
```cypher
MATCH (m:movie {title: 'The Matrix'}) -- one film
/* a block comment
over two lines */
RETURN m.title AS title;
```
```
title
--------------
"The Matrix"
```
## Null and three-valued logic
`null` means *unknown*. It is not a value you can compare with, and it is not a property
value. Assigning null to a property removes it, unless
[`allow_null_properties`](guc.md#allow_null_properties) is on.
### Truth tables
`AND`, `OR` and `XOR` extend to three values as follows. The table is the output of running
every combination:
```psql
SELECT a #>> '{}' AS a, b #>> '{}' AS b,
andv::text AS "a AND b", orv::text AS "a OR b", xorv::text AS "a XOR b"
FROM (UNWIND [true, false, null] AS a
UNWIND [true, false, null] AS b
RETURN a, b, a AND b AS andv, a OR b AS orv, a XOR b AS xorv) q;
```
```
a | b | a AND b | a OR b | a XOR b
-------+-------+---------+--------+---------
true | true | true | true | false
true | false | false | true | true
true | null | null | true | null
false | true | false | true | true
false | false | false | false | false
false | null | false | null | null
null | true | null | true | null
null | false | false | null | null
null | null | null | null | null
```
`NOT` maps true to false, false to true, and null to null:
```psql
SELECT a #>> '{}' AS a, notv::text AS "NOT a"
FROM (UNWIND [true, false, null] AS a RETURN a, NOT a AS notv) q;
```
```
a | NOT a
-------+-------
true | false
false | true
null | null
```
So `AND` is false as soon as one side is false, and `OR` is true as soon as one side is true,
whatever the other side is. Everything else involving null is null.
`IN` follows from the same rule: a value not found in a list that contains null might still
have been there, so the answer is unknown rather than false.
```psql
SELECT expr, res::text AS result FROM (
RETURN '2 IN [1, 2, 3]' AS expr, 2 IN [1,2,3] AS res
UNION ALL RETURN '2 IN [1, null, 3]', 2 IN [1,null,3]
UNION ALL RETURN '2 IN [1, 2, null]', 2 IN [1,2,null]
UNION ALL RETURN '2 IN [1]', 2 IN [1]
UNION ALL RETURN '2 IN []', 2 IN []
UNION ALL RETURN 'null IN [1, 2, 3]', null IN [1,2,3]
UNION ALL RETURN 'null IN [1, null, 3]', null IN [1,null,3]
UNION ALL RETURN 'null IN []', null IN []
) q;
```
```
expr | result
------------------------+--------
"2 IN [1, 2, 3]" | true
"2 IN [1, null, 3]" | null
"2 IN [1, 2, null]" | true
"2 IN [1]" | false
"2 IN []" | false
"null IN [1, 2, 3]" | null
"null IN [1, null, 3]" | null
"null IN []" | false
```
An empty list is the exception: nothing is in it, so the answer is false even for null.
### Expressions that yield null
These are the expressions that produce null rather than an error or a value:
| Expression | Why |
| --- | --- |
| `n.no_such_property` | The element has no such property. |
| `[][0]`, `head([])` | The list has no such element. |
| `1 + null` | Arithmetic with an unknown operand. |
| `1 < null`, `null = null` | Comparison with an unknown value. |
| `sin(null)`, `toUpper(null)` | A function called with null. |
```cypher
MATCH (m:movie {title: 'The Matrix'})
RETURN m.no_such_property AS missing_property, [][0] AS missing_element,
head([]) AS head_of_empty, 1 + null AS arithmetic, 1 < null AS comparison,
sin(null) AS function, null = null AS equality;
```
```
missing_property | missing_element | head_of_empty | arithmetic | comparison | function | equality
------------------+-----------------+---------------+------------+------------+----------+----------
null | null | null | null | null | null | null
```
`IS NULL` exists because `null = null` is null. It is the only way to test for null.
### A condition takes only a boolean
`WHERE`, `FILTER`, `CASE WHEN` and every other condition accept a boolean and nothing else. A
condition that evaluates to a `jsonb` string, number, list or map is an error:
```cypher
CREATE GRAPH demo;
SET graph_path = demo;
CREATE VLABEL t;
CREATE (:t {name: 'Jack'});
MATCH (n:t) WHERE n.name RETURN n.name;
```
```
ERROR: cannot cast "Jack" (jsonb type string) to boolean
```
That error comes from evaluating the condition, so the value it names is whichever row the
executor reached first. A condition that is not `jsonb` at all is rejected earlier, when the
statement is parsed:
```cypher
MATCH (n:t) WHERE 1 RETURN n.name;
```
```
ERROR: argument of WHERE must be type boolean, not type integer
LINE 1: MATCH (n:t) WHERE 1 RETURN n.name;
^
```
Write the test you mean. `WHERE n.name IS NOT NULL` asks whether the property is there:
```cypher
MATCH (n:t) WHERE n.name IS NOT NULL RETURN n.name AS name;
```
```
name
--------
"Jack"
```
`WHERE n.name <> ''` asks whether it is non-empty, and `WHERE n.active` works when the
property really holds a boolean, because a `jsonb` boolean casts to one.
Null is still accepted, and behaves as it does in SQL: a row whose condition is null is not
returned, and nothing is raised. So `WHERE n.no_such_property` returns no rows.
```{note}
This is a deliberate compatibility break with earlier releases, which accepted a non-empty
value as a condition. A query written that way fails rather than filtering on something it
did not mean.
[Cypher compatibility](../appendix/cypher-compatibility.md#a-condition-takes-only-a-boolean)
lists what to change.
```
## Compatibility
### openCypher
Three lexical differences will catch a reader who knows Cypher from elsewhere.
* **Identifiers fold to lower case, and backticks are not a quoting character.** Use double
quotes. `` `My Label` `` is a syntax error.
* **A string literal takes single quotes only.** Double quotes make an identifier.
* **`//` is not a comment.** Use `--` or `/* */`.
`XOR`, `STARTS WITH`, `ENDS WITH`, `CONTAINS` and `=~` are all present and mean what they mean
in openCypher.
### ISO/IEC 39075
The notation in this reference follows the same conventions as the standard's syntax
productions, but is not the standard's BNF.
`IS [NOT] UNKNOWN` is the standard's spelling of the null test on a boolean and is accepted
alongside `IS [NOT] NULL`. So is `XOR`. Both fall under the standard's
`` (ISO/IEC 39075:2024 §20.20). `UNKNOWN` used as a *value* is a
syntax error. See [GQL conformance](../appendix/gql-conformance.md) for the rest.
## See also
- [Data types](types.md) — `graphid`, `vertex`, `edge`, `graphpath` and `jsonb` properties
- [Server settings](guc.md) — including `graph_path`, which every session needs
- [Keywords](../appendix/keywords.md) — the full keyword list and each word's category
- [Cypher, GQL and SQL](../concepts/languages.md) — which language to reach for
- [PostgreSQL: SQL syntax](https://www.postgresql.org/docs/18/sql-syntax.html) — the SQL half
of the same lexer