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}