WHITESPACE = _{ " " | "\t" | "\r" | "\n" | block_comment | line_comment }
block_comment = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
line_comment = _{ "//" ~ (!"\n" ~ ANY)* }
query = { SOI ~ statement ~ semicolon? ~ EOI }
semicolon = _{ ";" }
statement = { schema_command | regular_query | standalone_call }
schema_command = {
create_index_command
| drop_index_command
| show_indexes_command
| create_constraint_command
| drop_constraint_command
| show_constraints_command
}
create_index_command = {
CREATE ~ index_kind? ~ INDEX ~ index_name_spec? ~ if_not_exists?
~ FOR ~ index_pattern ~ ON ~ index_property_spec ~ index_options?
}
index_kind = { TEXT | POINT | LOOKUP | RANGE | VECTOR | FULLTEXT }
// Negative lookahead so `CREATE INDEX FOR ...` and `CREATE INDEX IF NOT EXISTS ...`
// don't bind `FOR` / `IF` as the (optional) index name.
index_name_spec = { !(FOR | IF) ~ (parameter | symbolic_name) }
if_not_exists = { IF ~ NOT ~ EXISTS }
index_pattern = {
fulltext_node_pattern
| fulltext_rel_pattern
| indexed_node_pattern
| indexed_rel_pattern
| lookup_node_pattern
| lookup_rel_pattern
}
indexed_node_pattern = { lparen ~ index_var ~ colon ~ label_name ~ rparen }
indexed_rel_pattern = {
lparen ~ rparen ~ dash ~ lbrack ~ index_var ~ colon ~ rel_type_name ~ rbrack
~ dash ~ right_arrow? ~ lparen ~ rparen
}
// `(n:A|B|C)` and `()-[r:A|B|C]-()` — only used by FULLTEXT today.
// The parser layer enforces the single-label restriction for other kinds.
fulltext_node_pattern = {
lparen ~ index_var ~ colon ~ label_name ~ (pipe ~ label_name)+ ~ rparen
}
fulltext_rel_pattern = {
lparen ~ rparen ~ dash ~ lbrack ~ index_var ~ colon ~ rel_type_name
~ (pipe ~ rel_type_name)+ ~ rbrack ~ dash ~ right_arrow? ~ lparen ~ rparen
}
lookup_node_pattern = { lparen ~ index_var ~ rparen }
lookup_rel_pattern = {
lparen ~ rparen ~ dash ~ lbrack ~ index_var ~ rbrack ~ dash ~ right_arrow? ~ lparen ~ rparen
}
index_var = { symbolic_name }
index_property_spec = {
index_property_list
| index_property_each
| index_token_lookup
}
index_property_list = { lparen ~ index_property ~ (comma ~ index_property)* ~ rparen }
// FULLTEXT spelling: `ON EACH [n.p1, n.p2]`.
index_property_each = {
EACH ~ lbrack ~ index_property ~ (comma ~ index_property)* ~ rbrack
}
index_property = { index_var ~ dot ~ property_key_name }
index_token_lookup = { EACH ~ index_token_func }
index_token_func = { (LABELS_FN | TYPE_FN) ~ lparen ~ index_var ~ rparen }
index_options = { OPTIONS ~ map_literal }
show_indexes_command = {
SHOW ~ show_index_filter? ~ (INDEXES | INDEX) ~ show_pipeline?
}
// Type filter for SHOW INDEXES. `ALL` is the explicit unfiltered form;
// `FULLTEXT` parses but returns empty (not yet implemented).
show_index_filter = { ALL | FULLTEXT | LOOKUP | POINT | RANGE | TEXT | VECTOR }
// YIELD [* | items [ORDER BY ...] [SKIP ...] [LIMIT ...]]
// [WHERE expr]
// [RETURN items [ORDER BY ...] [SKIP ...] [LIMIT ...]]
//
// Neo4j requires that RETURN only appear when YIELD is present, so the
// pipeline is anchored on YIELD.
show_pipeline = { show_yield ~ where_clause? ~ show_return? }
show_yield = {
YIELD ~ (STAR | yield_items) ~ order_clause? ~ skip_clause? ~ limit_clause?
}
show_return = {
RETURN ~ projection_items ~ order_clause? ~ skip_clause? ~ limit_clause?
}
drop_index_command = { DROP_KW ~ INDEX ~ index_name_spec ~ if_exists? }
if_exists = { IF ~ EXISTS }
// ---------- CREATE / DROP / SHOW CONSTRAINT ----------
// Constraint name is required (matches CREATE CONSTRAINT name FOR ...).
// The pattern is reused from the index grammar (same shape).
create_constraint_command = {
CREATE ~ CONSTRAINT ~ constraint_name_spec ~ if_not_exists?
~ FOR ~ constraint_pattern
~ REQUIRE ~ constraint_property_spec ~ constraint_requirement
}
constraint_name_spec = { !(FOR | IF) ~ (parameter | symbolic_name) }
constraint_pattern = { constraint_node_pattern | constraint_rel_pattern }
constraint_node_pattern = { lparen ~ index_var ~ colon ~ label_name ~ rparen }
constraint_rel_pattern = {
lparen ~ rparen ~ dash ~ lbrack ~ index_var ~ colon ~ rel_type_name ~ rbrack
~ dash ~ right_arrow? ~ lparen ~ rparen
}
// REQUIRE n.prop IS UNIQUE -> single property without parens
// REQUIRE (n.a, n.b) IS UNIQUE -> composite, must be parenthesized
constraint_property_spec = { constraint_property_group | constraint_property }
constraint_property_group = { lparen ~ constraint_property ~ (comma ~ constraint_property)* ~ rparen }
constraint_property = { index_var ~ dot ~ property_key_name }
constraint_requirement = {
IS ~ UNIQUE
| IS ~ NODE ~ KEY
| IS ~ RELATIONSHIP ~ KEY
| IS ~ NOT ~ NULL
| IS ~ type_predicate ~ constraint_type_expr
}
type_predicate = { "::" }
// Closed dynamic union: `STRING | LIST<STRING NOT NULL>` — recursion via `pipe`.
constraint_type_expr = { constraint_type_term ~ (pipe ~ constraint_type_term)* }
constraint_type_term = {
constraint_vector_type
| constraint_list_type
| constraint_scalar_type
}
constraint_vector_type = {
VECTOR ~ "<" ~ vector_coord_type ~ ">" ~ lparen ~ integer_literal ~ rparen
}
vector_coord_type = { INT8 | INT16 | INT32 | INT64 | FLOAT32 | FLOAT64 }
constraint_list_type = {
LIST ~ "<" ~ constraint_type_term ~ (NOT ~ NULL)? ~ ">"
}
constraint_scalar_type = {
LOCAL_DATETIME
| ZONED_DATETIME
| LOCAL_TIME
| ZONED_TIME
| BOOLEAN
| STRING
| INTEGER
| FLOAT
| DATE
| DURATION
| POINT
| MAP_T
| ANY_T
}
drop_constraint_command = { DROP_KW ~ CONSTRAINT ~ constraint_name_spec ~ if_exists? }
show_constraints_command = { SHOW ~ (CONSTRAINTS | CONSTRAINT) ~ show_pipeline? }
regular_query = { single_query ~ (union_clause ~ single_query)* }
union_clause = { UNION ~ ALL? }
single_query = { multi_part_query | single_part_query }
multi_part_query = { query_part+ ~ single_part_query }
query_part = { reading_clause* ~ updating_clause* ~ with_clause }
single_part_query = {
(reading_clause* ~ return_clause)
| (reading_clause* ~ updating_clause+ ~ return_clause?)
}
reading_clause = { match_clause | unwind_clause | in_query_call }
updating_clause = { create_clause | merge_clause | delete_clause | set_clause | remove_clause }
match_clause = { OPTIONAL? ~ MATCH ~ pattern ~ where_clause? }
unwind_clause = { UNWIND ~ expression ~ AS ~ variable }
create_clause = { CREATE ~ pattern }
merge_clause = { MERGE ~ pattern_part ~ merge_action* }
merge_action = { ON ~ (MATCH | CREATE) ~ set_clause }
delete_clause = { DETACH? ~ DELETE ~ expression ~ (comma ~ expression)* }
set_clause = { SET ~ set_item ~ (comma ~ set_item)* }
set_item = {
variable ~ plus_eq ~ expression
| property_set_target ~ eq ~ expression
| variable ~ eq ~ expression
| variable ~ node_labels
}
property_set_target = { variable ~ property_lookup+ }
remove_clause = { REMOVE ~ remove_item ~ (comma ~ remove_item)* }
remove_item = {
variable ~ node_labels
| expression
}
in_query_call = { CALL ~ procedure_invocation ~ yield_clause? ~ where_clause? }
standalone_call = { CALL ~ (procedure_invocation | procedure_name) ~ yield_clause? }
yield_clause = { YIELD ~ (STAR | yield_items) }
yield_items = { yield_item ~ (comma ~ yield_item)* }
yield_item = { symbolic_name ~ (AS ~ variable)? }
with_clause = { WITH ~ projection_body ~ where_clause? }
where_clause = { WHERE ~ expression }
return_clause = { RETURN ~ projection_body }
projection_body = {
DISTINCT? ~ projection_items ~ order_clause? ~ skip_clause? ~ limit_clause?
}
projection_items = { projection_item ~ (comma ~ projection_item)* }
projection_item = { star | expression ~ (AS ~ variable)? }
star = { STAR }
order_clause = { ORDER ~ BY ~ sort_item ~ (comma ~ sort_item)* }
sort_item = { expression ~ (ASCENDING | ASC | DESCENDING | DESC)? }
skip_clause = { SKIP ~ expression }
limit_clause = { LIMIT ~ expression }
pattern = { pattern_part ~ (comma ~ pattern_part)* }
pattern_part = { (variable ~ eq)? ~ anonymous_pattern_part }
anonymous_pattern_part = {
shortest_path_pattern | pattern_element
}
shortest_path_pattern = {
(SHORTEST_PATH | ALL_SHORTEST_PATHS) ~ lparen ~ pattern_element ~ rparen
}
pattern_element = {
node_pattern ~ pattern_element_chain*
| lparen ~ pattern_element ~ rparen
}
pattern_element_chain = { relationship_pattern ~ node_pattern }
node_pattern = {
lparen ~ variable? ~ node_labels? ~ properties? ~ rparen
}
relationship_pattern = {
left_arrow? ~ dash ~ relationship_detail? ~ dash ~ right_arrow?
}
relationship_detail = {
lbrack ~ variable? ~ relationship_types? ~ range_literal? ~ properties? ~ rbrack
}
relationship_types = { colon ~ rel_type_name ~ (pipe ~ colon? ~ rel_type_name)* }
node_labels = { node_label_set+ }
node_label_set = { colon ~ label_name ~ (pipe ~ label_name)* }
node_label = { colon ~ label_name }
range_literal = { STAR ~ integer_literal? ~ (dotdot ~ integer_literal?)? }
properties = { map_literal | parameter }
expression = { or_expression }
case_expression = { simple_case_expression | generic_case_expression }
simple_case_expression = {
CASE ~ expression ~ (WHEN ~ expression ~ THEN ~ expression)+ ~ (ELSE ~ expression)? ~ END
}
generic_case_expression = {
CASE ~ (WHEN ~ expression ~ THEN ~ expression)+ ~ (ELSE ~ expression)? ~ END
}
or_expression = { xor_expression ~ (OR ~ xor_expression)* }
xor_expression = { and_expression ~ (XOR ~ and_expression)* }
and_expression = { not_expression ~ (AND ~ not_expression)* }
not_expression = { NOT* ~ comparison_expression }
comparison_expression = { add_expression ~ comparison_tail* }
comparison_tail = {
comparison_op ~ add_expression
| regex_match ~ add_expression
| IN ~ add_expression
| STARTS ~ WITH ~ add_expression
| ENDS ~ WITH ~ add_expression
| CONTAINS ~ add_expression
| IS ~ NOT ~ NULL
| IS ~ NULL
}
regex_match = { "=~" }
comparison_op = { eq | ne | le | ge | lt | gt }
add_expression = { mul_expression ~ ((add | sub) ~ mul_expression)* }
mul_expression = { pow_expression ~ ((mul | div | modulo) ~ pow_expression)* }
pow_expression = { unary_expression ~ (pow ~ unary_expression)* }
unary_expression = { (add | sub)* ~ postfix_expression }
postfix_expression = { atom ~ postfix_op* }
postfix_op = { property_lookup | map_projection_postfix | index_or_slice }
property_lookup = { dot ~ property_key_name }
map_projection_postfix = {
lbrace
~ (map_projection_selector ~ (comma ~ map_projection_selector)*)?
~ rbrace
}
map_projection_selector = {
dot ~ STAR // .*
| dot ~ property_key_name // .name
| property_key_name ~ colon_sep ~ expression // key: expr
}
atom = {
case_expression
| exists_subquery
| list_predicate
| reduce_expression
| function_invocation
| literal
| parameter
| variable
| parenthesized_expression
}
exists_subquery = {
EXISTS ~ lbrace ~ (MATCH ~ pattern ~ where_clause? | pattern) ~ rbrace
}
reduce_expression = {
REDUCE ~ lparen ~ variable ~ eq ~ expression ~ comma ~ variable ~ IN ~ expression ~ pipe ~ expression ~ rparen
}
REDUCE = @{ ^"reduce" ~ !ident_part }
list_predicate = {
(ANY_ | ALL | NONE | SINGLE)
~ lparen ~ variable ~ IN ~ expression ~ WHERE ~ expression ~ rparen
}
parenthesized_expression = { lparen ~ expression ~ rparen }
function_invocation = {
function_name ~ lparen ~ DISTINCT? ~ (STAR | expression ~ (comma ~ expression)*)? ~ rparen
}
procedure_invocation = {
procedure_name ~ lparen ~ (expression ~ (comma ~ expression)*)? ~ rparen
}
procedure_name = { namespace? ~ symbolic_name }
function_name = { namespace? ~ symbolic_name }
namespace = { (symbolic_name ~ dot)+ }
literal = {
number_literal
| string_literal
| boolean_literal
| null_literal
| map_literal
| list_literal
}
boolean_literal = { TRUE | FALSE }
null_literal = { NULL }
list_literal = { lbrack ~ (pattern_comprehension | list_comprehension | (expression ~ (comma ~ expression)*)?) ~ rbrack }
pattern_comprehension = {
pattern_element ~ (WHERE ~ expression)? ~ pipe ~ expression
}
list_comprehension = {
variable ~ IN ~ expression ~ (WHERE ~ expression)? ~ (pipe ~ expression)?
}
map_literal = {
lbrace
~ (property_key_name ~ colon_sep ~ expression ~ (comma ~ property_key_name ~ colon_sep ~ expression)*)?
~ rbrace
}
parameter = { "$" ~ (symbolic_name | decimal_integer) }
number_literal = { double_literal | integer_literal }
integer_literal = { hex_integer | octal_integer | decimal_integer }
double_literal = { exponent_decimal_real | regular_decimal_real }
hex_integer = @{ "0x" ~ ASCII_HEX_DIGIT+ }
octal_integer = @{ "0" ~ ASCII_OCT_DIGIT+ }
decimal_integer = @{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* }
exponent_decimal_real = @{
(ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ | ASCII_DIGIT+ | "." ~ ASCII_DIGIT+)
~ ^"E" ~ "-"? ~ ASCII_DIGIT+
}
regular_decimal_real = @{ ASCII_DIGIT* ~ "." ~ ASCII_DIGIT+ }
string_literal = { string_double | string_single }
string_double = @{ "\"" ~ ( "\\\"" | "\\\\" | "\\n" | "\\r" | "\\t" | !("\"") ~ ANY )* ~ "\"" }
string_single = @{ "'" ~ ( "\\'" | "\\\\" | "\\n" | "\\r" | "\\t" | !("'" ) ~ ANY )* ~ "'" }
variable = { symbolic_name }
property_key_name = { schema_name }
label_name = { schema_name }
rel_type_name = { schema_name }
schema_name = { symbolic_name | reserved_word }
symbolic_name = { escaped_symbolic_name | unescaped_symbolic_name | COUNT | ANY_ | NONE | SINGLE }
unescaped_symbolic_name = @{ ident_start ~ ident_part* }
escaped_symbolic_name = @{ "`" ~ (!"`" ~ ANY)* ~ "`" }
ident_start = _{ ASCII_ALPHA | "_" }
ident_part = _{ ASCII_ALPHANUMERIC | "_" | "$" }
reserved_word = {
ALL | ASC | ASCENDING | BY | CALL | CREATE | DELETE | DESC | DESCENDING | DETACH |
EXISTS | LIMIT | MATCH | MERGE | ON | OPTIONAL | ORDER | REMOVE | RETURN |
SET | SKIP | WHERE | WITH | UNION | UNWIND | AND | AS | CONTAINS | DISTINCT |
ENDS | IN | IS | NOT | OR | STARTS | XOR | FALSE | TRUE | NULL | CASE |
WHEN | THEN | ELSE | END | YIELD
}
lparen = _{ "(" }
rparen = _{ ")" }
lbrack = _{ "[" }
rbrack = _{ "]" }
lbrace = _{ "{" }
rbrace = _{ "}" }
comma = _{ "," }
dot = _{ "." }
pipe = _{ "|" }
colon = _{ ":" }
colon_sep = _{ ":" }
dotdot = _{ ".." }
eq = { "=" }
plus_eq = { "+=" }
ne = { "<>" }
lt = { "<" }
gt = { ">" }
le = { "<=" }
ge = { ">=" }
add = { "+" }
sub = { "-" }
mul = { "*" }
div = { "/" }
modulo = { "%" }
pow = { "^" }
dash = _{ "-" | "‐" | "–" | "—" | "−" }
left_arrow = { "<" }
right_arrow = { ">" }
ALL = @{ ^"ALL" ~ !ident_part }
ASC = @{ ^"ASC" ~ !ident_part }
ASCENDING = @{ ^"ASCENDING" ~ !ident_part }
BY = @{ ^"BY" ~ !ident_part }
CALL = @{ ^"CALL" ~ !ident_part }
CREATE = @{ ^"CREATE" ~ !ident_part }
DELETE = @{ ^"DELETE" ~ !ident_part }
DESC = @{ ^"DESC" ~ !ident_part }
DESCENDING = @{ ^"DESCENDING" ~ !ident_part }
DETACH = @{ ^"DETACH" ~ !ident_part }
DISTINCT = @{ ^"DISTINCT" ~ !ident_part }
EXISTS = @{ ^"EXISTS" ~ !ident_part }
LIMIT = @{ ^"LIMIT" ~ !ident_part }
MATCH = @{ ^"MATCH" ~ !ident_part }
MERGE = @{ ^"MERGE" ~ !ident_part }
ON = @{ ^"ON" ~ !ident_part }
OPTIONAL = @{ ^"OPTIONAL" ~ !ident_part }
ORDER = @{ ^"ORDER" ~ !ident_part }
REMOVE = @{ ^"REMOVE" ~ !ident_part }
RETURN = @{ ^"RETURN" ~ !ident_part }
SET = @{ ^"SET" ~ !ident_part }
SKIP = @{ ^"SKIP" ~ !ident_part }
WHERE = @{ ^"WHERE" ~ !ident_part }
WITH = @{ ^"WITH" ~ !ident_part }
YIELD = @{ ^"YIELD" ~ !ident_part }
UNION = @{ ^"UNION" ~ !ident_part }
UNWIND = @{ ^"UNWIND" ~ !ident_part }
AND = @{ ^"AND" ~ !ident_part }
AS = @{ ^"AS" ~ !ident_part }
CONTAINS = @{ ^"CONTAINS" ~ !ident_part }
ENDS = @{ ^"ENDS" ~ !ident_part }
IN = @{ ^"IN" ~ !ident_part }
IS = @{ ^"IS" ~ !ident_part }
NOT = @{ ^"NOT" ~ !ident_part }
OR = @{ ^"OR" ~ !ident_part }
STARTS = @{ ^"STARTS" ~ !ident_part }
XOR = @{ ^"XOR" ~ !ident_part }
FALSE = @{ ^"FALSE" ~ !ident_part }
TRUE = @{ ^"TRUE" ~ !ident_part }
NULL = @{ ^"NULL" ~ !ident_part }
CASE = @{ ^"CASE" ~ !ident_part }
WHEN = @{ ^"WHEN" ~ !ident_part }
THEN = @{ ^"THEN" ~ !ident_part }
ELSE = @{ ^"ELSE" ~ !ident_part }
END = @{ ^"END" ~ !ident_part }
COUNT = @{ ^"COUNT" ~ !ident_part }
ANY_ = @{ ^"ANY" ~ !ident_part }
NONE = @{ ^"NONE" ~ !ident_part }
SINGLE = @{ ^"SINGLE" ~ !ident_part }
index_or_slice = {
slice_op
| index_op
}
slice_op = { lbrack ~ expression? ~ slice_dots ~ expression? ~ rbrack }
slice_dots = { ".." }
index_op = { lbrack ~ expression ~ rbrack }
SHORTEST_PATH = @{ ^"shortestPath" ~ !ident_part }
ALL_SHORTEST_PATHS = @{ ^"allShortestPaths" ~ !ident_part }
INDEX = @{ ^"INDEX" ~ !ident_part }
INDEXES = @{ ^"INDEXES" ~ !ident_part }
RANGE = @{ ^"RANGE" ~ !ident_part }
TEXT = @{ ^"TEXT" ~ !ident_part }
POINT = @{ ^"POINT" ~ !ident_part }
LOOKUP = @{ ^"LOOKUP" ~ !ident_part }
FULLTEXT = @{ ^"FULLTEXT" ~ !ident_part }
FOR = @{ ^"FOR" ~ !ident_part }
IF = @{ ^"IF" ~ !ident_part }
EACH = @{ ^"EACH" ~ !ident_part }
OPTIONS = @{ ^"OPTIONS" ~ !ident_part }
SHOW = @{ ^"SHOW" ~ !ident_part }
DROP_KW = @{ ^"DROP" ~ !ident_part }
LABELS_FN = @{ ^"labels" ~ !ident_part }
TYPE_FN = @{ ^"type" ~ !ident_part }
CONSTRAINT = @{ ^"CONSTRAINT" ~ !ident_part }
CONSTRAINTS = @{ ^"CONSTRAINTS" ~ !ident_part }
REQUIRE = @{ ^"REQUIRE" ~ !ident_part }
UNIQUE = @{ ^"UNIQUE" ~ !ident_part }
NODE = @{ ^"NODE" ~ !ident_part }
KEY = @{ ^"KEY" ~ !ident_part }
RELATIONSHIP = @{ ^"RELATIONSHIP" ~ !ident_part }
// Property-type keywords. `LOCAL_DATETIME` / `ZONED_DATETIME` etc. are
// space-separated in source; ordered alternation makes the longer
// multi-word form win over the bare `LOCAL` / `ZONED`. Single-word forms
// (`BOOLEAN`, `STRING`, ...) ride alongside.
LOCAL_DATETIME = @{ ^"LOCAL" ~ (" " | "\t" | "\r" | "\n")+ ~ ^"DATETIME" ~ !ident_part }
ZONED_DATETIME = @{ ^"ZONED" ~ (" " | "\t" | "\r" | "\n")+ ~ ^"DATETIME" ~ !ident_part }
LOCAL_TIME = @{ ^"LOCAL" ~ (" " | "\t" | "\r" | "\n")+ ~ ^"TIME" ~ !ident_part }
ZONED_TIME = @{ ^"ZONED" ~ (" " | "\t" | "\r" | "\n")+ ~ ^"TIME" ~ !ident_part }
BOOLEAN = @{ ^"BOOLEAN" ~ !ident_part }
STRING = @{ ^"STRING" ~ !ident_part }
INTEGER = @{ ^"INTEGER" ~ !ident_part }
FLOAT = @{ ^"FLOAT" ~ !ident_part }
DATE = @{ ^"DATE" ~ !ident_part }
DURATION = @{ ^"DURATION" ~ !ident_part }
LIST = @{ ^"LIST" ~ !ident_part }
VECTOR = @{ ^"VECTOR" ~ !ident_part }
MAP_T = @{ ^"MAP" ~ !ident_part }
ANY_T = @{ ^"ANY" ~ !ident_part }
INT8 = @{ ^"INT8" ~ !ident_part }
INT16 = @{ ^"INT16" ~ !ident_part }
INT32 = @{ ^"INT32" ~ !ident_part }
INT64 = @{ ^"INT64" ~ !ident_part }
FLOAT32 = @{ ^"FLOAT32" ~ !ident_part }
FLOAT64 = @{ ^"FLOAT64" ~ !ident_part }
STAR = @{ "*" }