marsdb-query 0.2.0

openCypher query subset parser, planner, and executor used internally by MarsDB.
Documentation
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }

query = { SOI ~ 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)* ~ 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 ~ "(" ~ (return_expr ~ ("," ~ return_expr)*)? ~ ")" }

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 | "_")* }