WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
query = { SOI ~ statement ~ ";"? ~ EOI }
// A `;`-separated batch of statements — `parse_many` uses this, `parse`
// (single-statement) still uses `query` above unchanged. `;` only ever
// separates `statement`s here; a `;` inside a `string_literal` is
// consumed as part of that atomic rule and never reaches this level, so
// no special-casing is needed to avoid splitting inside a string.
queries = { SOI ~ statement ~ (";" ~ statement)* ~ ";"? ~ EOI }
statement = { create_stmt | match_stmt }
create_stmt = { ^"CREATE" ~ pattern ~ ("," ~ pattern)* }
match_stmt = { match_part+ ~ tail_clause ~ order_by_clause? ~ limit_clause? }
match_part = { match_keyword ~ pattern ~ ("," ~ pattern)* ~ where_clause? ~ with_clause? }
match_keyword = { (^"OPTIONAL" ~ ^"MATCH") | ^"MATCH" }
with_clause = { ^"WITH" ~ return_item ~ ("," ~ return_item)* ~ with_where_clause? ~ order_by_clause? ~ limit_clause? }
tail_clause = { return_clause | detach_delete_clause | delete_clause | set_clause }
return_clause = { ^"RETURN" ~ return_item ~ ("," ~ return_item)* }
return_item = { return_expr ~ (^"AS" ~ identifier)? }
// Order matters: case_expr/function_call/prop_access need their extra
// syntax tried before the bare `identifier` fallback, and `literal` must
// come before `identifier` too — otherwise the keywords true/false/null
// would parse as plain variable references instead of literals.
return_expr = { case_expr | function_call | prop_access | literal | identifier }
case_expr = { ^"CASE" ~ return_expr ~ case_when+ ~ (^"ELSE" ~ return_expr)? ~ ^"END" }
case_when = { ^"WHEN" ~ return_expr ~ ^"THEN" ~ return_expr }
function_call = { identifier ~ "(" ~ call_args ~ ")" }
// `"*"` (bare, only meaningful for `count(*)`) is tried first — no
// `return_expr` production can ever start with a bare `*`, so there's no
// backtracking ambiguity with the general-args branch. Which functions
// accept `"*"` or `DISTINCT` is a semantic check in parser.rs, not a
// grammar-level restriction — see cypher.pest's other permissive-grammar
// precedents (comma-pattern splicing, WITH-boundary counting).
call_args = { "*" | (distinct_kw? ~ (return_expr ~ ("," ~ return_expr)*)?) }
distinct_kw = { ^"DISTINCT" }
// WITH's HAVING-equivalent: filters on the already-projected/aggregated
// row, so its comparison LHS is a `return_expr` (a WITH alias or raw
// expression), not `prop_access` like pattern-level `comparison` below —
// structurally a separate mirror of expr/and_expr/or_expr, not a reuse of
// them, since widening pattern-`Expr`'s LHS type would ripple into the
// planner's pre-projection filter-pushdown logic for a WHERE that
// fundamentally belongs post-projection instead.
with_where_clause = { ^"WHERE" ~ with_expr }
with_expr = { with_or_expr }
with_or_expr = { with_and_expr ~ (^"OR" ~ with_and_expr)* }
with_and_expr = { with_unary_expr ~ (^"AND" ~ with_unary_expr)* }
with_unary_expr = { (^"NOT" ~ with_unary_expr) | with_comparison | ("(" ~ with_expr ~ ")") }
with_comparison = { return_expr ~ compare_op ~ literal }
detach_delete_clause = { ^"DETACH" ~ ^"DELETE" ~ identifier ~ ("," ~ identifier)* }
delete_clause = { ^"DELETE" ~ identifier ~ ("," ~ identifier)* }
set_clause = { ^"SET" ~ set_item ~ ("," ~ set_item)* }
set_item = { prop_access ~ "=" ~ literal }
where_clause = { ^"WHERE" ~ expr }
limit_clause = { ^"LIMIT" ~ int_literal }
order_by_clause = { ^"ORDER" ~ ^"BY" ~ sort_item ~ ("," ~ sort_item)* }
sort_item = { return_expr ~ sort_dir? }
sort_dir = { ^"ASC" | ^"DESC" }
pattern = { node_pattern ~ (rel_pattern ~ node_pattern)* }
node_pattern = { "(" ~ node_var? ~ node_label* ~ prop_map? ~ ")" }
node_var = { identifier }
node_label = { ":" ~ identifier }
// Order matters: rel_right's trailing "->" must be tried before rel_either's
// bare "-", or "-[:KNOWS]->" would parse as rel_either and leave a dangling
// ">" that fails the rest of the pattern instead of matching rel_right.
rel_pattern = { rel_right | rel_left | rel_either }
rel_right = { "-[" ~ rel_var? ~ rel_type? ~ rel_range? ~ prop_map? ~ "]->" }
rel_left = { "<-[" ~ rel_var? ~ rel_type? ~ rel_range? ~ prop_map? ~ "]-" }
rel_either = { "-[" ~ rel_var? ~ rel_type? ~ rel_range? ~ prop_map? ~ "]-" }
rel_var = { identifier }
rel_type = { ":" ~ identifier }
// Variable-length hop count: `*`, `*N`, `*N..`, `*N..M`, `*..M`. Captured
// as raw text and parsed directly in parser.rs rather than broken into
// sub-rules — simpler than distinguishing "*N" (exact) from "*N.." (N or
// more) structurally, since the ".." literal itself produces no Pair.
rel_range = @{ "*" ~ ASCII_DIGIT* ~ (".." ~ ASCII_DIGIT*)? }
prop_map = { "{" ~ (prop_kv ~ ("," ~ prop_kv)*)? ~ "}" }
prop_kv = { identifier ~ ":" ~ literal }
expr = { or_expr }
or_expr = { and_expr ~ (^"OR" ~ and_expr)* }
and_expr = { unary_expr ~ (^"AND" ~ unary_expr)* }
unary_expr = { (^"NOT" ~ unary_expr) | comparison | ("(" ~ expr ~ ")") }
comparison = { prop_access ~ compare_op ~ literal }
compare_op = { "<>" | "<=" | ">=" | "=" | "<" | ">" }
prop_access = { identifier ~ "." ~ identifier }
literal = { float_literal | int_literal | string_literal | bool_literal | null_literal | param }
int_literal = @{ "-"? ~ ASCII_DIGIT+ }
float_literal = @{ "-"? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
string_literal = @{ "'" ~ (!"'" ~ ANY)* ~ "'" }
bool_literal = @{ ^"true" | ^"false" }
null_literal = @{ ^"null" }
param = { "$" ~ identifier }
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }