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        Rule::identifier => Ok(UnwindSource::Var(inner.as_str().to_string())),
212        r => unreachable!("unexpected unwind_source child rule {r:?}"),
213    }
214}
215
216fn parse_match_part(pair: Pair<Rule>) -> Result<QueryPart, QueryError> {
217    let mut optional = false;
218    let mut path_var = None;
219    let mut shortest_path = false;
220    let mut patterns = Vec::new();
221    let mut where_clause = None;
222    let mut with = None;
223    for p in pair.into_inner() {
224        match p.as_rule() {
225            Rule::match_keyword => {
226                optional = p.as_str().to_ascii_uppercase().starts_with("OPTIONAL");
227            }
228            Rule::path_pattern => {
229                let (var, is_shortest, pattern) = parse_path_pattern(p)?;
230                path_var = var;
231                shortest_path = is_shortest;
232                patterns.push(pattern);
233            }
234            Rule::pattern => patterns.push(parse_pattern(p)?),
235            Rule::where_clause => {
236                let expr_pair = p.into_inner().next().expect("WHERE has an expr");
237                where_clause = Some(parse_expr(expr_pair)?);
238            }
239            Rule::with_clause => with = Some(parse_with_clause(p)?),
240            r => unreachable!("unexpected match_part child rule {r:?}"),
241        }
242    }
243    let pattern = splice_patterns(patterns)?;
244    if shortest_path {
245        validate_shortest_path_pattern(&pattern)?;
246    } else if path_var.is_some() {
247        validate_named_path_pattern(&pattern)?;
248    }
249    Ok(QueryPart {
250        optional,
251        path_var,
252        shortest_path,
253        pattern,
254        where_clause,
255        with,
256    })
257}
258
259fn parse_path_pattern(pair: Pair<Rule>) -> Result<(Option<String>, bool, Pattern), QueryError> {
260    let mut var = None;
261    let mut shortest_path = false;
262    let mut pattern = None;
263    for p in pair.into_inner() {
264        match p.as_rule() {
265            Rule::identifier => var = Some(p.as_str().to_string()),
266            Rule::shortest_path_wrapper => {
267                shortest_path = true;
268                let inner_pattern = p.into_inner().next().expect("shortest_path_wrapper has a pattern");
269                pattern = Some(parse_pattern(inner_pattern)?);
270            }
271            Rule::pattern => pattern = Some(parse_pattern(p)?),
272            r => unreachable!("unexpected path_pattern child rule {r:?}"),
273        }
274    }
275    Ok((var, shortest_path, pattern.expect("path_pattern always has a pattern or shortest_path_wrapper")))
276}
277
278/// `shortestPath()`'s inner pattern must be exactly the shape it's built
279/// for: one variable-length hop between two nodes — not fixed-hop (nothing
280/// to search shortest-among), not multi-hop (which hop would even be the
281/// variable-length one is ambiguous), not hopless (no relationship to
282/// traverse at all).
283fn validate_shortest_path_pattern(pattern: &Pattern) -> Result<(), QueryError> {
284    if pattern.hops.len() != 1 || pattern.hops[0].0.hop_range.is_none() {
285        return Err(QueryError::Parse(
286            "shortestPath() requires exactly one variable-length relationship pattern (e.g. (a)-[:TYPE*..5]-(b))"
287                .into(),
288        ));
289    }
290    Ok(())
291}
292
293/// General named-path capture (`p = (a)-->(b)`, no `shortestPath()`) is
294/// limited to fixed-hop patterns — see `QueryPart::path_var`'s docs for
295/// why a variable-length hop isn't supported there.
296fn validate_named_path_pattern(pattern: &Pattern) -> Result<(), QueryError> {
297    if pattern.hops.iter().any(|(rel, _)| rel.hop_range.is_some()) {
298        return Err(QueryError::Parse(
299            "named-path capture (`p = ...`) over a variable-length relationship pattern isn't supported yet \
300             — use shortestPath() instead, or drop the path variable"
301                .into(),
302        ));
303    }
304    Ok(())
305}
306
307fn parse_with_clause(pair: Pair<Rule>) -> Result<WithClause, QueryError> {
308    let mut items = Vec::new();
309    let mut where_clause = None;
310    let mut order_by = None;
311    let mut limit = None;
312    for p in pair.into_inner() {
313        match p.as_rule() {
314            Rule::return_item => items.push(parse_return_item(p)?),
315            Rule::with_where_clause => {
316                let expr_pair = p.into_inner().next().expect("WITH...WHERE has a with_expr");
317                where_clause = Some(parse_with_expr(expr_pair)?);
318            }
319            Rule::order_by_clause => order_by = Some(parse_order_by_clause(p)?),
320            Rule::limit_clause => limit = Some(parse_limit_clause(p)?),
321            r => unreachable!("unexpected with_clause child rule {r:?}"),
322        }
323    }
324    Ok(WithClause {
325        items,
326        where_clause,
327        order_by,
328        limit,
329    })
330}
331
332fn parse_with_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
333    // with_expr = { with_or_expr }
334    parse_with_or_expr(pair.into_inner().next().expect("with_expr has a with_or_expr"))
335}
336
337fn parse_with_or_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
338    let mut parts = pair.into_inner();
339    let mut acc = parse_with_and_expr(parts.next().expect("with_or_expr has at least one with_and_expr"))?;
340    for rest in parts {
341        acc = WithExpr::Or(Box::new(acc), Box::new(parse_with_and_expr(rest)?));
342    }
343    Ok(acc)
344}
345
346fn parse_with_and_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
347    let mut parts = pair.into_inner();
348    let mut acc = parse_with_unary_expr(parts.next().expect("with_and_expr has at least one with_unary_expr"))?;
349    for rest in parts {
350        acc = WithExpr::And(Box::new(acc), Box::new(parse_with_unary_expr(rest)?));
351    }
352    Ok(acc)
353}
354
355fn parse_with_unary_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
356    let inner = pair.into_inner().next().expect("with_unary_expr has one child");
357    match inner.as_rule() {
358        Rule::with_unary_expr => Ok(WithExpr::Not(Box::new(parse_with_unary_expr(inner)?))),
359        Rule::with_comparison => parse_with_comparison(inner),
360        Rule::with_expr => parse_with_expr(inner),
361        r => unreachable!("unexpected with_unary_expr child rule {r:?}"),
362    }
363}
364
365fn parse_with_comparison(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
366    let mut inner = pair.into_inner();
367    let lhs = parse_return_expr(inner.next().expect("with_comparison has a return_expr"))?;
368    let op_pair = inner.next().expect("with_comparison has a compare_op");
369    let op = parse_compare_op(op_pair);
370    let literal = parse_literal(inner.next().expect("with_comparison has a literal"))?;
371    Ok(WithExpr::Compare(lhs, op, literal))
372}
373
374fn parse_order_by_clause(pair: Pair<Rule>) -> Result<Vec<(ReturnExpr, SortDir)>, QueryError> {
375    pair.into_inner()
376        .filter(|c| c.as_rule() == Rule::sort_item)
377        .map(parse_sort_item)
378        .collect()
379}
380
381fn parse_limit_clause(pair: Pair<Rule>) -> Result<i64, QueryError> {
382    let n_pair = pair.into_inner().next().expect("LIMIT has an int_literal");
383    n_pair
384        .as_str()
385        .parse::<i64>()
386        .map_err(|_| QueryError::Parse("invalid LIMIT value".into()))
387}
388
389/// Merges comma-separated patterns within one `MATCH` into a single linear
390/// `Pattern`. Not a general cross-join — each subsequent pattern's start
391/// variable must be exactly the previous pattern's last-introduced
392/// variable (e.g. IS2's `MATCH (message)-[...]->(post:Post), (post)-[...]->
393/// (person)`, where `post` is both the first pattern's end and the second's
394/// start). Any labels/props the continuing pattern restates on that shared
395/// variable are merged in as additional filters. Non-linear/branching
396/// comma patterns (sharing a variable that isn't this exact splice point)
397/// are rejected rather than silently mishandled.
398fn splice_patterns(mut patterns: Vec<Pattern>) -> Result<Pattern, QueryError> {
399    if patterns.is_empty() {
400        return Err(QueryError::Parse("MATCH requires a pattern".into()));
401    }
402    let mut combined = patterns.remove(0);
403    for next in patterns {
404        let Some(start_var) = next.start.var.clone() else {
405            return Err(QueryError::Parse(
406                "a comma-separated MATCH pattern must start from a named variable".into(),
407            ));
408        };
409        let last_var = combined
410            .hops
411            .last()
412            .map(|(_, n)| n.var.clone())
413            .unwrap_or_else(|| combined.start.var.clone());
414        if last_var.as_deref() != Some(start_var.as_str()) {
415            return Err(QueryError::Parse(format!(
416                "comma-separated MATCH pattern must continue from the previous pattern's last \
417                 variable ('{}'), not '{start_var}' — general cross-joins aren't supported",
418                last_var.unwrap_or_default()
419            )));
420        }
421        let target = match combined.hops.last_mut() {
422            Some((_, node)) => node,
423            None => &mut combined.start,
424        };
425        target.labels.extend(next.start.labels);
426        target.props.extend(next.start.props);
427        combined.hops.extend(next.hops);
428    }
429    Ok(combined)
430}
431
432fn parse_sort_item(pair: Pair<Rule>) -> Result<(ReturnExpr, SortDir), QueryError> {
433    let mut inner = pair.into_inner();
434    let expr = parse_return_expr(inner.next().expect("sort_item has a return_expr"))?;
435    let dir = match inner.next() {
436        Some(d) if d.as_str().eq_ignore_ascii_case("desc") => SortDir::Desc,
437        _ => SortDir::Asc,
438    };
439    Ok((expr, dir))
440}
441
442fn parse_tail_clause(pair: Pair<Rule>) -> Result<Tail, QueryError> {
443    let inner = pair.into_inner().next().expect("tail_clause has one child");
444    match inner.as_rule() {
445        Rule::return_clause => {
446            let items = inner
447                .into_inner()
448                .filter(|p| p.as_rule() == Rule::return_item)
449                .map(parse_return_item)
450                .collect::<Result<Vec<_>, _>>()?;
451            Ok(Tail::Return(items))
452        }
453        Rule::detach_delete_clause => {
454            let vars = inner
455                .into_inner()
456                .filter(|p| p.as_rule() == Rule::identifier)
457                .map(|p| p.as_str().to_string())
458                .collect();
459            Ok(Tail::DetachDelete(vars))
460        }
461        Rule::delete_clause => {
462            let vars = inner
463                .into_inner()
464                .filter(|p| p.as_rule() == Rule::identifier)
465                .map(|p| p.as_str().to_string())
466                .collect();
467            Ok(Tail::Delete(vars))
468        }
469        Rule::set_clause => {
470            let items = inner
471                .into_inner()
472                .filter(|p| p.as_rule() == Rule::set_item)
473                .map(parse_set_item)
474                .collect::<Result<Vec<_>, _>>()?;
475            Ok(Tail::Set(items))
476        }
477        Rule::create_stmt => Ok(Tail::Create(parse_create_patterns(inner)?)),
478        r => unreachable!("unexpected tail_clause child rule {r:?}"),
479    }
480}
481
482fn parse_set_item(pair: Pair<Rule>) -> Result<(PropAccess, Literal), QueryError> {
483    let mut inner = pair.into_inner();
484    let prop_access_pair = inner.next().expect("set_item has a prop_access");
485    let literal_pair = inner.next().expect("set_item has a literal");
486    Ok((parse_prop_access(prop_access_pair), parse_literal(literal_pair)?))
487}
488
489fn parse_return_item(pair: Pair<Rule>) -> Result<ReturnItem, QueryError> {
490    let mut inner = pair.into_inner();
491    let expr_pair = inner.next().expect("return_item has a return_expr");
492    let expr = parse_return_expr(expr_pair)?;
493    let alias = inner.next().map(|p| p.as_str().to_string());
494    Ok(ReturnItem { expr, alias })
495}
496
497fn parse_return_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
498    let inner = pair.into_inner().next().expect("return_expr has one child");
499    match inner.as_rule() {
500        Rule::case_expr => parse_case_expr(inner),
501        Rule::function_call => parse_function_call(inner),
502        Rule::prop_access => Ok(ReturnExpr::Prop(parse_prop_access(inner))),
503        Rule::literal => Ok(ReturnExpr::Lit(parse_literal(inner)?)),
504        Rule::identifier => Ok(ReturnExpr::Var(inner.as_str().to_string())),
505        r => unreachable!("unexpected return_expr child rule {r:?}"),
506    }
507}
508
509fn parse_case_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
510    let mut inner = pair.into_inner();
511    let test = parse_return_expr(inner.next().expect("case_expr has a test expr"))?;
512    let mut whens = Vec::new();
513    let mut else_ = None;
514    for p in inner {
515        match p.as_rule() {
516            Rule::case_when => {
517                let mut when_inner = p.into_inner();
518                let when = parse_return_expr(when_inner.next().expect("case_when has a WHEN expr"))?;
519                let then = parse_return_expr(when_inner.next().expect("case_when has a THEN expr"))?;
520                whens.push((when, then));
521            }
522            // The only other possible child is the trailing ELSE return_expr.
523            _ => else_ = Some(Box::new(parse_return_expr(p)?)),
524        }
525    }
526    Ok(ReturnExpr::Case {
527        test: Some(Box::new(test)),
528        whens,
529        else_,
530    })
531}
532
533fn parse_function_call(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
534    let mut inner = pair.into_inner();
535    let name = inner.next().expect("function_call has a name").as_str().to_string();
536    let call_args = inner.next().expect("function_call has call_args");
537    let is_star = call_args.as_str().trim() == "*";
538    if is_star {
539        if !name.eq_ignore_ascii_case("count") {
540            return Err(QueryError::Parse(format!(
541                "'{name}(*)' isn't valid — '*' is only meaningful for count(*)"
542            )));
543        }
544        return Ok(ReturnExpr::CountStar);
545    }
546    let mut distinct = false;
547    let mut args = Vec::new();
548    for p in call_args.into_inner() {
549        match p.as_rule() {
550            Rule::distinct_kw => distinct = true,
551            _ => args.push(parse_return_expr(p)?),
552        }
553    }
554    if distinct && !is_aggregate_name(&name) {
555        return Err(QueryError::Parse(format!(
556            "'{name}(DISTINCT ...)' isn't valid — DISTINCT is only meaningful inside an aggregate function"
557        )));
558    }
559    Ok(ReturnExpr::Call { name, args, distinct })
560}
561
562fn parse_prop_access(pair: Pair<Rule>) -> PropAccess {
563    let mut inner = pair.into_inner();
564    let var = inner.next().expect("prop_access has a var").as_str().to_string();
565    let prop = inner.next().expect("prop_access has a prop").as_str().to_string();
566    PropAccess { var, prop }
567}
568
569fn parse_pattern(pair: Pair<Rule>) -> Result<Pattern, QueryError> {
570    let mut inner = pair.into_inner();
571    let start = parse_node_pattern(inner.next().expect("pattern has a start node"))?;
572    let mut hops = Vec::new();
573    loop {
574        let Some(rel_pair) = inner.next() else { break };
575        let node_pair = inner
576            .next()
577            .ok_or_else(|| QueryError::Parse("dangling relationship in pattern".into()))?;
578        hops.push((parse_rel_pattern(rel_pair)?, parse_node_pattern(node_pair)?));
579    }
580    Ok(Pattern { start, hops })
581}
582
583fn parse_node_pattern(pair: Pair<Rule>) -> Result<NodePattern, QueryError> {
584    let mut var = None;
585    let mut labels = Vec::new();
586    let mut props = Vec::new();
587    for p in pair.into_inner() {
588        match p.as_rule() {
589            Rule::node_var => var = Some(p.as_str().to_string()),
590            Rule::node_label => {
591                labels.push(p.into_inner().next().expect("node_label has an identifier").as_str().to_string())
592            }
593            Rule::prop_map => props = parse_prop_map(p)?,
594            r => unreachable!("unexpected node_pattern child rule {r:?}"),
595        }
596    }
597    Ok(NodePattern { var, labels, props })
598}
599
600fn parse_rel_pattern(pair: Pair<Rule>) -> Result<RelPattern, QueryError> {
601    let inner = pair.into_inner().next().expect("rel_pattern has one child");
602    let direction = match inner.as_rule() {
603        Rule::rel_right => RelDirection::Right,
604        Rule::rel_left => RelDirection::Left,
605        Rule::rel_either => RelDirection::Either,
606        r => unreachable!("unexpected rel_pattern child rule {r:?}"),
607    };
608    let mut var = None;
609    let mut rel_type = None;
610    let mut props = Vec::new();
611    let mut hop_range = None;
612    for p in inner.into_inner() {
613        match p.as_rule() {
614            Rule::rel_var => var = Some(p.as_str().to_string()),
615            Rule::rel_type => {
616                rel_type = Some(p.into_inner().next().expect("rel_type has an identifier").as_str().to_string())
617            }
618            Rule::rel_range => hop_range = Some(parse_rel_range(p.as_str())?),
619            Rule::prop_map => props = parse_prop_map(p)?,
620            r => unreachable!("unexpected rel_right/rel_left/rel_either child rule {r:?}"),
621        }
622    }
623    Ok(RelPattern {
624        var,
625        rel_type,
626        props,
627        direction,
628        hop_range,
629    })
630}
631
632/// Parses the raw `rel_range` text (`*`, `*N`, `*N..`, `*N..M`, `*..M`)
633/// directly rather than via sub-rules, since the `..` literal produces no
634/// child `Pair` to structurally distinguish "*N" (exact) from "*N.." (N or
635/// more).
636fn parse_rel_range(text: &str) -> Result<(u32, Option<u32>), QueryError> {
637    let rest = &text[1..]; // strip leading '*'
638    if rest.is_empty() {
639        return Ok((0, None));
640    }
641    if let Some(idx) = rest.find("..") {
642        let min_str = &rest[..idx];
643        let max_str = &rest[idx + 2..];
644        let min = if min_str.is_empty() {
645            0
646        } else {
647            min_str
648                .parse()
649                .map_err(|_| QueryError::Parse("invalid variable-length min hop count".into()))?
650        };
651        let max = if max_str.is_empty() {
652            None
653        } else {
654            Some(
655                max_str
656                    .parse()
657                    .map_err(|_| QueryError::Parse("invalid variable-length max hop count".into()))?,
658            )
659        };
660        Ok((min, max))
661    } else {
662        let n: u32 = rest
663            .parse()
664            .map_err(|_| QueryError::Parse("invalid variable-length hop count".into()))?;
665        Ok((n, Some(n)))
666    }
667}
668
669fn parse_prop_map(pair: Pair<Rule>) -> Result<Vec<(String, Literal)>, QueryError> {
670    pair.into_inner()
671        .filter(|p| p.as_rule() == Rule::prop_kv)
672        .map(|p| {
673            let mut inner = p.into_inner();
674            let key = inner.next().expect("prop_kv has a key").as_str().to_string();
675            let value = parse_literal(inner.next().expect("prop_kv has a value"))?;
676            Ok((key, value))
677        })
678        .collect()
679}
680
681/// Resolves `\`-escapes in a `string_literal`'s already-quote-stripped
682/// inner text. The grammar accepts any `\`-prefixed char (see
683/// `cypher.pest`'s comment); only a fixed recognized set actually means
684/// something -- an unrecognized escape (e.g. `\q`) errors here rather
685/// than silently dropping the backslash or passing it through, matching
686/// this codebase's stance elsewhere (error on an untested shape, don't
687/// guess). No `\uXXXX` unicode escapes -- not needed yet, noted as a gap
688/// in the README alongside the other documented Cypher-coverage gaps.
689fn unescape_string(s: &str) -> Result<String, QueryError> {
690    if !s.contains('\\') {
691        return Ok(s.to_string());
692    }
693    let mut out = String::with_capacity(s.len());
694    let mut chars = s.chars();
695    while let Some(c) = chars.next() {
696        if c != '\\' {
697            out.push(c);
698            continue;
699        }
700        match chars.next() {
701            Some('\\') => out.push('\\'),
702            Some('\'') => out.push('\''),
703            Some('"') => out.push('"'),
704            Some('n') => out.push('\n'),
705            Some('r') => out.push('\r'),
706            Some('t') => out.push('\t'),
707            Some('b') => out.push('\u{8}'),
708            Some('f') => out.push('\u{c}'),
709            Some(other) => {
710                return Err(QueryError::Parse(format!("unrecognized string escape '\\{other}'")))
711            }
712            None => return Err(QueryError::Parse("string ends with a trailing '\\'".into())),
713        }
714    }
715    Ok(out)
716}
717
718fn parse_literal(pair: Pair<Rule>) -> Result<Literal, QueryError> {
719    let inner = pair.into_inner().next().expect("literal has one child");
720    Ok(match inner.as_rule() {
721        Rule::int_literal => Literal::Int(
722            inner
723                .as_str()
724                .parse()
725                .map_err(|_| QueryError::Parse("invalid integer literal".into()))?,
726        ),
727        Rule::float_literal => Literal::Float(
728            inner
729                .as_str()
730                .parse()
731                .map_err(|_| QueryError::Parse("invalid float literal".into()))?,
732        ),
733        Rule::string_literal => {
734            let s = inner.as_str();
735            Literal::String(unescape_string(&s[1..s.len() - 1])?)
736        }
737        Rule::bool_literal => Literal::Bool(inner.as_str().eq_ignore_ascii_case("true")),
738        Rule::null_literal => Literal::Null,
739        Rule::param => {
740            let name = inner.into_inner().next().expect("param has an identifier").as_str().to_string();
741            Literal::Param(name)
742        }
743        r => unreachable!("unexpected literal child rule {r:?}"),
744    })
745}
746
747fn parse_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
748    // expr = { or_expr }
749    parse_or_expr(pair.into_inner().next().expect("expr has an or_expr"))
750}
751
752fn parse_or_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
753    let mut parts = pair.into_inner();
754    let mut acc = parse_and_expr(parts.next().expect("or_expr has at least one and_expr"))?;
755    for rest in parts {
756        acc = Expr::Or(Box::new(acc), Box::new(parse_and_expr(rest)?));
757    }
758    Ok(acc)
759}
760
761fn parse_and_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
762    let mut parts = pair.into_inner();
763    let mut acc = parse_unary_expr(parts.next().expect("and_expr has at least one unary_expr"))?;
764    for rest in parts {
765        acc = Expr::And(Box::new(acc), Box::new(parse_unary_expr(rest)?));
766    }
767    Ok(acc)
768}
769
770fn parse_unary_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
771    let inner = pair.into_inner().next().expect("unary_expr has one child");
772    match inner.as_rule() {
773        Rule::unary_expr => Ok(Expr::Not(Box::new(parse_unary_expr(inner)?))),
774        Rule::comparison => parse_comparison(inner),
775        Rule::expr => parse_expr(inner),
776        r => unreachable!("unexpected unary_expr child rule {r:?}"),
777    }
778}
779
780fn parse_comparison(pair: Pair<Rule>) -> Result<Expr, QueryError> {
781    let mut inner = pair.into_inner();
782    let prop_access = parse_prop_access(inner.next().expect("comparison has a prop_access"));
783    let op = parse_compare_op(inner.next().expect("comparison has a compare_op"));
784    let literal = parse_literal(inner.next().expect("comparison has a literal"))?;
785    Ok(Expr::Compare(prop_access, op, literal))
786}
787
788fn parse_compare_op(pair: Pair<Rule>) -> CompareOp {
789    match pair.as_str() {
790        "=" => CompareOp::Eq,
791        "<>" => CompareOp::Ne,
792        "<" => CompareOp::Lt,
793        "<=" => CompareOp::Le,
794        ">" => CompareOp::Gt,
795        ">=" => CompareOp::Ge,
796        other => unreachable!("unexpected compare_op {other:?}"),
797    }
798}