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    let patterns = pair
48        .into_inner()
49        .filter(|p| p.as_rule() == Rule::pattern)
50        .map(parse_pattern)
51        .collect::<Result<Vec<_>, _>>()?;
52    Ok(Statement::Create(patterns))
53}
54
55fn parse_match_stmt(pair: Pair<Rule>) -> Result<Statement, QueryError> {
56    let mut parts = Vec::new();
57    let mut tail = None;
58    let mut order_by = None;
59    let mut limit = None;
60    for p in pair.into_inner() {
61        match p.as_rule() {
62            Rule::match_part => parts.push(parse_match_part(p)?),
63            Rule::tail_clause => tail = Some(parse_tail_clause(p)?),
64            Rule::order_by_clause => order_by = Some(parse_order_by_clause(p)?),
65            Rule::limit_clause => limit = Some(parse_limit_clause(p)?),
66            r => unreachable!("unexpected match_stmt child rule {r:?}"),
67        }
68    }
69
70    // Mirrors real Cypher's rule that multiple reading clauses need a WITH
71    // between them, and additionally caps chaining at one WITH boundary
72    // total — nothing IS1-7 needs requires more, and a hand-rolled parser
73    // is safer erroring on untested shapes than silently mishandling them.
74    // OPTIONAL MATCH is exempt from the WITH requirement (matching real
75    // Cypher: `MATCH (a) OPTIONAL MATCH (b) RETURN a, b` is valid without a
76    // WITH between them — OPTIONAL MATCH continues in the same scope
77    // rather than starting a fresh reading context).
78    let with_count = parts.iter().filter(|p| p.with.is_some()).count();
79    if with_count > 1 {
80        return Err(QueryError::Parse(
81            "chaining past one WITH boundary in a single MATCH isn't supported yet".into(),
82        ));
83    }
84    for (i, part) in parts.iter().enumerate() {
85        if i + 1 < parts.len() && part.with.is_none() && !parts[i + 1].optional {
86            return Err(QueryError::Parse(
87                "multiple MATCH clauses must be separated by WITH".into(),
88            ));
89        }
90    }
91
92    Ok(Statement::Match {
93        parts,
94        tail: tail.ok_or_else(|| QueryError::Parse("MATCH requires RETURN/DELETE/SET".into()))?,
95        order_by,
96        limit,
97    })
98}
99
100fn parse_match_part(pair: Pair<Rule>) -> Result<QueryPart, QueryError> {
101    let mut optional = false;
102    let mut patterns = Vec::new();
103    let mut where_clause = None;
104    let mut with = None;
105    for p in pair.into_inner() {
106        match p.as_rule() {
107            Rule::match_keyword => {
108                optional = p.as_str().to_ascii_uppercase().starts_with("OPTIONAL");
109            }
110            Rule::pattern => patterns.push(parse_pattern(p)?),
111            Rule::where_clause => {
112                let expr_pair = p.into_inner().next().expect("WHERE has an expr");
113                where_clause = Some(parse_expr(expr_pair)?);
114            }
115            Rule::with_clause => with = Some(parse_with_clause(p)?),
116            r => unreachable!("unexpected match_part child rule {r:?}"),
117        }
118    }
119    let pattern = splice_patterns(patterns)?;
120    Ok(QueryPart {
121        optional,
122        pattern,
123        where_clause,
124        with,
125    })
126}
127
128fn parse_with_clause(pair: Pair<Rule>) -> Result<WithClause, QueryError> {
129    let mut items = Vec::new();
130    let mut where_clause = None;
131    let mut order_by = None;
132    let mut limit = None;
133    for p in pair.into_inner() {
134        match p.as_rule() {
135            Rule::return_item => items.push(parse_return_item(p)?),
136            Rule::with_where_clause => {
137                let expr_pair = p.into_inner().next().expect("WITH...WHERE has a with_expr");
138                where_clause = Some(parse_with_expr(expr_pair)?);
139            }
140            Rule::order_by_clause => order_by = Some(parse_order_by_clause(p)?),
141            Rule::limit_clause => limit = Some(parse_limit_clause(p)?),
142            r => unreachable!("unexpected with_clause child rule {r:?}"),
143        }
144    }
145    Ok(WithClause {
146        items,
147        where_clause,
148        order_by,
149        limit,
150    })
151}
152
153fn parse_with_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
154    // with_expr = { with_or_expr }
155    parse_with_or_expr(pair.into_inner().next().expect("with_expr has a with_or_expr"))
156}
157
158fn parse_with_or_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
159    let mut parts = pair.into_inner();
160    let mut acc = parse_with_and_expr(parts.next().expect("with_or_expr has at least one with_and_expr"))?;
161    for rest in parts {
162        acc = WithExpr::Or(Box::new(acc), Box::new(parse_with_and_expr(rest)?));
163    }
164    Ok(acc)
165}
166
167fn parse_with_and_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
168    let mut parts = pair.into_inner();
169    let mut acc = parse_with_unary_expr(parts.next().expect("with_and_expr has at least one with_unary_expr"))?;
170    for rest in parts {
171        acc = WithExpr::And(Box::new(acc), Box::new(parse_with_unary_expr(rest)?));
172    }
173    Ok(acc)
174}
175
176fn parse_with_unary_expr(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
177    let inner = pair.into_inner().next().expect("with_unary_expr has one child");
178    match inner.as_rule() {
179        Rule::with_unary_expr => Ok(WithExpr::Not(Box::new(parse_with_unary_expr(inner)?))),
180        Rule::with_comparison => parse_with_comparison(inner),
181        Rule::with_expr => parse_with_expr(inner),
182        r => unreachable!("unexpected with_unary_expr child rule {r:?}"),
183    }
184}
185
186fn parse_with_comparison(pair: Pair<Rule>) -> Result<WithExpr, QueryError> {
187    let mut inner = pair.into_inner();
188    let lhs = parse_return_expr(inner.next().expect("with_comparison has a return_expr"))?;
189    let op_pair = inner.next().expect("with_comparison has a compare_op");
190    let op = parse_compare_op(op_pair);
191    let literal = parse_literal(inner.next().expect("with_comparison has a literal"))?;
192    Ok(WithExpr::Compare(lhs, op, literal))
193}
194
195fn parse_order_by_clause(pair: Pair<Rule>) -> Result<Vec<(ReturnExpr, SortDir)>, QueryError> {
196    pair.into_inner()
197        .filter(|c| c.as_rule() == Rule::sort_item)
198        .map(parse_sort_item)
199        .collect()
200}
201
202fn parse_limit_clause(pair: Pair<Rule>) -> Result<i64, QueryError> {
203    let n_pair = pair.into_inner().next().expect("LIMIT has an int_literal");
204    n_pair
205        .as_str()
206        .parse::<i64>()
207        .map_err(|_| QueryError::Parse("invalid LIMIT value".into()))
208}
209
210/// Merges comma-separated patterns within one `MATCH` into a single linear
211/// `Pattern`. Not a general cross-join — each subsequent pattern's start
212/// variable must be exactly the previous pattern's last-introduced
213/// variable (e.g. IS2's `MATCH (message)-[...]->(post:Post), (post)-[...]->
214/// (person)`, where `post` is both the first pattern's end and the second's
215/// start). Any labels/props the continuing pattern restates on that shared
216/// variable are merged in as additional filters. Non-linear/branching
217/// comma patterns (sharing a variable that isn't this exact splice point)
218/// are rejected rather than silently mishandled.
219fn splice_patterns(mut patterns: Vec<Pattern>) -> Result<Pattern, QueryError> {
220    if patterns.is_empty() {
221        return Err(QueryError::Parse("MATCH requires a pattern".into()));
222    }
223    let mut combined = patterns.remove(0);
224    for next in patterns {
225        let Some(start_var) = next.start.var.clone() else {
226            return Err(QueryError::Parse(
227                "a comma-separated MATCH pattern must start from a named variable".into(),
228            ));
229        };
230        let last_var = combined
231            .hops
232            .last()
233            .map(|(_, n)| n.var.clone())
234            .unwrap_or_else(|| combined.start.var.clone());
235        if last_var.as_deref() != Some(start_var.as_str()) {
236            return Err(QueryError::Parse(format!(
237                "comma-separated MATCH pattern must continue from the previous pattern's last \
238                 variable ('{}'), not '{start_var}' — general cross-joins aren't supported",
239                last_var.unwrap_or_default()
240            )));
241        }
242        let target = match combined.hops.last_mut() {
243            Some((_, node)) => node,
244            None => &mut combined.start,
245        };
246        target.labels.extend(next.start.labels);
247        target.props.extend(next.start.props);
248        combined.hops.extend(next.hops);
249    }
250    Ok(combined)
251}
252
253fn parse_sort_item(pair: Pair<Rule>) -> Result<(ReturnExpr, SortDir), QueryError> {
254    let mut inner = pair.into_inner();
255    let expr = parse_return_expr(inner.next().expect("sort_item has a return_expr"))?;
256    let dir = match inner.next() {
257        Some(d) if d.as_str().eq_ignore_ascii_case("desc") => SortDir::Desc,
258        _ => SortDir::Asc,
259    };
260    Ok((expr, dir))
261}
262
263fn parse_tail_clause(pair: Pair<Rule>) -> Result<Tail, QueryError> {
264    let inner = pair.into_inner().next().expect("tail_clause has one child");
265    match inner.as_rule() {
266        Rule::return_clause => {
267            let items = inner
268                .into_inner()
269                .filter(|p| p.as_rule() == Rule::return_item)
270                .map(parse_return_item)
271                .collect::<Result<Vec<_>, _>>()?;
272            Ok(Tail::Return(items))
273        }
274        Rule::detach_delete_clause => {
275            let vars = inner
276                .into_inner()
277                .filter(|p| p.as_rule() == Rule::identifier)
278                .map(|p| p.as_str().to_string())
279                .collect();
280            Ok(Tail::DetachDelete(vars))
281        }
282        Rule::delete_clause => {
283            let vars = inner
284                .into_inner()
285                .filter(|p| p.as_rule() == Rule::identifier)
286                .map(|p| p.as_str().to_string())
287                .collect();
288            Ok(Tail::Delete(vars))
289        }
290        Rule::set_clause => {
291            let items = inner
292                .into_inner()
293                .filter(|p| p.as_rule() == Rule::set_item)
294                .map(parse_set_item)
295                .collect::<Result<Vec<_>, _>>()?;
296            Ok(Tail::Set(items))
297        }
298        r => unreachable!("unexpected tail_clause child rule {r:?}"),
299    }
300}
301
302fn parse_set_item(pair: Pair<Rule>) -> Result<(PropAccess, Literal), QueryError> {
303    let mut inner = pair.into_inner();
304    let prop_access_pair = inner.next().expect("set_item has a prop_access");
305    let literal_pair = inner.next().expect("set_item has a literal");
306    Ok((parse_prop_access(prop_access_pair), parse_literal(literal_pair)?))
307}
308
309fn parse_return_item(pair: Pair<Rule>) -> Result<ReturnItem, QueryError> {
310    let mut inner = pair.into_inner();
311    let expr_pair = inner.next().expect("return_item has a return_expr");
312    let expr = parse_return_expr(expr_pair)?;
313    let alias = inner.next().map(|p| p.as_str().to_string());
314    Ok(ReturnItem { expr, alias })
315}
316
317fn parse_return_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
318    let inner = pair.into_inner().next().expect("return_expr has one child");
319    match inner.as_rule() {
320        Rule::case_expr => parse_case_expr(inner),
321        Rule::function_call => parse_function_call(inner),
322        Rule::prop_access => Ok(ReturnExpr::Prop(parse_prop_access(inner))),
323        Rule::literal => Ok(ReturnExpr::Lit(parse_literal(inner)?)),
324        Rule::identifier => Ok(ReturnExpr::Var(inner.as_str().to_string())),
325        r => unreachable!("unexpected return_expr child rule {r:?}"),
326    }
327}
328
329fn parse_case_expr(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
330    let mut inner = pair.into_inner();
331    let test = parse_return_expr(inner.next().expect("case_expr has a test expr"))?;
332    let mut whens = Vec::new();
333    let mut else_ = None;
334    for p in inner {
335        match p.as_rule() {
336            Rule::case_when => {
337                let mut when_inner = p.into_inner();
338                let when = parse_return_expr(when_inner.next().expect("case_when has a WHEN expr"))?;
339                let then = parse_return_expr(when_inner.next().expect("case_when has a THEN expr"))?;
340                whens.push((when, then));
341            }
342            // The only other possible child is the trailing ELSE return_expr.
343            _ => else_ = Some(Box::new(parse_return_expr(p)?)),
344        }
345    }
346    Ok(ReturnExpr::Case {
347        test: Some(Box::new(test)),
348        whens,
349        else_,
350    })
351}
352
353fn parse_function_call(pair: Pair<Rule>) -> Result<ReturnExpr, QueryError> {
354    let mut inner = pair.into_inner();
355    let name = inner.next().expect("function_call has a name").as_str().to_string();
356    let call_args = inner.next().expect("function_call has call_args");
357    let is_star = call_args.as_str().trim() == "*";
358    if is_star {
359        if !name.eq_ignore_ascii_case("count") {
360            return Err(QueryError::Parse(format!(
361                "'{name}(*)' isn't valid — '*' is only meaningful for count(*)"
362            )));
363        }
364        return Ok(ReturnExpr::CountStar);
365    }
366    let mut distinct = false;
367    let mut args = Vec::new();
368    for p in call_args.into_inner() {
369        match p.as_rule() {
370            Rule::distinct_kw => distinct = true,
371            _ => args.push(parse_return_expr(p)?),
372        }
373    }
374    if distinct && !is_aggregate_name(&name) {
375        return Err(QueryError::Parse(format!(
376            "'{name}(DISTINCT ...)' isn't valid — DISTINCT is only meaningful inside an aggregate function"
377        )));
378    }
379    Ok(ReturnExpr::Call { name, args, distinct })
380}
381
382fn parse_prop_access(pair: Pair<Rule>) -> PropAccess {
383    let mut inner = pair.into_inner();
384    let var = inner.next().expect("prop_access has a var").as_str().to_string();
385    let prop = inner.next().expect("prop_access has a prop").as_str().to_string();
386    PropAccess { var, prop }
387}
388
389fn parse_pattern(pair: Pair<Rule>) -> Result<Pattern, QueryError> {
390    let mut inner = pair.into_inner();
391    let start = parse_node_pattern(inner.next().expect("pattern has a start node"))?;
392    let mut hops = Vec::new();
393    loop {
394        let Some(rel_pair) = inner.next() else { break };
395        let node_pair = inner
396            .next()
397            .ok_or_else(|| QueryError::Parse("dangling relationship in pattern".into()))?;
398        hops.push((parse_rel_pattern(rel_pair)?, parse_node_pattern(node_pair)?));
399    }
400    Ok(Pattern { start, hops })
401}
402
403fn parse_node_pattern(pair: Pair<Rule>) -> Result<NodePattern, QueryError> {
404    let mut var = None;
405    let mut labels = Vec::new();
406    let mut props = Vec::new();
407    for p in pair.into_inner() {
408        match p.as_rule() {
409            Rule::node_var => var = Some(p.as_str().to_string()),
410            Rule::node_label => {
411                labels.push(p.into_inner().next().expect("node_label has an identifier").as_str().to_string())
412            }
413            Rule::prop_map => props = parse_prop_map(p)?,
414            r => unreachable!("unexpected node_pattern child rule {r:?}"),
415        }
416    }
417    Ok(NodePattern { var, labels, props })
418}
419
420fn parse_rel_pattern(pair: Pair<Rule>) -> Result<RelPattern, QueryError> {
421    let inner = pair.into_inner().next().expect("rel_pattern has one child");
422    let direction = match inner.as_rule() {
423        Rule::rel_right => RelDirection::Right,
424        Rule::rel_left => RelDirection::Left,
425        Rule::rel_either => RelDirection::Either,
426        r => unreachable!("unexpected rel_pattern child rule {r:?}"),
427    };
428    let mut var = None;
429    let mut rel_type = None;
430    let mut props = Vec::new();
431    let mut hop_range = None;
432    for p in inner.into_inner() {
433        match p.as_rule() {
434            Rule::rel_var => var = Some(p.as_str().to_string()),
435            Rule::rel_type => {
436                rel_type = Some(p.into_inner().next().expect("rel_type has an identifier").as_str().to_string())
437            }
438            Rule::rel_range => hop_range = Some(parse_rel_range(p.as_str())?),
439            Rule::prop_map => props = parse_prop_map(p)?,
440            r => unreachable!("unexpected rel_right/rel_left/rel_either child rule {r:?}"),
441        }
442    }
443    Ok(RelPattern {
444        var,
445        rel_type,
446        props,
447        direction,
448        hop_range,
449    })
450}
451
452/// Parses the raw `rel_range` text (`*`, `*N`, `*N..`, `*N..M`, `*..M`)
453/// directly rather than via sub-rules, since the `..` literal produces no
454/// child `Pair` to structurally distinguish "*N" (exact) from "*N.." (N or
455/// more).
456fn parse_rel_range(text: &str) -> Result<(u32, Option<u32>), QueryError> {
457    let rest = &text[1..]; // strip leading '*'
458    if rest.is_empty() {
459        return Ok((0, None));
460    }
461    if let Some(idx) = rest.find("..") {
462        let min_str = &rest[..idx];
463        let max_str = &rest[idx + 2..];
464        let min = if min_str.is_empty() {
465            0
466        } else {
467            min_str
468                .parse()
469                .map_err(|_| QueryError::Parse("invalid variable-length min hop count".into()))?
470        };
471        let max = if max_str.is_empty() {
472            None
473        } else {
474            Some(
475                max_str
476                    .parse()
477                    .map_err(|_| QueryError::Parse("invalid variable-length max hop count".into()))?,
478            )
479        };
480        Ok((min, max))
481    } else {
482        let n: u32 = rest
483            .parse()
484            .map_err(|_| QueryError::Parse("invalid variable-length hop count".into()))?;
485        Ok((n, Some(n)))
486    }
487}
488
489fn parse_prop_map(pair: Pair<Rule>) -> Result<Vec<(String, Literal)>, QueryError> {
490    pair.into_inner()
491        .filter(|p| p.as_rule() == Rule::prop_kv)
492        .map(|p| {
493            let mut inner = p.into_inner();
494            let key = inner.next().expect("prop_kv has a key").as_str().to_string();
495            let value = parse_literal(inner.next().expect("prop_kv has a value"))?;
496            Ok((key, value))
497        })
498        .collect()
499}
500
501fn parse_literal(pair: Pair<Rule>) -> Result<Literal, QueryError> {
502    let inner = pair.into_inner().next().expect("literal has one child");
503    Ok(match inner.as_rule() {
504        Rule::int_literal => Literal::Int(
505            inner
506                .as_str()
507                .parse()
508                .map_err(|_| QueryError::Parse("invalid integer literal".into()))?,
509        ),
510        Rule::float_literal => Literal::Float(
511            inner
512                .as_str()
513                .parse()
514                .map_err(|_| QueryError::Parse("invalid float literal".into()))?,
515        ),
516        Rule::string_literal => {
517            let s = inner.as_str();
518            Literal::String(s[1..s.len() - 1].to_string())
519        }
520        Rule::bool_literal => Literal::Bool(inner.as_str().eq_ignore_ascii_case("true")),
521        Rule::null_literal => Literal::Null,
522        Rule::param => {
523            let name = inner.into_inner().next().expect("param has an identifier").as_str().to_string();
524            Literal::Param(name)
525        }
526        r => unreachable!("unexpected literal child rule {r:?}"),
527    })
528}
529
530fn parse_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
531    // expr = { or_expr }
532    parse_or_expr(pair.into_inner().next().expect("expr has an or_expr"))
533}
534
535fn parse_or_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
536    let mut parts = pair.into_inner();
537    let mut acc = parse_and_expr(parts.next().expect("or_expr has at least one and_expr"))?;
538    for rest in parts {
539        acc = Expr::Or(Box::new(acc), Box::new(parse_and_expr(rest)?));
540    }
541    Ok(acc)
542}
543
544fn parse_and_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
545    let mut parts = pair.into_inner();
546    let mut acc = parse_unary_expr(parts.next().expect("and_expr has at least one unary_expr"))?;
547    for rest in parts {
548        acc = Expr::And(Box::new(acc), Box::new(parse_unary_expr(rest)?));
549    }
550    Ok(acc)
551}
552
553fn parse_unary_expr(pair: Pair<Rule>) -> Result<Expr, QueryError> {
554    let inner = pair.into_inner().next().expect("unary_expr has one child");
555    match inner.as_rule() {
556        Rule::unary_expr => Ok(Expr::Not(Box::new(parse_unary_expr(inner)?))),
557        Rule::comparison => parse_comparison(inner),
558        Rule::expr => parse_expr(inner),
559        r => unreachable!("unexpected unary_expr child rule {r:?}"),
560    }
561}
562
563fn parse_comparison(pair: Pair<Rule>) -> Result<Expr, QueryError> {
564    let mut inner = pair.into_inner();
565    let prop_access = parse_prop_access(inner.next().expect("comparison has a prop_access"));
566    let op = parse_compare_op(inner.next().expect("comparison has a compare_op"));
567    let literal = parse_literal(inner.next().expect("comparison has a literal"))?;
568    Ok(Expr::Compare(prop_access, op, literal))
569}
570
571fn parse_compare_op(pair: Pair<Rule>) -> CompareOp {
572    match pair.as_str() {
573        "=" => CompareOp::Eq,
574        "<>" => CompareOp::Ne,
575        "<" => CompareOp::Lt,
576        "<=" => CompareOp::Le,
577        ">" => CompareOp::Gt,
578        ">=" => CompareOp::Ge,
579        other => unreachable!("unexpected compare_op {other:?}"),
580    }
581}