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