Skip to main content

omnigraph_compiler/query/
parser.rs

1use pest::Parser;
2use pest::error::InputLocation;
3use pest_derive::Parser;
4
5use crate::error::{
6    NanoError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
7};
8
9use super::ast::*;
10
11#[derive(Parser)]
12#[grammar = "query/query.pest"]
13struct QueryParser;
14
15pub fn parse_query(input: &str) -> Result<QueryFile> {
16    parse_query_diagnostic(input).map_err(|e| NanoError::Parse(e.to_string()))
17}
18
19pub fn parse_query_diagnostic(input: &str) -> std::result::Result<QueryFile, ParseDiagnostic> {
20    let pairs = QueryParser::parse(Rule::query_file, input).map_err(pest_error_to_diagnostic)?;
21
22    let mut queries = Vec::new();
23    for pair in pairs {
24        if let Rule::query_file = pair.as_rule() {
25            for inner in pair.into_inner() {
26                if let Rule::query_decl = inner.as_rule() {
27                    queries.push(parse_query_decl(inner).map_err(nano_error_to_diagnostic)?);
28                }
29            }
30        }
31    }
32    Ok(QueryFile { queries })
33}
34
35fn pest_error_to_diagnostic(err: pest::error::Error<Rule>) -> ParseDiagnostic {
36    let span = match err.location {
37        InputLocation::Pos(pos) => Some(render_span(SourceSpan::new(pos, pos))),
38        InputLocation::Span((start, end)) => Some(render_span(SourceSpan::new(start, end))),
39    };
40    ParseDiagnostic::new(err.to_string(), span)
41}
42
43fn nano_error_to_diagnostic(err: NanoError) -> ParseDiagnostic {
44    ParseDiagnostic::new(err.to_string(), None)
45}
46
47fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
48    let mut inner = pair.into_inner();
49    let name = inner.next().unwrap().as_str().to_string();
50
51    let mut description = None;
52    let mut instruction = None;
53    let mut params = Vec::new();
54    let mut match_clause = Vec::new();
55    let mut return_clause = Vec::new();
56    let mut order_clause = Vec::new();
57    let mut limit = None;
58    let mut mutations = Vec::new();
59
60    for item in inner {
61        match item.as_rule() {
62            Rule::param_list => {
63                for p in item.into_inner() {
64                    if let Rule::param = p.as_rule() {
65                        params.push(parse_param(p)?);
66                    }
67                }
68            }
69            Rule::query_annotation => {
70                let (annotation_name, value) = parse_query_annotation(item)?;
71                match annotation_name {
72                    "description" => {
73                        if description.replace(value).is_some() {
74                            return Err(NanoError::Parse(format!(
75                                "query `{}` cannot include duplicate @description annotations",
76                                name
77                            )));
78                        }
79                    }
80                    "instruction" => {
81                        if instruction.replace(value).is_some() {
82                            return Err(NanoError::Parse(format!(
83                                "query `{}` cannot include duplicate @instruction annotations",
84                                name
85                            )));
86                        }
87                    }
88                    other => {
89                        return Err(NanoError::Parse(format!(
90                            "unsupported query annotation: @{}",
91                            other
92                        )));
93                    }
94                }
95            }
96            Rule::query_body => {
97                let body = item
98                    .into_inner()
99                    .next()
100                    .ok_or_else(|| NanoError::Parse("query body cannot be empty".to_string()))?;
101                match body.as_rule() {
102                    Rule::read_query_body => {
103                        for section in body.into_inner() {
104                            match section.as_rule() {
105                                Rule::match_clause => {
106                                    for c in section.into_inner() {
107                                        if let Rule::clause = c.as_rule() {
108                                            match_clause.push(parse_clause(c)?);
109                                        }
110                                    }
111                                }
112                                Rule::return_clause => {
113                                    for proj in section.into_inner() {
114                                        if let Rule::projection = proj.as_rule() {
115                                            return_clause.push(parse_projection(proj)?);
116                                        }
117                                    }
118                                }
119                                Rule::order_clause => {
120                                    for ord in section.into_inner() {
121                                        if let Rule::ordering = ord.as_rule() {
122                                            order_clause.push(parse_ordering(ord)?);
123                                        }
124                                    }
125                                }
126                                Rule::limit_clause => {
127                                    let int_pair = section.into_inner().next().unwrap();
128                                    limit =
129                                        Some(int_pair.as_str().parse::<u64>().map_err(|e| {
130                                            NanoError::Parse(format!("invalid limit: {}", e))
131                                        })?);
132                                }
133                                _ => {}
134                            }
135                        }
136                    }
137                    Rule::mutation_body => {
138                        for mutation_pair in body.into_inner() {
139                            if let Rule::mutation_stmt = mutation_pair.as_rule() {
140                                let stmt =
141                                    mutation_pair.into_inner().next().ok_or_else(|| {
142                                        NanoError::Parse(
143                                            "mutation statement cannot be empty".to_string(),
144                                        )
145                                    })?;
146                                mutations.push(parse_mutation_stmt(stmt)?);
147                            }
148                        }
149                    }
150                    _ => {}
151                }
152            }
153            _ => {}
154        }
155    }
156
157    Ok(QueryDecl {
158        name,
159        description,
160        instruction,
161        params,
162        match_clause,
163        return_clause,
164        order_clause,
165        limit,
166        mutations,
167    })
168}
169
170fn parse_query_annotation(pair: pest::iterators::Pair<Rule>) -> Result<(&'static str, String)> {
171    let inner = pair
172        .into_inner()
173        .next()
174        .ok_or_else(|| NanoError::Parse("query annotation cannot be empty".to_string()))?;
175    match inner.as_rule() {
176        Rule::description_annotation => {
177            let value = inner
178                .into_inner()
179                .next()
180                .ok_or_else(|| {
181                    NanoError::Parse("@description requires a string literal".to_string())
182                })
183                .map(|value| parse_string_lit(value.as_str()))??;
184            Ok(("description", value))
185        }
186        Rule::instruction_annotation => {
187            let value = inner
188                .into_inner()
189                .next()
190                .ok_or_else(|| {
191                    NanoError::Parse("@instruction requires a string literal".to_string())
192                })
193                .map(|value| parse_string_lit(value.as_str()))??;
194            Ok(("instruction", value))
195        }
196        other => Err(NanoError::Parse(format!(
197            "unexpected query annotation rule: {:?}",
198            other
199        ))),
200    }
201}
202
203fn parse_param(pair: pest::iterators::Pair<Rule>) -> Result<Param> {
204    let mut inner = pair.into_inner();
205    let var = inner.next().unwrap().as_str();
206    let name = var.strip_prefix('$').unwrap_or(var).to_string();
207    let type_ref = inner.next().unwrap();
208    let nullable = type_ref.as_str().trim_end().ends_with('?');
209    let mut type_inner = type_ref.into_inner();
210    let core = type_inner
211        .next()
212        .ok_or_else(|| NanoError::Parse("parameter type is missing".to_string()))?;
213    let base = match core.as_rule() {
214        Rule::base_type => core.as_str().to_string(),
215        Rule::list_type => {
216            let inner = core
217                .into_inner()
218                .next()
219                .ok_or_else(|| NanoError::Parse("list type missing item type".to_string()))?;
220            format!("[{}]", inner.as_str().trim())
221        }
222        Rule::vector_type => {
223            let vector = core
224                .into_inner()
225                .next()
226                .ok_or_else(|| NanoError::Parse("Vector type missing dimension".to_string()))?;
227            format!("Vector({})", vector.as_str().trim())
228        }
229        other => {
230            return Err(NanoError::Parse(format!(
231                "unexpected param type rule: {:?}",
232                other
233            )));
234        }
235    };
236
237    Ok(Param {
238        name,
239        type_name: base,
240        nullable,
241    })
242}
243
244fn parse_clause(pair: pest::iterators::Pair<Rule>) -> Result<Clause> {
245    let inner = pair.into_inner().next().unwrap();
246    match inner.as_rule() {
247        Rule::binding => Ok(Clause::Binding(parse_binding(inner)?)),
248        Rule::traversal => Ok(Clause::Traversal(parse_traversal(inner)?)),
249        Rule::filter => Ok(Clause::Filter(parse_filter(inner)?)),
250        Rule::text_search_clause => Ok(parse_text_search_clause(inner)?),
251        Rule::negation => {
252            let mut clauses = Vec::new();
253            for c in inner.into_inner() {
254                if let Rule::clause = c.as_rule() {
255                    clauses.push(parse_clause(c)?);
256                }
257            }
258            Ok(Clause::Negation(clauses))
259        }
260        _ => Err(NanoError::Parse(format!(
261            "unexpected clause rule: {:?}",
262            inner.as_rule()
263        ))),
264    }
265}
266
267fn parse_text_search_clause(pair: pest::iterators::Pair<Rule>) -> Result<Clause> {
268    let inner = pair
269        .into_inner()
270        .next()
271        .ok_or_else(|| NanoError::Parse("text search clause cannot be empty".to_string()))?;
272    let expr = match inner.as_rule() {
273        Rule::search_call => parse_search_call(inner)?,
274        Rule::fuzzy_call => parse_fuzzy_call(inner)?,
275        Rule::match_text_call => parse_match_text_call(inner)?,
276        other => {
277            return Err(NanoError::Parse(format!(
278                "unexpected text search clause rule: {:?}",
279                other
280            )));
281        }
282    };
283
284    Ok(Clause::Filter(Filter {
285        left: expr,
286        op: CompOp::Eq,
287        right: Expr::Literal(Literal::Bool(true)),
288    }))
289}
290
291fn parse_binding(pair: pest::iterators::Pair<Rule>) -> Result<Binding> {
292    let mut inner = pair.into_inner();
293    let var = inner.next().unwrap().as_str();
294    let variable = var.strip_prefix('$').unwrap_or(var).to_string();
295    let type_name = inner.next().unwrap().as_str().to_string();
296
297    let mut prop_matches = Vec::new();
298    for item in inner {
299        if let Rule::prop_match_list = item.as_rule() {
300            for pm in item.into_inner() {
301                if let Rule::prop_match = pm.as_rule() {
302                    prop_matches.push(parse_prop_match(pm)?);
303                }
304            }
305        }
306    }
307
308    Ok(Binding {
309        variable,
310        type_name,
311        prop_matches,
312    })
313}
314
315fn parse_prop_match(pair: pest::iterators::Pair<Rule>) -> Result<PropMatch> {
316    let mut inner = pair.into_inner();
317    let prop_name = inner.next().unwrap().as_str().to_string();
318    let value_pair = inner.next().unwrap();
319    let value = parse_match_value(value_pair)?;
320
321    Ok(PropMatch { prop_name, value })
322}
323
324fn parse_mutation_stmt(pair: pest::iterators::Pair<Rule>) -> Result<Mutation> {
325    match pair.as_rule() {
326        Rule::insert_stmt => parse_insert_mutation(pair).map(Mutation::Insert),
327        Rule::update_stmt => parse_update_mutation(pair).map(Mutation::Update),
328        Rule::delete_stmt => parse_delete_mutation(pair).map(Mutation::Delete),
329        other => Err(NanoError::Parse(format!(
330            "unexpected mutation statement rule: {:?}",
331            other
332        ))),
333    }
334}
335
336fn parse_insert_mutation(pair: pest::iterators::Pair<Rule>) -> Result<InsertMutation> {
337    let mut inner = pair.into_inner();
338    let type_name = inner.next().unwrap().as_str().to_string();
339    let mut assignments = Vec::new();
340    for item in inner {
341        if let Rule::mutation_assignment = item.as_rule() {
342            assignments.push(parse_mutation_assignment(item)?);
343        }
344    }
345    Ok(InsertMutation {
346        type_name,
347        assignments,
348    })
349}
350
351fn parse_update_mutation(pair: pest::iterators::Pair<Rule>) -> Result<UpdateMutation> {
352    let mut inner = pair.into_inner();
353    let type_name = inner.next().unwrap().as_str().to_string();
354
355    let mut assignments = Vec::new();
356    let mut predicate = None;
357
358    for item in inner {
359        match item.as_rule() {
360            Rule::mutation_assignment => assignments.push(parse_mutation_assignment(item)?),
361            Rule::mutation_predicate => predicate = Some(parse_mutation_predicate(item)?),
362            _ => {}
363        }
364    }
365
366    let predicate = predicate.ok_or_else(|| {
367        NanoError::Parse("update mutation requires a where predicate".to_string())
368    })?;
369
370    Ok(UpdateMutation {
371        type_name,
372        assignments,
373        predicate,
374    })
375}
376
377fn parse_delete_mutation(pair: pest::iterators::Pair<Rule>) -> Result<DeleteMutation> {
378    let mut inner = pair.into_inner();
379    let type_name = inner.next().unwrap().as_str().to_string();
380    let predicate = inner
381        .next()
382        .ok_or_else(|| NanoError::Parse("delete mutation requires a where predicate".to_string()))
383        .and_then(parse_mutation_predicate)?;
384    Ok(DeleteMutation {
385        type_name,
386        predicate,
387    })
388}
389
390fn parse_mutation_assignment(pair: pest::iterators::Pair<Rule>) -> Result<MutationAssignment> {
391    let mut inner = pair.into_inner();
392    let property = inner.next().unwrap().as_str().to_string();
393    let value = parse_match_value(inner.next().unwrap())?;
394    Ok(MutationAssignment { property, value })
395}
396
397fn parse_mutation_predicate(pair: pest::iterators::Pair<Rule>) -> Result<MutationPredicate> {
398    let mut inner = pair.into_inner();
399    let property = inner.next().unwrap().as_str().to_string();
400    let op = parse_comp_op(inner.next().unwrap())?;
401    let value = parse_match_value(inner.next().unwrap())?;
402    Ok(MutationPredicate {
403        property,
404        op,
405        value,
406    })
407}
408
409fn parse_match_value(pair: pest::iterators::Pair<Rule>) -> Result<MatchValue> {
410    let value_inner = pair.into_inner().next().unwrap();
411    match value_inner.as_rule() {
412        Rule::variable => {
413            let v = value_inner.as_str();
414            Ok(MatchValue::Variable(
415                v.strip_prefix('$').unwrap_or(v).to_string(),
416            ))
417        }
418        Rule::now_call => Ok(MatchValue::Now),
419        Rule::literal => Ok(MatchValue::Literal(parse_literal(value_inner)?)),
420        _ => Err(NanoError::Parse(format!(
421            "unexpected match value: {:?}",
422            value_inner.as_rule()
423        ))),
424    }
425}
426
427fn parse_traversal(pair: pest::iterators::Pair<Rule>) -> Result<Traversal> {
428    let mut inner = pair.into_inner();
429    let src_var = inner.next().unwrap().as_str();
430    let src = src_var.strip_prefix('$').unwrap_or(src_var).to_string();
431    let edge_name = inner.next().unwrap().as_str().to_string();
432    let mut min_hops = 1u32;
433    let mut max_hops = Some(1u32);
434
435    let next = inner.next().unwrap();
436    let dst_pair = if let Rule::traversal_bounds = next.as_rule() {
437        let (min, max) = parse_traversal_bounds(next)?;
438        min_hops = min;
439        max_hops = max;
440        inner
441            .next()
442            .ok_or_else(|| NanoError::Parse("traversal missing destination variable".to_string()))?
443    } else {
444        next
445    };
446
447    let dst_var = dst_pair.as_str();
448    let dst = dst_var.strip_prefix('$').unwrap_or(dst_var).to_string();
449
450    Ok(Traversal {
451        src,
452        edge_name,
453        dst,
454        min_hops,
455        max_hops,
456    })
457}
458
459fn parse_traversal_bounds(pair: pest::iterators::Pair<Rule>) -> Result<(u32, Option<u32>)> {
460    let mut inner = pair.into_inner();
461    let min = inner
462        .next()
463        .ok_or_else(|| NanoError::Parse("traversal bound missing min hop".to_string()))?
464        .as_str()
465        .parse::<u32>()
466        .map_err(|e| NanoError::Parse(format!("invalid traversal min bound: {}", e)))?;
467    let max = inner
468        .next()
469        .map(|p| {
470            p.as_str()
471                .parse::<u32>()
472                .map_err(|e| NanoError::Parse(format!("invalid traversal max bound: {}", e)))
473        })
474        .transpose()?;
475    Ok((min, max))
476}
477
478fn parse_filter(pair: pest::iterators::Pair<Rule>) -> Result<Filter> {
479    let mut inner = pair.into_inner();
480    let left = parse_expr(inner.next().unwrap())?;
481    let op = parse_filter_op(inner.next().unwrap())?;
482    let right = parse_expr(inner.next().unwrap())?;
483
484    Ok(Filter { left, op, right })
485}
486
487fn parse_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
488    let inner = pair.into_inner().next().unwrap();
489    match inner.as_rule() {
490        Rule::now_call => Ok(Expr::Now),
491        Rule::prop_access => {
492            let mut parts = inner.into_inner();
493            let var = parts.next().unwrap().as_str();
494            let variable = var.strip_prefix('$').unwrap_or(var).to_string();
495            let property = parts.next().unwrap().as_str().to_string();
496            Ok(Expr::PropAccess { variable, property })
497        }
498        Rule::variable => {
499            let v = inner.as_str();
500            Ok(Expr::Variable(v.strip_prefix('$').unwrap_or(v).to_string()))
501        }
502        Rule::literal => Ok(Expr::Literal(parse_literal(inner)?)),
503        Rule::agg_call => {
504            let mut parts = inner.into_inner();
505            let func = match parts.next().unwrap().as_str() {
506                "count" => AggFunc::Count,
507                "sum" => AggFunc::Sum,
508                "avg" => AggFunc::Avg,
509                "min" => AggFunc::Min,
510                "max" => AggFunc::Max,
511                other => return Err(NanoError::Parse(format!("unknown aggregate: {}", other))),
512            };
513            let arg = parse_expr(parts.next().unwrap())?;
514            Ok(Expr::Aggregate {
515                func,
516                arg: Box::new(arg),
517            })
518        }
519        Rule::search_call => parse_search_call(inner),
520        Rule::fuzzy_call => parse_fuzzy_call(inner),
521        Rule::match_text_call => parse_match_text_call(inner),
522        Rule::nearest_ordering => parse_nearest_ordering(inner),
523        Rule::bm25_call => parse_bm25_call(inner),
524        Rule::rrf_call => parse_rrf_call(inner),
525        Rule::ident => Ok(Expr::AliasRef(inner.as_str().to_string())),
526        _ => Err(NanoError::Parse(format!(
527            "unexpected expr rule: {:?}",
528            inner.as_rule()
529        ))),
530    }
531}
532
533fn parse_search_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
534    let mut args = pair.into_inner();
535    let field = args
536        .next()
537        .ok_or_else(|| NanoError::Parse("search() missing field argument".to_string()))?;
538    let query = args
539        .next()
540        .ok_or_else(|| NanoError::Parse("search() missing query argument".to_string()))?;
541    if args.next().is_some() {
542        return Err(NanoError::Parse(
543            "search() accepts exactly 2 arguments".to_string(),
544        ));
545    }
546    Ok(Expr::Search {
547        field: Box::new(parse_expr(field)?),
548        query: Box::new(parse_expr(query)?),
549    })
550}
551
552fn parse_fuzzy_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
553    let mut args = pair.into_inner();
554    let field = args
555        .next()
556        .ok_or_else(|| NanoError::Parse("fuzzy() missing field argument".to_string()))?;
557    let query = args
558        .next()
559        .ok_or_else(|| NanoError::Parse("fuzzy() missing query argument".to_string()))?;
560    let max_edits = args.next().map(parse_expr).transpose()?.map(Box::new);
561    if args.next().is_some() {
562        return Err(NanoError::Parse(
563            "fuzzy() accepts at most 3 arguments".to_string(),
564        ));
565    }
566    Ok(Expr::Fuzzy {
567        field: Box::new(parse_expr(field)?),
568        query: Box::new(parse_expr(query)?),
569        max_edits,
570    })
571}
572
573fn parse_match_text_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
574    let mut args = pair.into_inner();
575    let field = args
576        .next()
577        .ok_or_else(|| NanoError::Parse("match_text() missing field argument".to_string()))?;
578    let query = args
579        .next()
580        .ok_or_else(|| NanoError::Parse("match_text() missing query argument".to_string()))?;
581    if args.next().is_some() {
582        return Err(NanoError::Parse(
583            "match_text() accepts exactly 2 arguments".to_string(),
584        ));
585    }
586    Ok(Expr::MatchText {
587        field: Box::new(parse_expr(field)?),
588        query: Box::new(parse_expr(query)?),
589    })
590}
591
592fn parse_bm25_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
593    let mut args = pair.into_inner();
594    let field = args
595        .next()
596        .ok_or_else(|| NanoError::Parse("bm25() missing field argument".to_string()))?;
597    let query = args
598        .next()
599        .ok_or_else(|| NanoError::Parse("bm25() missing query argument".to_string()))?;
600    if args.next().is_some() {
601        return Err(NanoError::Parse(
602            "bm25() accepts exactly 2 arguments".to_string(),
603        ));
604    }
605    Ok(Expr::Bm25 {
606        field: Box::new(parse_expr(field)?),
607        query: Box::new(parse_expr(query)?),
608    })
609}
610
611fn parse_rank_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
612    let inner = if pair.as_rule() == Rule::rank_expr {
613        pair.into_inner()
614            .next()
615            .ok_or_else(|| NanoError::Parse("rank expression cannot be empty".to_string()))?
616    } else {
617        pair
618    };
619    match inner.as_rule() {
620        Rule::nearest_ordering => parse_nearest_ordering(inner),
621        Rule::bm25_call => parse_bm25_call(inner),
622        other => Err(NanoError::Parse(format!(
623            "rrf() rank expression must be nearest(...) or bm25(...), got {:?}",
624            other
625        ))),
626    }
627}
628
629fn parse_rrf_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
630    let mut args = pair.into_inner();
631    let primary = args
632        .next()
633        .ok_or_else(|| NanoError::Parse("rrf() missing primary rank expression".to_string()))?;
634    let secondary = args
635        .next()
636        .ok_or_else(|| NanoError::Parse("rrf() missing secondary rank expression".to_string()))?;
637    let k = args.next().map(parse_expr).transpose()?.map(Box::new);
638    if args.next().is_some() {
639        return Err(NanoError::Parse(
640            "rrf() accepts at most 3 arguments".to_string(),
641        ));
642    }
643    Ok(Expr::Rrf {
644        primary: Box::new(parse_rank_expr(primary)?),
645        secondary: Box::new(parse_rank_expr(secondary)?),
646        k,
647    })
648}
649
650fn parse_comp_op(pair: pest::iterators::Pair<Rule>) -> Result<CompOp> {
651    match pair.as_str() {
652        "=" => Ok(CompOp::Eq),
653        "!=" => Ok(CompOp::Ne),
654        ">" => Ok(CompOp::Gt),
655        "<" => Ok(CompOp::Lt),
656        ">=" => Ok(CompOp::Ge),
657        "<=" => Ok(CompOp::Le),
658        other => Err(NanoError::Parse(format!("unknown operator: {}", other))),
659    }
660}
661
662fn parse_filter_op(pair: pest::iterators::Pair<Rule>) -> Result<CompOp> {
663    match pair.as_str() {
664        "contains" => Ok(CompOp::Contains),
665        _ => parse_comp_op(pair),
666    }
667}
668
669fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
670    let inner = pair.into_inner().next().unwrap();
671    match inner.as_rule() {
672        Rule::string_lit => Ok(Literal::String(parse_string_lit(inner.as_str())?)),
673        Rule::integer => {
674            let n: i64 = inner
675                .as_str()
676                .parse()
677                .map_err(|e| NanoError::Parse(format!("invalid integer: {}", e)))?;
678            Ok(Literal::Integer(n))
679        }
680        Rule::float_lit => {
681            let f: f64 = inner
682                .as_str()
683                .parse()
684                .map_err(|e| NanoError::Parse(format!("invalid float: {}", e)))?;
685            Ok(Literal::Float(f))
686        }
687        Rule::bool_lit => {
688            let b = match inner.as_str() {
689                "true" => true,
690                "false" => false,
691                other => {
692                    return Err(NanoError::Parse(format!(
693                        "invalid boolean literal: {}",
694                        other
695                    )));
696                }
697            };
698            Ok(Literal::Bool(b))
699        }
700        Rule::date_lit => {
701            let date_str = inner
702                .into_inner()
703                .next()
704                .map(|s| parse_string_lit(s.as_str()))
705                .ok_or_else(|| NanoError::Parse("date literal requires a string".to_string()))?;
706            Ok(Literal::Date(date_str?))
707        }
708        Rule::datetime_lit => {
709            let dt_str = inner
710                .into_inner()
711                .next()
712                .map(|s| parse_string_lit(s.as_str()))
713                .ok_or_else(|| {
714                    NanoError::Parse("datetime literal requires a string".to_string())
715                })?;
716            Ok(Literal::DateTime(dt_str?))
717        }
718        Rule::list_lit => {
719            let mut items = Vec::new();
720            for item in inner.into_inner() {
721                if item.as_rule() == Rule::literal {
722                    items.push(parse_literal(item)?);
723                }
724            }
725            Ok(Literal::List(items))
726        }
727        _ => Err(NanoError::Parse(format!(
728            "unexpected literal: {:?}",
729            inner.as_rule()
730        ))),
731    }
732}
733
734fn parse_string_lit(raw: &str) -> Result<String> {
735    decode_string_literal(raw)
736}
737
738fn parse_projection(pair: pest::iterators::Pair<Rule>) -> Result<Projection> {
739    let mut inner = pair.into_inner();
740    let expr = parse_expr(inner.next().unwrap())?;
741    let alias = inner.next().map(|p| p.as_str().to_string());
742
743    Ok(Projection { expr, alias })
744}
745
746fn parse_ordering(pair: pest::iterators::Pair<Rule>) -> Result<Ordering> {
747    let mut inner = pair.into_inner();
748    let first = inner
749        .next()
750        .ok_or_else(|| NanoError::Parse("ordering cannot be empty".to_string()))?;
751    let (expr, descending) = match first.as_rule() {
752        Rule::nearest_ordering => (parse_nearest_ordering(first)?, false),
753        Rule::expr => {
754            let expr = parse_expr(first)?;
755            let direction = inner.next().map(|p| p.as_str().to_string());
756            if matches!(expr, Expr::Nearest { .. }) && direction.is_some() {
757                return Err(NanoError::Parse(
758                    "nearest() ordering does not accept asc/desc modifiers".to_string(),
759                ));
760            }
761            let descending = matches!(direction.as_deref(), Some("desc"));
762            (expr, descending)
763        }
764        other => {
765            return Err(NanoError::Parse(format!(
766                "unexpected ordering rule: {:?}",
767                other
768            )));
769        }
770    };
771
772    Ok(Ordering { expr, descending })
773}
774
775fn parse_nearest_ordering(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
776    let mut inner = pair.into_inner();
777    let prop = inner
778        .next()
779        .ok_or_else(|| NanoError::Parse("nearest() missing property".to_string()))?;
780    let mut prop_parts = prop.into_inner();
781    let var = prop_parts
782        .next()
783        .ok_or_else(|| NanoError::Parse("nearest() missing variable".to_string()))?
784        .as_str();
785    let variable = var.strip_prefix('$').unwrap_or(var).to_string();
786    let property = prop_parts
787        .next()
788        .ok_or_else(|| NanoError::Parse("nearest() missing property name".to_string()))?
789        .as_str()
790        .to_string();
791
792    let query = inner
793        .next()
794        .ok_or_else(|| NanoError::Parse("nearest() missing query expression".to_string()))?;
795    Ok(Expr::Nearest {
796        variable,
797        property,
798        query: Box::new(parse_expr(query)?),
799    })
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805
806    #[test]
807    fn test_parse_basic_query() {
808        let input = r#"
809query get_person($name: String) {
810    match {
811        $p: Person { name: $name }
812    }
813    return { $p.name, $p.age }
814}
815"#;
816        let qf = parse_query(input).unwrap();
817        assert_eq!(qf.queries.len(), 1);
818        let q = &qf.queries[0];
819        assert_eq!(q.name, "get_person");
820        assert_eq!(q.params.len(), 1);
821        assert_eq!(q.params[0].name, "name");
822        assert_eq!(q.match_clause.len(), 1);
823        assert_eq!(q.return_clause.len(), 2);
824    }
825
826    #[test]
827    fn test_parse_query_metadata_annotations() {
828        let input = r#"
829query semantic_search($q: String)
830    @description("Find semantically similar documents.")
831    @instruction("Use for conceptual search; prefer keyword_search for exact terms.")
832{
833    match {
834        $d: Doc
835    }
836    return { $d.slug }
837}
838"#;
839        let qf = parse_query(input).unwrap();
840        let q = &qf.queries[0];
841        assert_eq!(
842            q.description.as_deref(),
843            Some("Find semantically similar documents.")
844        );
845        assert_eq!(
846            q.instruction.as_deref(),
847            Some("Use for conceptual search; prefer keyword_search for exact terms.")
848        );
849    }
850
851    #[test]
852    fn test_duplicate_query_description_is_rejected() {
853        let input = r#"
854query q()
855    @description("one")
856    @description("two")
857{
858    match {
859        $p: Person
860    }
861    return { $p.name }
862}
863"#;
864        let err = parse_query(input).unwrap_err();
865        assert!(err.to_string().contains("duplicate @description"));
866    }
867
868    #[test]
869    fn test_parse_no_params() {
870        let input = r#"
871query adults() {
872    match {
873        $p: Person
874        $p.age > 30
875    }
876    return { $p.name, $p.age }
877    order { $p.age desc }
878}
879"#;
880        let qf = parse_query(input).unwrap();
881        let q = &qf.queries[0];
882        assert_eq!(q.name, "adults");
883        assert!(q.params.is_empty());
884        assert_eq!(q.match_clause.len(), 2);
885        assert_eq!(q.order_clause.len(), 1);
886        assert!(q.order_clause[0].descending);
887    }
888
889    #[test]
890    fn test_parse_traversal() {
891        let input = r#"
892query friends_of($name: String) {
893    match {
894        $p: Person { name: $name }
895        $p knows $f
896    }
897    return { $f.name, $f.age }
898}
899"#;
900        let qf = parse_query(input).unwrap();
901        let q = &qf.queries[0];
902        assert_eq!(q.match_clause.len(), 2);
903        match &q.match_clause[1] {
904            Clause::Traversal(t) => {
905                assert_eq!(t.src, "p");
906                assert_eq!(t.edge_name, "knows");
907                assert_eq!(t.dst, "f");
908                assert_eq!(t.min_hops, 1);
909                assert_eq!(t.max_hops, Some(1));
910            }
911            _ => panic!("expected Traversal"),
912        }
913    }
914
915    #[test]
916    fn test_parse_negation() {
917        let input = r#"
918query unemployed() {
919    match {
920        $p: Person
921        not { $p worksAt $_ }
922    }
923    return { $p.name }
924}
925"#;
926        let qf = parse_query(input).unwrap();
927        let q = &qf.queries[0];
928        assert_eq!(q.match_clause.len(), 2);
929        match &q.match_clause[1] {
930            Clause::Negation(clauses) => {
931                assert_eq!(clauses.len(), 1);
932                match &clauses[0] {
933                    Clause::Traversal(t) => {
934                        assert_eq!(t.src, "p");
935                        assert_eq!(t.edge_name, "worksAt");
936                        assert_eq!(t.dst, "_");
937                        assert_eq!(t.min_hops, 1);
938                        assert_eq!(t.max_hops, Some(1));
939                    }
940                    _ => panic!("expected Traversal inside negation"),
941                }
942            }
943            _ => panic!("expected Negation"),
944        }
945    }
946
947    #[test]
948    fn test_parse_aggregation() {
949        let input = r#"
950query friend_counts() {
951    match {
952        $p: Person
953        $p knows $f
954    }
955    return {
956        $p.name
957        count($f) as friends
958    }
959    order { friends desc }
960    limit 20
961}
962"#;
963        let qf = parse_query(input).unwrap();
964        let q = &qf.queries[0];
965        assert_eq!(q.return_clause.len(), 2);
966        match &q.return_clause[1].expr {
967            Expr::Aggregate { func, .. } => {
968                assert_eq!(*func, AggFunc::Count);
969            }
970            _ => panic!("expected Aggregate"),
971        }
972        assert_eq!(q.return_clause[1].alias.as_deref(), Some("friends"));
973        assert_eq!(q.limit, Some(20));
974    }
975
976    #[test]
977    fn test_parse_two_hop() {
978        let input = r#"
979query friends_of_friends($name: String) {
980    match {
981        $p: Person { name: $name }
982        $p knows $mid
983        $mid knows $fof
984    }
985    return { $fof.name }
986}
987"#;
988        let qf = parse_query(input).unwrap();
989        let q = &qf.queries[0];
990        assert_eq!(q.match_clause.len(), 3);
991    }
992
993    #[test]
994    fn test_parse_reverse_traversal() {
995        let input = r#"
996query employees_of($company: String) {
997    match {
998        $c: Company { name: $company }
999        $p worksAt $c
1000    }
1001    return { $p.name }
1002}
1003"#;
1004        let qf = parse_query(input).unwrap();
1005        let q = &qf.queries[0];
1006        assert_eq!(q.match_clause.len(), 2);
1007        match &q.match_clause[1] {
1008            Clause::Traversal(t) => {
1009                assert_eq!(t.src, "p");
1010                assert_eq!(t.edge_name, "worksAt");
1011                assert_eq!(t.dst, "c");
1012                assert_eq!(t.min_hops, 1);
1013                assert_eq!(t.max_hops, Some(1));
1014            }
1015            _ => panic!("expected Traversal"),
1016        }
1017    }
1018
1019    #[test]
1020    fn test_parse_bounded_traversal() {
1021        let input = r#"
1022query q() {
1023    match {
1024        $a: Person
1025        $a knows{1,3} $b
1026    }
1027    return { $b.name }
1028}
1029"#;
1030        let qf = parse_query(input).unwrap();
1031        let q = &qf.queries[0];
1032        match &q.match_clause[1] {
1033            Clause::Traversal(t) => {
1034                assert_eq!(t.min_hops, 1);
1035                assert_eq!(t.max_hops, Some(3));
1036            }
1037            _ => panic!("expected Traversal"),
1038        }
1039    }
1040
1041    #[test]
1042    fn test_parse_unbounded_traversal() {
1043        let input = r#"
1044query q() {
1045    match {
1046        $a: Person
1047        $a knows{1,} $b
1048    }
1049    return { $b.name }
1050}
1051"#;
1052        let qf = parse_query(input).unwrap();
1053        let q = &qf.queries[0];
1054        match &q.match_clause[1] {
1055            Clause::Traversal(t) => {
1056                assert_eq!(t.min_hops, 1);
1057                assert_eq!(t.max_hops, None);
1058            }
1059            _ => panic!("expected Traversal"),
1060        }
1061    }
1062
1063    #[test]
1064    fn test_parse_multi_query_file() {
1065        let input = r#"
1066query q1() {
1067    match { $p: Person }
1068    return { $p.name }
1069}
1070query q2() {
1071    match { $c: Company }
1072    return { $c.name }
1073}
1074"#;
1075        let qf = parse_query(input).unwrap();
1076        assert_eq!(qf.queries.len(), 2);
1077    }
1078
1079    #[test]
1080    fn test_parse_complex_negation() {
1081        let input = r#"
1082query knows_alice_not_bob() {
1083    match {
1084        $a: Person { name: "Alice" }
1085        $b: Person { name: "Bob" }
1086        $p: Person
1087        $p knows $a
1088        not { $p knows $b }
1089    }
1090    return { $p.name }
1091}
1092"#;
1093        let qf = parse_query(input).unwrap();
1094        let q = &qf.queries[0];
1095        assert_eq!(q.match_clause.len(), 5);
1096    }
1097
1098    #[test]
1099    fn test_parse_filter_string() {
1100        let input = r#"
1101query test() {
1102    match {
1103        $p: Person
1104        $p.name != "Bob"
1105    }
1106    return { $p.name }
1107}
1108"#;
1109        let qf = parse_query(input).unwrap();
1110        let q = &qf.queries[0];
1111        match &q.match_clause[1] {
1112            Clause::Filter(f) => {
1113                assert_eq!(f.op, CompOp::Ne);
1114            }
1115            _ => panic!("expected Filter"),
1116        }
1117    }
1118
1119    #[test]
1120    fn test_parse_filter_string_decodes_escapes() {
1121        let input = r#"
1122query test() {
1123    match {
1124        $p: Person
1125        $p.name = "Bob\n\"Builder\"\t\\"
1126    }
1127    return { $p.name }
1128}
1129"#;
1130        let qf = parse_query(input).unwrap();
1131        let q = &qf.queries[0];
1132        match &q.match_clause[1] {
1133            Clause::Filter(f) => match &f.right {
1134                Expr::Literal(Literal::String(value)) => {
1135                    assert_eq!(value, "Bob\n\"Builder\"\t\\");
1136                }
1137                other => panic!("expected string literal, got {:?}", other),
1138            },
1139            _ => panic!("expected Filter"),
1140        }
1141    }
1142
1143    #[test]
1144    fn test_parse_string_literal_rejects_unknown_escape() {
1145        let input = r#"
1146query test() {
1147    match {
1148        $p: Person
1149        $p.name = "Bob\q"
1150    }
1151    return { $p.name }
1152}
1153"#;
1154        let err = parse_query(input).unwrap_err();
1155        assert!(err.to_string().contains("unsupported escape sequence"));
1156    }
1157
1158    #[test]
1159    fn test_parse_bool_literals() {
1160        let input = r#"
1161query flags() {
1162    match {
1163        $p: Person
1164        $p.active = true
1165        $p.active != false
1166    }
1167    return { $p.name }
1168}
1169"#;
1170        let qf = parse_query(input).unwrap();
1171        let q = &qf.queries[0];
1172        match &q.match_clause[1] {
1173            Clause::Filter(f) => match &f.right {
1174                Expr::Literal(Literal::Bool(value)) => assert!(*value),
1175                other => panic!("expected bool literal, got {:?}", other),
1176            },
1177            _ => panic!("expected Filter"),
1178        }
1179        match &q.match_clause[2] {
1180            Clause::Filter(f) => match &f.right {
1181                Expr::Literal(Literal::Bool(value)) => assert!(!*value),
1182                other => panic!("expected bool literal, got {:?}", other),
1183            },
1184            _ => panic!("expected Filter"),
1185        }
1186    }
1187
1188    #[test]
1189    fn test_parse_contains_filter() {
1190        let input = r#"
1191query tagged($tag: String) {
1192    match {
1193        $p: Person
1194        $p.tags contains $tag
1195    }
1196    return { $p.name }
1197}
1198"#;
1199        let qf = parse_query(input).unwrap();
1200        let q = &qf.queries[0];
1201        match &q.match_clause[1] {
1202            Clause::Filter(f) => {
1203                assert_eq!(f.op, CompOp::Contains);
1204                assert!(matches!(
1205                    &f.left,
1206                    Expr::PropAccess { variable, property } if variable == "p" && property == "tags"
1207                ));
1208                assert!(matches!(&f.right, Expr::Variable(v) if v == "tag"));
1209            }
1210            _ => panic!("expected Filter"),
1211        }
1212    }
1213
1214    #[test]
1215    fn test_parse_contains_is_rejected_in_mutation_predicate() {
1216        let input = r#"
1217query drop_person($tag: String) {
1218    delete Person where tags contains $tag
1219}
1220"#;
1221        assert!(parse_query(input).is_err());
1222    }
1223
1224    #[test]
1225    fn test_parse_triangle() {
1226        let input = r#"
1227query triangles($name: String) {
1228    match {
1229        $a: Person { name: $name }
1230        $a knows $b
1231        $b knows $c
1232        $c knows $a
1233    }
1234    return { $b.name, $c.name }
1235}
1236"#;
1237        let qf = parse_query(input).unwrap();
1238        let q = &qf.queries[0];
1239        assert_eq!(q.match_clause.len(), 4);
1240    }
1241
1242    #[test]
1243    fn test_parse_avg_aggregation() {
1244        let input = r#"
1245query avg_age_by_company() {
1246    match {
1247        $p: Person
1248        $p worksAt $c
1249    }
1250    return {
1251        $c.name
1252        avg($p.age) as avg_age
1253        count($p) as headcount
1254    }
1255    order { headcount desc }
1256}
1257"#;
1258        let qf = parse_query(input).unwrap();
1259        let q = &qf.queries[0];
1260        assert_eq!(q.return_clause.len(), 3);
1261    }
1262
1263    #[test]
1264    fn test_parse_insert_mutation() {
1265        let input = r#"
1266query add_person($name: String, $age: I32) {
1267    insert Person {
1268        name: $name
1269        age: $age
1270    }
1271}
1272"#;
1273        let qf = parse_query(input).unwrap();
1274        let q = &qf.queries[0];
1275        match q.mutations.first().expect("expected mutation") {
1276            Mutation::Insert(ins) => {
1277                assert_eq!(ins.type_name, "Person");
1278                assert_eq!(ins.assignments.len(), 2);
1279            }
1280            _ => panic!("expected Insert mutation"),
1281        }
1282    }
1283
1284    #[test]
1285    fn test_parse_update_mutation() {
1286        let input = r#"
1287query set_age($name: String, $age: I32) {
1288    update Person set {
1289        age: $age
1290    } where name = $name
1291}
1292"#;
1293        let qf = parse_query(input).unwrap();
1294        let q = &qf.queries[0];
1295        match q.mutations.first().expect("expected mutation") {
1296            Mutation::Update(upd) => {
1297                assert_eq!(upd.type_name, "Person");
1298                assert_eq!(upd.assignments.len(), 1);
1299                assert_eq!(upd.predicate.property, "name");
1300                assert_eq!(upd.predicate.op, CompOp::Eq);
1301            }
1302            _ => panic!("expected Update mutation"),
1303        }
1304    }
1305
1306    #[test]
1307    fn test_parse_delete_mutation() {
1308        let input = r#"
1309query drop_person($name: String) {
1310    delete Person where name = $name
1311}
1312"#;
1313        let qf = parse_query(input).unwrap();
1314        let q = &qf.queries[0];
1315        match q.mutations.first().expect("expected mutation") {
1316            Mutation::Delete(del) => {
1317                assert_eq!(del.type_name, "Person");
1318                assert_eq!(del.predicate.property, "name");
1319                assert_eq!(del.predicate.op, CompOp::Eq);
1320            }
1321            _ => panic!("expected Delete mutation"),
1322        }
1323    }
1324
1325    #[test]
1326    fn test_parse_date_and_datetime_literals() {
1327        let input = r#"
1328query dated() {
1329    match {
1330        $e: Event
1331        $e.on = date("2026-02-14")
1332        $e.at >= datetime("2026-02-14T10:00:00Z")
1333    }
1334    return { $e.id }
1335}
1336"#;
1337        let qf = parse_query(input).unwrap();
1338        let q = &qf.queries[0];
1339        match &q.match_clause[1] {
1340            Clause::Filter(f) => match &f.right {
1341                Expr::Literal(Literal::Date(v)) => assert_eq!(v, "2026-02-14"),
1342                other => panic!("expected date literal, got {:?}", other),
1343            },
1344            _ => panic!("expected Filter"),
1345        }
1346        match &q.match_clause[2] {
1347            Clause::Filter(f) => match &f.right {
1348                Expr::Literal(Literal::DateTime(v)) => assert_eq!(v, "2026-02-14T10:00:00Z"),
1349                other => panic!("expected datetime literal, got {:?}", other),
1350            },
1351            _ => panic!("expected Filter"),
1352        }
1353    }
1354
1355    #[test]
1356    fn test_parse_now_expression_and_mutation_value() {
1357        let input = r#"
1358query clock() {
1359    match {
1360        $e: Event
1361        $e.at <= now()
1362    }
1363    return { now() as ts }
1364}
1365"#;
1366        let qf = parse_query(input).unwrap();
1367        let q = &qf.queries[0];
1368        match &q.match_clause[1] {
1369            Clause::Filter(f) => assert!(matches!(f.right, Expr::Now)),
1370            _ => panic!("expected Filter"),
1371        }
1372        assert!(matches!(q.return_clause[0].expr, Expr::Now));
1373
1374        let mutation = parse_query(
1375            r#"
1376query stamp() {
1377    update Event set { updated_at: now() } where created_at <= now()
1378}
1379"#,
1380        )
1381        .unwrap();
1382        match mutation.queries[0].mutations.first().unwrap() {
1383            Mutation::Update(update) => {
1384                assert!(matches!(update.assignments[0].value, MatchValue::Now));
1385                assert!(matches!(update.predicate.value, MatchValue::Now));
1386            }
1387            _ => panic!("expected update mutation"),
1388        }
1389    }
1390
1391    #[test]
1392    fn test_parse_multi_mutation() {
1393        let input = r#"
1394query add_and_link($name: String, $age: I32, $friend: String) {
1395    insert Person { name: $name, age: $age }
1396    insert Knows { from: $name, to: $friend }
1397}
1398"#;
1399        let qf = parse_query(input).unwrap();
1400        let q = &qf.queries[0];
1401        assert_eq!(q.mutations.len(), 2);
1402        assert!(matches!(&q.mutations[0], Mutation::Insert(ins) if ins.type_name == "Person"));
1403        assert!(matches!(&q.mutations[1], Mutation::Insert(ins) if ins.type_name == "Knows"));
1404    }
1405
1406    #[test]
1407    fn test_parse_multi_mutation_mixed_ops() {
1408        let input = r#"
1409query create_and_clean($name: String, $age: I32, $old: String) {
1410    insert Person { name: $name, age: $age }
1411    delete Person where name = $old
1412}
1413"#;
1414        let qf = parse_query(input).unwrap();
1415        let q = &qf.queries[0];
1416        assert_eq!(q.mutations.len(), 2);
1417        assert!(matches!(&q.mutations[0], Mutation::Insert(_)));
1418        assert!(matches!(&q.mutations[1], Mutation::Delete(_)));
1419    }
1420
1421    #[test]
1422    fn test_parse_single_mutation_backward_compat() {
1423        let input = r#"
1424query add($name: String, $age: I32) {
1425    insert Person { name: $name, age: $age }
1426}
1427"#;
1428        let qf = parse_query(input).unwrap();
1429        assert_eq!(qf.queries[0].mutations.len(), 1);
1430    }
1431
1432    #[test]
1433    fn test_parse_list_literal() {
1434        let input = r#"
1435query listy() {
1436    match { $p: Person { tags: ["rust", "db"] } }
1437    return { $p.tags }
1438}
1439"#;
1440        let qf = parse_query(input).unwrap();
1441        let q = &qf.queries[0];
1442        match &q.match_clause[0] {
1443            Clause::Binding(b) => match &b.prop_matches[0].value {
1444                MatchValue::Literal(Literal::List(items)) => {
1445                    assert_eq!(items.len(), 2);
1446                }
1447                other => panic!("expected list literal, got {:?}", other),
1448            },
1449            _ => panic!("expected Binding"),
1450        }
1451    }
1452
1453    #[test]
1454    fn test_parse_nearest_ordering_and_vector_param_type() {
1455        let input = r#"
1456query similar($q: Vector(3)) {
1457    match { $d: Doc }
1458    return { $d.id }
1459    order { nearest($d.embedding, $q) }
1460    limit 5
1461}
1462"#;
1463        let qf = parse_query(input).unwrap();
1464        let q = &qf.queries[0];
1465        assert_eq!(q.params[0].type_name, "Vector(3)");
1466        assert_eq!(q.order_clause.len(), 1);
1467        assert!(!q.order_clause[0].descending);
1468        match &q.order_clause[0].expr {
1469            Expr::Nearest {
1470                variable,
1471                property,
1472                query,
1473            } => {
1474                assert_eq!(variable, "d");
1475                assert_eq!(property, "embedding");
1476                assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1477            }
1478            other => panic!("expected nearest ordering, got {:?}", other),
1479        }
1480    }
1481
1482    #[test]
1483    fn test_parse_nearest_with_spaced_vector_param_type() {
1484        let input = r#"
1485query similar($q: Vector( 3 ) ?) {
1486    match { $d: Doc }
1487    return { $d.id }
1488    order { nearest($d.embedding, $q) }
1489    limit 5
1490}
1491"#;
1492        let qf = parse_query(input).unwrap();
1493        let q = &qf.queries[0];
1494        assert_eq!(q.params[0].type_name, "Vector(3)");
1495        assert!(q.params[0].nullable);
1496    }
1497
1498    #[test]
1499    fn test_parse_list_and_datetime_param_types() {
1500        let input = r#"
1501query tasks($tags: [String], $days: [Date]?, $due_at: DateTime) {
1502    match { $t: Task }
1503    return { $t.slug }
1504}
1505"#;
1506        let qf = parse_query(input).unwrap();
1507        let q = &qf.queries[0];
1508        assert_eq!(q.params[0].type_name, "[String]");
1509        assert!(!q.params[0].nullable);
1510        assert_eq!(q.params[1].type_name, "[Date]");
1511        assert!(q.params[1].nullable);
1512        assert_eq!(q.params[2].type_name, "DateTime");
1513    }
1514
1515    #[test]
1516    fn test_parse_nearest_rejects_direction_modifier() {
1517        let input = r#"
1518query similar($q: Vector(3)) {
1519    match { $d: Doc }
1520    return { $d.id }
1521    order { nearest($d.embedding, $q) desc }
1522    limit 5
1523}
1524"#;
1525        assert!(parse_query(input).is_err());
1526    }
1527
1528    #[test]
1529    fn test_parse_nearest_expression_in_return_projection() {
1530        let input = r#"
1531query similar($q: Vector(3)) {
1532    match { $d: Doc }
1533    return { $d.id, nearest($d.embedding, $q) as score }
1534    order { nearest($d.embedding, $q) }
1535    limit 5
1536}
1537"#;
1538        let qf = parse_query(input).unwrap();
1539        let q = &qf.queries[0];
1540        assert_eq!(q.return_clause.len(), 2);
1541        match &q.return_clause[1].expr {
1542            Expr::Nearest {
1543                variable,
1544                property,
1545                query,
1546            } => {
1547                assert_eq!(variable, "d");
1548                assert_eq!(property, "embedding");
1549                assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1550            }
1551            other => panic!(
1552                "expected nearest expression in return projection, got {:?}",
1553                other
1554            ),
1555        }
1556        assert_eq!(q.return_clause[1].alias.as_deref(), Some("score"));
1557    }
1558
1559    #[test]
1560    fn test_parse_search_clause_sugar() {
1561        let input = r#"
1562query q($q: String) {
1563    match {
1564        $s: Signal
1565        search($s.summary, $q)
1566    }
1567    return { $s.slug }
1568}
1569"#;
1570        let qf = parse_query(input).unwrap();
1571        let q = &qf.queries[0];
1572        assert_eq!(q.match_clause.len(), 2);
1573        match &q.match_clause[1] {
1574            Clause::Filter(Filter { left, op, right }) => {
1575                assert_eq!(*op, CompOp::Eq);
1576                assert!(matches!(right, Expr::Literal(Literal::Bool(true))));
1577                match left {
1578                    Expr::Search { field, query } => {
1579                        assert!(matches!(
1580                            field.as_ref(),
1581                            Expr::PropAccess { variable, property } if variable == "s" && property == "summary"
1582                        ));
1583                        assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1584                    }
1585                    other => panic!("expected search expression, got {:?}", other),
1586                }
1587            }
1588            other => panic!("expected filter clause, got {:?}", other),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_parse_fuzzy_clause_with_max_edits() {
1594        let input = r#"
1595query q($q: String) {
1596    match {
1597        $s: Signal
1598        fuzzy($s.summary, $q, 2)
1599    }
1600    return { $s.slug }
1601}
1602"#;
1603        let qf = parse_query(input).unwrap();
1604        let q = &qf.queries[0];
1605        assert_eq!(q.match_clause.len(), 2);
1606        match &q.match_clause[1] {
1607            Clause::Filter(Filter { left, op, right }) => {
1608                assert_eq!(*op, CompOp::Eq);
1609                assert!(matches!(right, Expr::Literal(Literal::Bool(true))));
1610                match left {
1611                    Expr::Fuzzy {
1612                        field,
1613                        query,
1614                        max_edits,
1615                    } => {
1616                        assert!(matches!(
1617                            field.as_ref(),
1618                            Expr::PropAccess { variable, property } if variable == "s" && property == "summary"
1619                        ));
1620                        assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1621                        assert!(matches!(
1622                            max_edits.as_deref(),
1623                            Some(Expr::Literal(Literal::Integer(2)))
1624                        ));
1625                    }
1626                    other => panic!("expected fuzzy expression, got {:?}", other),
1627                }
1628            }
1629            other => panic!("expected filter clause, got {:?}", other),
1630        }
1631    }
1632
1633    #[test]
1634    fn test_parse_match_text_clause_sugar() {
1635        let input = r#"
1636query q($q: String) {
1637    match {
1638        $s: Signal
1639        match_text($s.summary, $q)
1640    }
1641    return { $s.slug }
1642}
1643"#;
1644        let qf = parse_query(input).unwrap();
1645        let q = &qf.queries[0];
1646        assert_eq!(q.match_clause.len(), 2);
1647        match &q.match_clause[1] {
1648            Clause::Filter(Filter { left, op, right }) => {
1649                assert_eq!(*op, CompOp::Eq);
1650                assert!(matches!(right, Expr::Literal(Literal::Bool(true))));
1651                match left {
1652                    Expr::MatchText { field, query } => {
1653                        assert!(matches!(
1654                            field.as_ref(),
1655                            Expr::PropAccess { variable, property } if variable == "s" && property == "summary"
1656                        ));
1657                        assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1658                    }
1659                    other => panic!("expected match_text expression, got {:?}", other),
1660                }
1661            }
1662            other => panic!("expected filter clause, got {:?}", other),
1663        }
1664    }
1665
1666    #[test]
1667    fn test_parse_bm25_expression_in_order() {
1668        let input = r#"
1669query q($q: String) {
1670    match { $s: Signal }
1671    return { $s.slug, bm25($s.summary, $q) as score }
1672    order { bm25($s.summary, $q) desc }
1673    limit 5
1674}
1675"#;
1676        let qf = parse_query(input).unwrap();
1677        let q = &qf.queries[0];
1678        assert_eq!(q.return_clause.len(), 2);
1679        match &q.return_clause[1].expr {
1680            Expr::Bm25 { field, query } => {
1681                assert!(matches!(
1682                    field.as_ref(),
1683                    Expr::PropAccess { variable, property } if variable == "s" && property == "summary"
1684                ));
1685                assert!(matches!(query.as_ref(), Expr::Variable(v) if v == "q"));
1686            }
1687            other => panic!("expected bm25 expression, got {:?}", other),
1688        }
1689        assert_eq!(q.order_clause.len(), 1);
1690        assert!(q.order_clause[0].descending);
1691    }
1692
1693    #[test]
1694    fn test_parse_rrf_ordering_with_nearest_and_bm25() {
1695        let input = r#"
1696query q($vq: Vector(3), $tq: String) {
1697    match { $s: Signal }
1698    return { $s.slug }
1699    order { rrf(nearest($s.embedding, $vq), bm25($s.summary, $tq), 60) desc }
1700    limit 5
1701}
1702"#;
1703        let qf = parse_query(input).unwrap();
1704        let q = &qf.queries[0];
1705        assert_eq!(q.order_clause.len(), 1);
1706        assert!(q.order_clause[0].descending);
1707        match &q.order_clause[0].expr {
1708            Expr::Rrf {
1709                primary,
1710                secondary,
1711                k,
1712            } => {
1713                assert!(matches!(primary.as_ref(), Expr::Nearest { .. }));
1714                assert!(matches!(secondary.as_ref(), Expr::Bm25 { .. }));
1715                assert!(matches!(
1716                    k.as_deref(),
1717                    Some(Expr::Literal(Literal::Integer(60)))
1718                ));
1719            }
1720            other => panic!("expected rrf expression, got {:?}", other),
1721        }
1722    }
1723
1724    #[test]
1725    fn test_parse_error_diagnostic_has_span() {
1726        let input = r#"
1727query q() {
1728    match {
1729        $p: Person
1730    }
1731    return { $p.name
1732}
1733"#;
1734        let err = parse_query_diagnostic(input).unwrap_err();
1735        assert!(err.span.is_some());
1736    }
1737}