Skip to main content

marsdb_query/
parser.rs

1use pest::iterators::Pair;
2use pest::Parser;
3use pest_derive::Parser;
4
5use crate::ast::*;
6use crate::error::QueryError;
7
8#[derive(Parser)]
9#[grammar = "cypher.pest"]
10struct CypherParser;
11
12pub fn parse(input: &str) -> Result<Statement, QueryError> {
13    let mut pairs = CypherParser::parse(Rule::query, input)
14        .map_err(|e| QueryError::Parse(e.to_string()))?;
15    let query_pair = pairs.next().expect("query rule always produces one pair");
16    let statement_pair = query_pair
17        .into_inner()
18        .find(|p| p.as_rule() == Rule::statement)
19        .expect("query grammar guarantees a statement");
20    parse_statement(statement_pair)
21}
22
23/// Parses a `;`-separated batch of one or more statements (e.g.
24/// `"CREATE (a); CREATE (b); MATCH (n) RETURN n"`). A `;` inside a string
25/// literal doesn't split anything — see `queries`' grammar comment.
26pub fn parse_many(input: &str) -> Result<Vec<Statement>, QueryError> {
27    let mut pairs = CypherParser::parse(Rule::queries, input)
28        .map_err(|e| QueryError::Parse(e.to_string()))?;
29    let queries_pair = pairs.next().expect("queries rule always produces one pair");
30    queries_pair
31        .into_inner()
32        .filter(|p| p.as_rule() == Rule::statement)
33        .map(parse_statement)
34        .collect()
35}
36
37fn parse_statement(pair: Pair<Rule>) -> Result<Statement, QueryError> {
38    let inner = pair.into_inner().next().expect("statement has one child");
39    match inner.as_rule() {
40        Rule::create_stmt => parse_create_stmt(inner),
41        Rule::match_stmt => parse_match_stmt(inner),
42        r => unreachable!("unexpected statement child rule {r:?}"),
43    }
44}
45
46fn parse_create_stmt(pair: Pair<Rule>) -> Result<Statement, QueryError> {
47    Ok(Statement::Create(parse_create_patterns(pair)?))
48}
49
50/// Shared by standalone `CREATE` (`parse_create_stmt`) and a `MATCH ...
51/// CREATE` tail (`parse_tail_clause`'s `create_stmt` arm) — both reuse the
52/// `create_stmt` grammar rule (`^"CREATE" ~ pattern ~ ("," ~ pattern)*`),
53/// only what the executor does with the resulting patterns differs.
54fn parse_create_patterns(pair: Pair<Rule>) -> Result<Vec<Pattern>, QueryError> {
55    pair.into_inner()
56        .filter(|p| p.as_rule() == Rule::pattern)
57        .map(parse_pattern)
58        .collect()
59}
60
61fn parse_match_stmt(pair: Pair<Rule>) -> Result<Statement, QueryError> {
62    let mut clauses = Vec::new();
63    let mut tail = None;
64    let mut order_by = None;
65    let mut limit = None;
66    for p in pair.into_inner() {
67        match p.as_rule() {
68            Rule::clause => clauses.push(parse_clause(p)?),
69            Rule::tail_clause => tail = Some(parse_tail_clause(p)?),
70            Rule::order_by_clause => order_by = Some(parse_order_by_clause(p)?),
71            Rule::limit_clause => limit = Some(parse_limit_clause(p)?),
72            r => unreachable!("unexpected match_stmt child rule {r:?}"),
73        }
74    }
75
76    // Mirrors real Cypher's rule that multiple reading clauses need a WITH
77    // between them, and additionally caps chaining at one WITH boundary
78    // total — nothing IS1-7 needs requires more, and a hand-rolled parser
79    // is safer erroring on untested shapes than silently mishandling them.
80    // OPTIONAL MATCH and UNWIND are both exempt from the WITH-separation
81    // requirement (matching real Cypher: `MATCH (a) OPTIONAL MATCH (b) ...`
82    // and `MATCH (a) UNWIND [1,2] AS x ...` are both valid without a WITH
83    // between them — they continue in the same scope rather than starting
84    // a fresh reading context). The one-WITH-total cap still counts every
85    // clause kind's `with` uniformly.
86    let with_count = clauses.iter().filter(|c| clause_with(c).is_some()).count();
87    if with_count > 1 {
88        return Err(QueryError::Parse(
89            "chaining past one WITH boundary in a single MATCH isn't supported yet".into(),
90        ));
91    }
92    for i in 0..clauses.len() {
93        let (QueryClause::Match(part), Some(QueryClause::Match(next))) = (&clauses[i], clauses.get(i + 1)) else {
94            continue;
95        };
96        if part.with.is_none() && !next.optional {
97            return Err(QueryError::Parse(
98                "multiple MATCH clauses must be separated by WITH".into(),
99            ));
100        }
101    }
102
103    // A missing tail is only valid when a MERGE clause is present (a bare
104    // `MERGE (n:Label)`, a pure write with nothing to return — same as
105    // standalone CREATE). Otherwise a missing tail is almost certainly a
106    // mistake (`MATCH (n)` alone does nothing at all), so it's still
107    // rejected.
108    if tail.is_none() && !clauses.iter().any(|c| matches!(c, QueryClause::Merge(_))) {
109        return Err(QueryError::Parse(
110            "a query needs a RETURN/DELETE/SET tail, unless it has a MERGE clause with nothing after it".into(),
111        ));
112    }
113
114    Ok(Statement::Match {
115        clauses,
116        tail,
117        order_by,
118        limit,
119    })
120}
121
122fn clause_with(clause: &QueryClause) -> Option<&WithClause> {
123    match clause {
124        QueryClause::Match(part) => part.with.as_ref(),
125        QueryClause::Unwind(u) => u.with.as_ref(),
126        QueryClause::Merge(m) => m.with.as_ref(),
127    }
128}
129
130fn parse_clause(pair: Pair<Rule>) -> Result<QueryClause, QueryError> {
131    let inner = pair.into_inner().next().expect("clause has one child");
132    match inner.as_rule() {
133        Rule::match_part => Ok(QueryClause::Match(parse_match_part(inner)?)),
134        Rule::unwind_clause => Ok(QueryClause::Unwind(parse_unwind_clause(inner)?)),
135        Rule::merge_clause => Ok(QueryClause::Merge(parse_merge_clause(inner)?)),
136        r => unreachable!("unexpected clause child rule {r:?}"),
137    }
138}
139
140/// `pattern.hops.len() > 1` is rejected here, not left to the executor —
141/// whole-pattern atomicity across multiple simultaneously-unbound hops
142/// isn't attempted in v1 (see `executor::eval_merge`'s docs), so a clear
143/// parse-time error is better than a confusing runtime one.
144fn parse_merge_clause(pair: Pair<Rule>) -> Result<MergeClause, QueryError> {
145    let mut inner = pair.into_inner();
146    let pattern = parse_pattern(inner.next().expect("merge_clause has a pattern"))?;
147    if pattern.hops.len() > 1 {
148        return Err(QueryError::Parse(
149            "MERGE with more than one relationship hop isn't supported yet — split it into a MATCH \
150             for the already-known part and a MERGE for one new hop"
151                .into(),
152        ));
153    }
154    let mut on_create = Vec::new();
155    let mut on_match = Vec::new();
156    let mut with = None;
157    for p in inner {
158        match p.as_rule() {
159            Rule::on_create_clause => {
160                on_create = p.into_inner().filter(|p| p.as_rule() == Rule::set_item).map(parse_set_item).collect::<Result<_, _>>()?;
161            }
162            Rule::on_match_clause => {
163                on_match = p.into_inner().filter(|p| p.as_rule() == Rule::set_item).map(parse_set_item).collect::<Result<_, _>>()?;
164            }
165            Rule::with_clause => with = Some(parse_with_clause(p)?),
166            r => unreachable!("unexpected merge_clause child rule {r:?}"),
167        }
168    }
169    Ok(MergeClause {
170        pattern,
171        on_create,
172        on_match,
173        with,
174    })
175}
176
177fn parse_unwind_clause(pair: Pair<Rule>) -> Result<UnwindClause, QueryError> {
178    let mut inner = pair.into_inner();
179    let source = parse_unwind_source(inner.next().expect("unwind_clause has an unwind_source"))?;
180    let var = inner.next().expect("unwind_clause has an AS identifier").as_str().to_string();
181    let mut where_clause = None;
182    let mut with = None;
183    for p in inner {
184        match p.as_rule() {
185            Rule::with_where_clause => {
186                let expr_pair = p.into_inner().next().expect("WHERE has a with_expr");
187                where_clause = Some(parse_with_expr(expr_pair)?);
188            }
189            Rule::with_clause => with = Some(parse_with_clause(p)?),
190            r => unreachable!("unexpected unwind_clause child rule {r:?}"),
191        }
192    }
193    Ok(UnwindClause {
194        source,
195        var,
196        where_clause,
197        with,
198    })
199}
200
201fn parse_unwind_source(pair: Pair<Rule>) -> Result<UnwindSource, QueryError> {
202    let inner = pair.into_inner().next().expect("unwind_source has one child");
203    match inner.as_rule() {
204        Rule::list_literal => Ok(UnwindSource::List(
205            inner
206                .into_inner()
207                .filter(|p| p.as_rule() == Rule::literal)
208                .map(parse_literal)
209                .collect::<Result<Vec<_>, _>>()?,
210        )),
211        // `UNWIND null AS x` is real Cypher -- unwinding null behaves like
212        // unwinding an empty list (zero rows), not a bound variable lookup.
213        Rule::null_literal => Ok(UnwindSource::List(vec![])),
214        Rule::identifier => Ok(UnwindSource::Var(inner.as_str().to_string())),
215        r => unreachable!("unexpected unwind_source child rule {r:?}"),
216    }
217}
218
219fn parse_match_part(pair: Pair<Rule>) -> Result<QueryPart, QueryError> {
220    let mut optional = false;
221    let mut path_var = None;
222    let mut shortest_path = false;
223    let mut patterns = Vec::new();
224    let mut where_clause = None;
225    let mut with = None;
226    for p in pair.into_inner() {
227        match p.as_rule() {
228            Rule::match_keyword => {
229                optional = p.as_str().to_ascii_uppercase().starts_with("OPTIONAL");
230            }
231            Rule::path_pattern => {
232                let (var, is_shortest, pattern) = parse_path_pattern(p)?;
233                path_var = var;
234                shortest_path = is_shortest;
235                patterns.push(pattern);
236            }
237            Rule::pattern => patterns.push(parse_pattern(p)?),
238            Rule::where_clause => {
239                let expr_pair = p.into_inner().next().expect("WHERE has an expr");
240                where_clause = Some(parse_expr(expr_pair)?);
241            }
242            Rule::with_clause => with = Some(parse_with_clause(p)?),
243            r => unreachable!("unexpected match_part child rule {r:?}"),
244        }
245    }
246    let pattern = splice_patterns(patterns)?;
247    if shortest_path {
248        validate_shortest_path_pattern(&pattern)?;
249    } else if path_var.is_some() {
250        validate_named_path_pattern(&pattern)?;
251    }
252    Ok(QueryPart {
253        optional,
254        path_var,
255        shortest_path,
256        pattern,
257        where_clause,
258        with,
259    })
260}
261
262fn parse_path_pattern(pair: Pair<Rule>) -> Result<(Option<String>, bool, Pattern), QueryError> {
263    let mut var = None;
264    let mut shortest_path = false;
265    let mut pattern = None;
266    for p in pair.into_inner() {
267        match p.as_rule() {
268            Rule::identifier => var = Some(p.as_str().to_string()),
269            Rule::shortest_path_wrapper => {
270                shortest_path = true;
271                let inner_pattern = p.into_inner().next().expect("shortest_path_wrapper has a pattern");
272                pattern = Some(parse_pattern(inner_pattern)?);
273            }
274            Rule::pattern => pattern = Some(parse_pattern(p)?),
275            r => unreachable!("unexpected path_pattern child rule {r:?}"),
276        }
277    }
278    Ok((var, shortest_path, pattern.expect("path_pattern always has a pattern or shortest_path_wrapper")))
279}
280
281/// `shortestPath()`'s inner pattern must be exactly the shape it's built
282/// for: one variable-length hop between two nodes — not fixed-hop (nothing
283/// to search shortest-among), not multi-hop (which hop would even be the
284/// variable-length one is ambiguous), not hopless (no relationship to
285/// traverse at all).
286fn validate_shortest_path_pattern(pattern: &Pattern) -> Result<(), QueryError> {
287    if pattern.hops.len() != 1 || pattern.hops[0].0.hop_range.is_none() {
288        return Err(QueryError::Parse(
289            "shortestPath() requires exactly one variable-length relationship pattern (e.g. (a)-[:TYPE*..5]-(b))"
290                .into(),
291        ));
292    }
293    Ok(())
294}
295
296/// General named-path capture (`p = (a)-->(b)`, no `shortestPath()`) is
297/// limited to fixed-hop patterns — see `QueryPart::path_var`'s docs for
298/// why a variable-length hop isn't supported there.
299fn validate_named_path_pattern(pattern: &Pattern) -> Result<(), QueryError> {
300    if pattern.hops.iter().any(|(rel, _)| rel.hop_range.is_some()) {
301        return Err(QueryError::Parse(
302            "named-path capture (`p = ...`) over a variable-length relationship pattern isn't supported yet \
303             — use shortestPath() instead, or drop the path variable"
304                .into(),
305        ));
306    }
307    Ok(())
308}
309
310fn parse_with_clause(pair: Pair<Rule>) -> Result<WithClause, QueryError> {
311    let mut items = Vec::new();
312    let mut where_clause = None;
313    let mut order_by = None;
314    let mut limit = None;
315    for p in pair.into_inner() {
316        match p.as_rule() {
317            Rule::return_item => items.push(parse_return_item(p)?),
318            Rule::with_where_clause => {
319                let expr_pair = p.into_inner().next().expect("WITH...WHERE has a with_expr");
320                where_clause = Some(parse_with_expr(expr_pair)?);
321            }
322            Rule::order_by_clause => order_by = Some(parse_order_by_clause(p)?),
323            Rule::limit_clause => limit = Some(parse_limit_clause(p)?),
324            r => unreachable!("unexpected with_clause child rule {r:?}"),
325        }
326    }
327    Ok(WithClause {
328        items,
329        where_clause,
330        order_by,
331        limit,
332    })
333}
334
335fn parse_with_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
336    // with_expr = { with_or_expr }
337    parse_with_or_expr(pair.into_inner().next().expect("with_expr has a with_or_expr"))
338}
339
340fn parse_with_or_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
341    let mut parts = pair.into_inner();
342    let mut acc = parse_with_and_expr(parts.next().expect("with_or_expr has at least one with_and_expr"))?;
343    for rest in parts {
344        acc = WithExpr::Or(Box::new(acc), Box::new(parse_with_and_expr(rest)?));
345    }
346    Ok(acc)
347}
348
349fn parse_with_and_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
350    let mut parts = pair.into_inner();
351    let mut acc = parse_with_unary_expr(parts.next().expect("with_and_expr has at least one with_unary_expr"))?;
352    for rest in parts {
353        acc = WithExpr::And(Box::new(acc), Box::new(parse_with_unary_expr(rest)?));
354    }
355    Ok(acc)
356}
357
358fn parse_with_unary_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
359    let inner = pair.into_inner().next().expect("with_unary_expr has one child");
360    match inner.as_rule() {
361        Rule::with_unary_expr => Ok(WithExpr::Not(Box::new(parse_with_unary_expr(inner)?))),
362        Rule::with_comparison => parse_with_comparison(inner),
363        Rule::with_expr => parse_with_expr(inner),
364        r => unreachable!("unexpected with_unary_expr child rule {r:?}"),
365    }
366}
367
368fn parse_with_comparison(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
369    let mut inner = pair.into_inner();
370    let lhs = parse_return_expr(inner.next().expect("with_comparison has a return_expr"))?;
371    let op_pair = inner.next().expect("with_comparison has a compare_op");
372    let op = parse_compare_op(op_pair);
373    let literal = parse_literal(inner.next().expect("with_comparison has a literal"))?;
374    Ok(WithExpr::Compare(lhs, op, literal))
375}
376
377fn parse_order_by_clause(pair: Pair<Rule>) -> Result<Vec<(ReturnExpr, SortDir)>, QueryError> {
378    pair.into_inner()
379        .filter(|c| c.as_rule() == Rule::sort_item)
380        .map(parse_sort_item)
381        .collect()
382}
383
384fn parse_limit_clause(pair: Pair<Rule>) -> Result<i64, QueryError> {
385    let n_pair = pair.into_inner().next().expect("LIMIT has an int_literal");
386    let n = n_pair
387        .as_str()
388        .parse::<i64>()
389        .map_err(|_| QueryError::Parse("invalid LIMIT value".into()))?;
390    if n < 0 {
391        return Err(QueryError::Parse("LIMIT can't be negative".into()));
392    }
393    Ok(n)
394}
395
396/// Merges comma-separated patterns within one `MATCH` into a single linear
397/// `Pattern`. Not a general cross-join — each subsequent pattern's start
398/// variable must be exactly the previous pattern's last-introduced
399/// variable (e.g. IS2's `MATCH (message)-[...]->(post:Post), (post)-[...]->
400/// (person)`, where `post` is both the first pattern's end and the second's
401/// start). Any labels/props the continuing pattern restates on that shared
402/// variable are merged in as additional filters. Non-linear/branching
403/// comma patterns (sharing a variable that isn't this exact splice point)
404/// are rejected rather than silently mishandled.
405fn splice_patterns(mut patterns: Vec<Pattern>) -> Result<Pattern, QueryError> {
406    if patterns.is_empty() {
407        return Err(QueryError::Parse("MATCH requires a pattern".into()));
408    }
409    let mut combined = patterns.remove(0);
410    for next in patterns {
411        let Some(start_var) = next.start.var.clone() else {
412            return Err(QueryError::Parse(
413                "a comma-separated MATCH pattern must start from a named variable".into(),
414            ));
415        };
416        let last_var = combined
417            .hops
418            .last()
419            .map(|(_, n)| n.var.clone())
420            .unwrap_or_else(|| combined.start.var.clone());
421        if last_var.as_deref() != Some(start_var.as_str()) {
422            return Err(QueryError::Parse(format!(
423                "comma-separated MATCH pattern must continue from the previous pattern's last \
424                 variable ('{}'), not '{start_var}' — general cross-joins aren't supported",
425                last_var.unwrap_or_default()
426            )));
427        }
428        let target = match combined.hops.last_mut() {
429            Some((_, node)) => node,
430            None => &mut combined.start,
431        };
432        target.labels.extend(next.start.labels);
433        target.props.extend(next.start.props);
434        combined.hops.extend(next.hops);
435    }
436    Ok(combined)
437}
438
439fn parse_sort_item(pair: Pair<Rule>) -> Result<(ReturnExpr, SortDir), QueryError> {
440    let mut inner = pair.into_inner();
441    let expr = parse_return_expr(inner.next().expect("sort_item has a return_expr"))?;
442    let dir = match inner.next() {
443        Some(d) if d.as_str().eq_ignore_ascii_case("desc") => SortDir::Desc,
444        _ => SortDir::Asc,
445    };
446    Ok((expr, dir))
447}
448
449fn parse_tail_clause(pair: Pair<Rule>) -> Result<Tail, QueryError> {
450    let inner = pair.into_inner().next().expect("tail_clause has one child");
451    match inner.as_rule() {
452        Rule::return_clause => {
453            let children: Vec<_> = inner.into_inner().collect();
454            let distinct = children.iter().any(|p| p.as_rule() == Rule::distinct_kw);
455            let items = children
456                .into_iter()
457                .filter(|p| p.as_rule() == Rule::return_item)
458                .map(parse_return_item)
459                .collect::<Result<Vec<_>, _>>()?;
460            Ok(Tail::Return(items, distinct))
461        }
462        Rule::detach_delete_clause => {
463            let vars = inner
464                .into_inner()
465                .filter(|p| p.as_rule() == Rule::identifier)
466                .map(|p| p.as_str().to_string())
467                .collect();
468            Ok(Tail::DetachDelete(vars))
469        }
470        Rule::delete_clause => {
471            let vars = inner
472                .into_inner()
473                .filter(|p| p.as_rule() == Rule::identifier)
474                .map(|p| p.as_str().to_string())
475                .collect();
476            Ok(Tail::Delete(vars))
477        }
478        Rule::set_clause => {
479            let items = inner
480                .into_inner()
481                .filter(|p| p.as_rule() == Rule::set_item)
482                .map(parse_set_item)
483                .collect::<Result<Vec<_>, _>>()?;
484            Ok(Tail::Set(items))
485        }
486        Rule::remove_clause => {
487            let items = inner.into_inner().filter(|p| p.as_rule() == Rule::remove_item).map(parse_remove_item).collect();
488            Ok(Tail::Remove(items))
489        }
490        Rule::create_stmt => Ok(Tail::Create(parse_create_patterns(inner)?)),
491        r => unreachable!("unexpected tail_clause child rule {r:?}"),
492    }
493}
494
495fn parse_set_item(pair: Pair<Rule>) -> Result<SetItem, QueryError> {
496    let mut inner = pair.into_inner();
497    let first = inner.next().expect("set_item has at least one child");
498    match first.as_rule() {
499        Rule::prop_access => {
500            let literal_pair = inner.next().expect("set_item's prop_access form has a literal");
501            Ok(SetItem::Prop(parse_prop_access(first), parse_literal(literal_pair)?))
502        }
503        Rule::set_label_item => {
504            let (var, labels) = parse_set_label_item(first);
505            Ok(SetItem::Labels(var, labels))
506        }
507        r => unreachable!("unexpected set_item child rule {r:?}"),
508    }
509}
510
511fn parse_set_label_item(pair: Pair<Rule>) -> (String, Vec<String>) {
512    let mut inner = pair.into_inner();
513    let var = inner.next().expect("set_label_item has a var identifier").as_str().to_string();
514    let labels = inner.map(|p| p.as_str().to_string()).collect();
515    (var, labels)
516}
517
518fn parse_remove_item(pair: Pair<Rule>) -> RemoveItem {
519    let inner = pair.into_inner().next().expect("remove_item has one child");
520    match inner.as_rule() {
521        Rule::prop_access => RemoveItem::Prop(parse_prop_access(inner)),
522        Rule::set_label_item => {
523            let (var, labels) = parse_set_label_item(inner);
524            RemoveItem::Labels(var, labels)
525        }
526        r => unreachable!("unexpected remove_item child rule {r:?}"),
527    }
528}
529
530fn parse_return_item(pair: Pair<Rule>) -> Result<ReturnItem, QueryError> {
531    let mut inner = pair.into_inner();
532    let expr_pair = inner.next().expect("return_item has a return_expr");
533    let expr = parse_return_expr(expr_pair)?;
534    let alias = inner.next().map(|p| p.as_str().to_string());
535    Ok(ReturnItem { expr, alias })
536}
537
538fn parse_return_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
539    let inner = pair.into_inner().next().expect("return_expr has one child");
540    match inner.as_rule() {
541        Rule::case_expr => parse_case_expr(inner),
542        Rule::function_call => parse_function_call(inner),
543        Rule::prop_access => Ok(ReturnExpr::Prop(parse_prop_access(inner))),
544        Rule::literal => Ok(ReturnExpr::Lit(parse_literal(inner)?)),
545        Rule::identifier => Ok(ReturnExpr::Var(inner.as_str().to_string())),
546        r => unreachable!("unexpected return_expr child rule {r:?}"),
547    }
548}
549
550fn parse_case_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
551    let mut inner = pair.into_inner();
552    let test = parse_return_expr(inner.next().expect("case_expr has a test expr"))?;
553    let mut whens = Vec::new();
554    let mut else_ = None;
555    for p in inner {
556        match p.as_rule() {
557            Rule::case_when => {
558                let mut when_inner = p.into_inner();
559                let when = parse_return_expr(when_inner.next().expect("case_when has a WHEN expr"))?;
560                let then = parse_return_expr(when_inner.next().expect("case_when has a THEN expr"))?;
561                whens.push((when, then));
562            }
563            // The only other possible child is the trailing ELSE return_expr.
564            _ => else_ = Some(Box::new(parse_return_expr(p)?)),
565        }
566    }
567    Ok(ReturnExpr::Case {
568        test: Some(Box::new(test)),
569        whens,
570        else_,
571    })
572}
573
574fn parse_function_call(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
575    let mut inner = pair.into_inner();
576    let name = inner.next().expect("function_call has a name").as_str().to_string();
577    let call_args = inner.next().expect("function_call has call_args");
578    let is_star = call_args.as_str().trim() == "*";
579    if is_star {
580        if !name.eq_ignore_ascii_case("count") {
581            return Err(QueryError::Parse(format!(
582                "'{name}(*)' isn't valid — '*' is only meaningful for count(*)"
583            )));
584        }
585        return Ok(ReturnExpr::CountStar);
586    }
587    let mut distinct = false;
588    let mut args = Vec::new();
589    for p in call_args.into_inner() {
590        match p.as_rule() {
591            Rule::distinct_kw => distinct = true,
592            _ => args.push(parse_return_expr(p)?),
593        }
594    }
595    if distinct && !is_aggregate_name(&name) {
596        return Err(QueryError::Parse(format!(
597            "'{name}(DISTINCT ...)' isn't valid — DISTINCT is only meaningful inside an aggregate function"
598        )));
599    }
600    Ok(ReturnExpr::Call { name, args, distinct })
601}
602
603fn parse_prop_access(pair: Pair<Rule>) -> PropAccess {
604    let mut inner = pair.into_inner();
605    let var = inner.next().expect("prop_access has a var").as_str().to_string();
606    let prop = inner.next().expect("prop_access has a prop").as_str().to_string();
607    PropAccess { var, prop }
608}
609
610fn parse_pattern(pair: Pair<Rule>) -> Result<Pattern, QueryError> {
611    let mut inner = pair.into_inner();
612    let start = parse_node_pattern(inner.next().expect("pattern has a start node"))?;
613    let mut hops = Vec::new();
614    loop {
615        let Some(rel_pair) = inner.next() else { break };
616        let node_pair = inner
617            .next()
618            .ok_or_else(|| QueryError::Parse("dangling relationship in pattern".into()))?;
619        hops.push((parse_rel_pattern(rel_pair)?, parse_node_pattern(node_pair)?));
620    }
621    Ok(Pattern { start, hops })
622}
623
624fn parse_node_pattern(pair: Pair<Rule>) -> Result<NodePattern, QueryError> {
625    let mut var = None;
626    let mut labels = Vec::new();
627    let mut props = Vec::new();
628    for p in pair.into_inner() {
629        match p.as_rule() {
630            Rule::node_var => var = Some(p.as_str().to_string()),
631            Rule::node_label => {
632                labels.push(p.into_inner().next().expect("node_label has an identifier").as_str().to_string())
633            }
634            Rule::prop_map => props = parse_prop_map(p)?,
635            r => unreachable!("unexpected node_pattern child rule {r:?}"),
636        }
637    }
638    Ok(NodePattern { var, labels, props })
639}
640
641fn parse_rel_pattern(pair: Pair<Rule>) -> Result<RelPattern, QueryError> {
642    let inner = pair.into_inner().next().expect("rel_pattern has one child");
643    let direction = match inner.as_rule() {
644        Rule::rel_right => RelDirection::Right,
645        Rule::rel_left => RelDirection::Left,
646        Rule::rel_either => RelDirection::Either,
647        r => unreachable!("unexpected rel_pattern child rule {r:?}"),
648    };
649    let mut var = None;
650    let mut rel_type = None;
651    let mut props = Vec::new();
652    let mut hop_range = None;
653    for p in inner.into_inner() {
654        match p.as_rule() {
655            Rule::rel_var => var = Some(p.as_str().to_string()),
656            Rule::rel_type => {
657                rel_type = Some(p.into_inner().next().expect("rel_type has an identifier").as_str().to_string())
658            }
659            Rule::rel_range => hop_range = Some(parse_rel_range(p.as_str())?),
660            Rule::prop_map => props = parse_prop_map(p)?,
661            r => unreachable!("unexpected rel_right/rel_left/rel_either child rule {r:?}"),
662        }
663    }
664    Ok(RelPattern {
665        var,
666        rel_type,
667        props,
668        direction,
669        hop_range,
670    })
671}
672
673/// Parses the raw `rel_range` text (`*`, `*N`, `*N..`, `*N..M`, `*..M`)
674/// directly rather than via sub-rules, since the `..` literal produces no
675/// child `Pair` to structurally distinguish "*N" (exact) from "*N.." (N or
676/// more).
677fn parse_rel_range(text: &str) -> Result<(u32, Option<u32>), QueryError> {
678    let rest = &text[1..]; // strip leading '*'
679    if rest.is_empty() {
680        return Ok((0, None));
681    }
682    if let Some(idx) = rest.find("..") {
683        let min_str = &rest[..idx];
684        let max_str = &rest[idx + 2..];
685        let min = if min_str.is_empty() {
686            0
687        } else {
688            min_str
689                .parse()
690                .map_err(|_| QueryError::Parse("invalid variable-length min hop count".into()))?
691        };
692        let max = if max_str.is_empty() {
693            None
694        } else {
695            Some(
696                max_str
697                    .parse()
698                    .map_err(|_| QueryError::Parse("invalid variable-length max hop count".into()))?,
699            )
700        };
701        Ok((min, max))
702    } else {
703        let n: u32 = rest
704            .parse()
705            .map_err(|_| QueryError::Parse("invalid variable-length hop count".into()))?;
706        Ok((n, Some(n)))
707    }
708}
709
710fn parse_prop_map(pair: Pair<Rule>) -> Result<Vec<(String, Literal)>, QueryError> {
711    pair.into_inner()
712        .filter(|p| p.as_rule() == Rule::prop_kv)
713        .map(|p| {
714            let mut inner = p.into_inner();
715            let key = inner.next().expect("prop_kv has a key").as_str().to_string();
716            let value = parse_literal(inner.next().expect("prop_kv has a value"))?;
717            Ok((key, value))
718        })
719        .collect()
720}
721
722/// Resolves `\`-escapes in a `string_literal`'s already-quote-stripped
723/// inner text. The grammar accepts any `\`-prefixed char (see
724/// `cypher.pest`'s comment); only a fixed recognized set actually means
725/// something -- an unrecognized escape (e.g. `\q`) errors here rather
726/// than silently dropping the backslash or passing it through, matching
727/// this codebase's stance elsewhere (error on an untested shape, don't
728/// guess). No `\uXXXX` unicode escapes -- not needed yet, noted as a gap
729/// in the README alongside the other documented Cypher-coverage gaps.
730fn unescape_string(s: &str) -> Result<String, QueryError> {
731    if !s.contains('\\') {
732        return Ok(s.to_string());
733    }
734    let mut out = String::with_capacity(s.len());
735    let mut chars = s.chars();
736    while let Some(c) = chars.next() {
737        if c != '\\' {
738            out.push(c);
739            continue;
740        }
741        match chars.next() {
742            Some('\\') => out.push('\\'),
743            Some('\'') => out.push('\''),
744            Some('"') => out.push('"'),
745            Some('n') => out.push('\n'),
746            Some('r') => out.push('\r'),
747            Some('t') => out.push('\t'),
748            Some('b') => out.push('\u{8}'),
749            Some('f') => out.push('\u{c}'),
750            Some(other) => {
751                return Err(QueryError::Parse(format!("unrecognized string escape '\\{other}'")))
752            }
753            None => return Err(QueryError::Parse("string ends with a trailing '\\'".into())),
754        }
755    }
756    Ok(out)
757}
758
759fn parse_literal(pair: Pair<Rule>) -> Result<Literal, QueryError> {
760    let inner = pair.into_inner().next().expect("literal has one child");
761    Ok(match inner.as_rule() {
762        Rule::int_literal => Literal::Int(
763            inner
764                .as_str()
765                .parse()
766                .map_err(|_| QueryError::Parse("invalid integer literal".into()))?,
767        ),
768        Rule::float_literal => Literal::Float(
769            inner
770                .as_str()
771                .parse()
772                .map_err(|_| QueryError::Parse("invalid float literal".into()))?,
773        ),
774        Rule::string_literal => {
775            let s = inner.as_str();
776            Literal::String(unescape_string(&s[1..s.len() - 1])?)
777        }
778        Rule::bool_literal => Literal::Bool(inner.as_str().eq_ignore_ascii_case("true")),
779        Rule::null_literal => Literal::Null,
780        Rule::param => {
781            let name = inner.into_inner().next().expect("param has an identifier").as_str().to_string();
782            Literal::Param(name)
783        }
784        r => unreachable!("unexpected literal child rule {r:?}"),
785    })
786}
787
788fn parse_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
789    // expr = { or_expr }
790    parse_or_expr(pair.into_inner().next().expect("expr has an or_expr"))
791}
792
793fn parse_or_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
794    let mut parts = pair.into_inner();
795    let mut acc = parse_and_expr(parts.next().expect("or_expr has at least one and_expr"))?;
796    for rest in parts {
797        acc = Expr::Or(Box::new(acc), Box::new(parse_and_expr(rest)?));
798    }
799    Ok(acc)
800}
801
802fn parse_and_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
803    let mut parts = pair.into_inner();
804    let mut acc = parse_unary_expr(parts.next().expect("and_expr has at least one unary_expr"))?;
805    for rest in parts {
806        acc = Expr::And(Box::new(acc), Box::new(parse_unary_expr(rest)?));
807    }
808    Ok(acc)
809}
810
811fn parse_unary_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
812    let inner = pair.into_inner().next().expect("unary_expr has one child");
813    match inner.as_rule() {
814        Rule::unary_expr => Ok(Expr::Not(Box::new(parse_unary_expr(inner)?))),
815        Rule::comparison => parse_comparison(inner),
816        Rule::expr => parse_expr(inner),
817        r => unreachable!("unexpected unary_expr child rule {r:?}"),
818    }
819}
820
821fn parse_comparison(pair: Pair<Rule>) -> Result<Expr, QueryError> {
822    let mut inner = pair.into_inner();
823    let prop_access = parse_prop_access(inner.next().expect("comparison has a prop_access"));
824    let op = parse_compare_op(inner.next().expect("comparison has a compare_op"));
825    let literal = parse_literal(inner.next().expect("comparison has a literal"))?;
826    Ok(Expr::Compare(prop_access, op, literal))
827}
828
829fn parse_compare_op(pair: Pair<Rule>) -> CompareOp {
830    // `STARTS WITH`/`ENDS WITH` are two separate keyword tokens in the
831    // grammar (so any amount of whitespace between them matches, same as
832    // `DETACH DELETE`) -- normalize before matching so the exact source
833    // spacing/casing doesn't matter.
834    let normalized = pair.as_str().split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_uppercase();
835    match normalized.as_str() {
836        "=" => CompareOp::Eq,
837        "<>" => CompareOp::Ne,
838        "<" => CompareOp::Lt,
839        "<=" => CompareOp::Le,
840        ">" => CompareOp::Gt,
841        ">=" => CompareOp::Ge,
842        "STARTS WITH" => CompareOp::StartsWith,
843        "ENDS WITH" => CompareOp::EndsWith,
844        "CONTAINS" => CompareOp::Contains,
845        other => unreachable!("unexpected compare_op {other:?}"),
846    }
847}