1pub mod ast;
4pub mod clause;
6pub mod expression;
8pub mod pattern;
10
11use crate::lexer::{lex, LexError, Span, Token};
12pub use ast::*;
13
14pub fn parse_query(input: &str) -> Result<Query, ParseError> {
22 let tokens = lex(input).map_err(|e: LexError| {
23 let mut line = 1;
25 let mut col = 1;
26 for (i, ch) in input.char_indices() {
27 if i >= e.position {
28 break;
29 }
30 if ch == '\n' {
31 line += 1;
32 col = 1;
33 } else {
34 col += 1;
35 }
36 }
37 ParseError {
38 line,
39 column: col,
40 message: e.to_string(),
41 }
42 })?;
43
44 let mut parser = Parser::new(&tokens, input);
45 let mut clauses = Vec::new();
46
47 while !parser.at_end() {
48 let clause = match parser.peek() {
49 Some(Token::Optional) => {
50 parser.advance(); if !parser.check(&Token::Match) {
52 return Err(parser.error("expected MATCH after OPTIONAL"));
53 }
54 Clause::Match(parser.parse_match_clause(true)?)
55 }
56 Some(Token::Match) => {
57 let next1 = parser.tokens.get(parser.pos + 1).map(|(t, _)| t);
59 #[cfg(feature = "hypergraph")]
60 {
61 if next1 == Some(&Token::Hyperedge) {
62 parser.advance(); Clause::MatchHyperedge(parser.parse_match_hyperedge_clause()?)
64 } else {
65 Clause::Match(parser.parse_match_clause(false)?)
66 }
67 }
68 #[cfg(not(feature = "hypergraph"))]
69 {
70 let _ = next1;
71 Clause::Match(parser.parse_match_clause(false)?)
72 }
73 }
74 Some(Token::Return) => Clause::Return(parser.parse_return_clause()?),
75 Some(Token::Create) => {
76 let next1 = parser.tokens.get(parser.pos + 1).map(|(t, _)| t);
78 let next2 = parser.tokens.get(parser.pos + 2).map(|(t, _)| t);
79 if next1 == Some(&Token::Index) {
80 parser.advance(); Clause::CreateIndex(parser.parse_create_index_clause()?)
82 } else if next1 == Some(&Token::Edge) && next2 == Some(&Token::Index) {
83 parser.advance(); Clause::CreateIndex(parser.parse_create_edge_index_clause()?)
85 } else {
86 #[allow(unused_labels)]
89 'create_dispatch: {
90 #[cfg(feature = "hypergraph")]
91 if next1 == Some(&Token::Hyperedge) {
92 parser.advance(); break 'create_dispatch Clause::CreateHyperedge(
94 parser.parse_create_hyperedge_clause()?,
95 );
96 }
97 #[cfg(feature = "subgraph")]
98 if next1 == Some(&Token::Snapshot) {
99 parser.advance(); break 'create_dispatch Clause::CreateSnapshot(
101 parser.parse_create_snapshot_clause()?,
102 );
103 }
104 Clause::Create(parser.parse_create_clause()?)
105 }
106 }
107 }
108 Some(Token::Set) => Clause::Set(parser.parse_set_clause()?),
109 Some(Token::Remove) => Clause::Remove(parser.parse_remove_clause()?),
110 Some(Token::Delete) => Clause::Delete(parser.parse_delete_clause(false)?),
111 Some(Token::Detach) => {
112 parser.advance(); if !parser.check(&Token::Delete) {
114 return Err(parser.error("expected DELETE after DETACH"));
115 }
116 Clause::Delete(parser.parse_delete_clause(true)?)
117 }
118 Some(Token::With) => Clause::With(parser.parse_with_clause()?),
119 Some(Token::Merge) => Clause::Merge(parser.parse_merge_clause()?),
120 Some(Token::Unwind) => Clause::Unwind(parser.parse_unwind_clause()?),
121 Some(Token::Drop) => Clause::DropIndex(parser.parse_drop_index_clause()?),
122 Some(Token::Where) => {
123 return Err(parser.error("WHERE clause must follow a MATCH clause"));
124 }
125 Some(Token::Order | Token::Limit | Token::Skip) => {
126 return Err(parser.error("ORDER BY / SKIP / LIMIT must be part of a RETURN clause"));
127 }
128 Some(tok) => {
129 return Err(parser.error(format!("unexpected token at top level: {:?}", tok)));
130 }
131 None => break,
132 };
133 clauses.push(clause);
134 }
135
136 if clauses.is_empty() {
137 return Err(ParseError {
138 line: 1,
139 column: 1,
140 message: "empty query".to_string(),
141 });
142 }
143
144 Ok(Query { clauses })
145}
146
147#[derive(Debug, Clone, PartialEq)]
149pub struct ParseError {
150 pub line: usize,
152 pub column: usize,
154 pub message: String,
156}
157
158impl std::fmt::Display for ParseError {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 write!(
161 f,
162 "Parse error at {}:{}: {}",
163 self.line, self.column, self.message
164 )
165 }
166}
167
168impl std::error::Error for ParseError {}
169
170pub struct Parser<'a> {
172 tokens: &'a [(Token, Span)],
173 pos: usize,
174 input: &'a str,
175}
176
177impl<'a> Parser<'a> {
178 pub fn new(tokens: &'a [(Token, Span)], input: &'a str) -> Self {
180 Self {
181 tokens,
182 pos: 0,
183 input,
184 }
185 }
186
187 pub fn peek(&self) -> Option<&Token> {
189 self.tokens.get(self.pos).map(|(t, _)| t)
190 }
191
192 pub fn advance(&mut self) -> Option<&(Token, Span)> {
194 let tok = self.tokens.get(self.pos);
195 if tok.is_some() {
196 self.pos += 1;
197 }
198 tok
199 }
200
201 pub fn expect(&mut self, expected: &Token) -> Result<Span, ParseError> {
203 match self.tokens.get(self.pos) {
204 Some((tok, span)) if tok == expected => {
205 let s = *span;
206 self.pos += 1;
207 Ok(s)
208 }
209 Some((tok, span)) => {
210 let (line, col) = self.offset_to_line_col(span.start);
211 Err(ParseError {
212 line,
213 column: col,
214 message: format!("expected {:?}, found {:?}", expected, tok),
215 })
216 }
217 None => {
218 let offset = self.tokens.last().map(|(_, s)| s.end).unwrap_or(0);
219 let (line, col) = self.offset_to_line_col(offset);
220 Err(ParseError {
221 line,
222 column: col,
223 message: format!("expected {:?}, found end of input", expected),
224 })
225 }
226 }
227 }
228
229 pub fn check(&self, expected: &Token) -> bool {
231 self.peek() == Some(expected)
232 }
233
234 pub fn eat(&mut self, expected: &Token) -> bool {
236 if self.check(expected) {
237 self.pos += 1;
238 true
239 } else {
240 false
241 }
242 }
243
244 pub fn current_span(&self) -> Span {
246 self.tokens
247 .get(self.pos)
248 .map(|(_, s)| *s)
249 .unwrap_or(Span { start: 0, end: 0 })
250 }
251
252 pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
254 let mut line = 1;
255 let mut col = 1;
256 for (i, ch) in self.input.char_indices() {
257 if i >= offset {
258 break;
259 }
260 if ch == '\n' {
261 line += 1;
262 col = 1;
263 } else {
264 col += 1;
265 }
266 }
267 (line, col)
268 }
269
270 pub fn error(&self, message: impl Into<String>) -> ParseError {
272 let span = self.current_span();
273 let (line, col) = self.offset_to_line_col(span.start);
274 ParseError {
275 line,
276 column: col,
277 message: message.into(),
278 }
279 }
280
281 pub fn at_end(&self) -> bool {
283 self.pos >= self.tokens.len()
284 }
285
286 pub fn expect_ident(&mut self) -> Result<String, ParseError> {
288 match self.tokens.get(self.pos) {
289 Some((Token::Ident(name), _)) => {
290 let name = name.clone();
291 self.pos += 1;
292 Ok(name)
293 }
294 Some((Token::BacktickIdent(name), _)) => {
295 let name = name.clone();
296 self.pos += 1;
297 Ok(name)
298 }
299 Some((tok, span)) => {
300 let (line, col) = self.offset_to_line_col(span.start);
301 Err(ParseError {
302 line,
303 column: col,
304 message: format!("expected identifier, found {:?}", tok),
305 })
306 }
307 None => {
308 let offset = self.tokens.last().map(|(_, s)| s.end).unwrap_or(0);
309 let (line, col) = self.offset_to_line_col(offset);
310 Err(ParseError {
311 line,
312 column: col,
313 message: "expected identifier, found end of input".to_string(),
314 })
315 }
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::lexer::Span;
324
325 #[test]
326 fn parser_peek_and_advance() {
327 let tokens = lex("MATCH").expect("should lex");
328 let mut p = Parser::new(&tokens, "MATCH");
329 assert_eq!(p.peek(), Some(&Token::Match));
330 let tok = p.advance();
331 assert!(tok.is_some());
332 assert_eq!(p.peek(), None);
333 }
334
335 #[test]
336 fn parser_expect_success() {
337 let tokens = lex("(").expect("should lex");
338 let mut p = Parser::new(&tokens, "(");
339 let span = p.expect(&Token::LParen);
340 assert!(span.is_ok());
341 assert_eq!(span.expect("checked above"), Span { start: 0, end: 1 });
342 }
343
344 #[test]
345 fn parser_expect_failure() {
346 let tokens = lex("(").expect("should lex");
347 let mut p = Parser::new(&tokens, "(");
348 let result = p.expect(&Token::RParen);
349 assert!(result.is_err());
350 }
351
352 #[test]
353 fn parser_offset_to_line_col() {
354 let input = "line1\nline2\nline3";
355 let tokens = lex(input).expect("should lex");
356 let p = Parser::new(&tokens, input);
357 assert_eq!(p.offset_to_line_col(0), (1, 1));
358 assert_eq!(p.offset_to_line_col(6), (2, 1));
359 assert_eq!(p.offset_to_line_col(12), (3, 1));
360 }
361
362 #[test]
363 fn parser_eat_and_check() {
364 let tokens = lex("( )").expect("should lex");
365 let mut p = Parser::new(&tokens, "( )");
366 assert!(p.check(&Token::LParen));
367 assert!(!p.check(&Token::RParen));
368 assert!(p.eat(&Token::LParen));
369 assert!(!p.eat(&Token::LParen));
370 assert!(p.eat(&Token::RParen));
371 assert!(p.at_end());
372 }
373
374 #[test]
375 fn parser_expect_ident() {
376 let tokens = lex("foo").expect("should lex");
377 let mut p = Parser::new(&tokens, "foo");
378 let name = p.expect_ident();
379 assert_eq!(name.expect("should be ident"), "foo");
380 }
381
382 #[test]
383 fn parser_expect_ident_backtick() {
384 let tokens = lex("`my var`").expect("should lex");
385 let mut p = Parser::new(&tokens, "`my var`");
386 let name = p.expect_ident();
387 assert_eq!(name.expect("should be ident"), "my var");
388 }
389
390 #[test]
391 fn parse_error_display() {
392 let err = ParseError {
393 line: 1,
394 column: 5,
395 message: "unexpected token".to_string(),
396 };
397 assert_eq!(err.to_string(), "Parse error at 1:5: unexpected token");
398 }
399
400 #[test]
405 fn query_match_return() {
406 let q = parse_query("MATCH (n:Person) RETURN n").expect("should parse");
407 assert_eq!(q.clauses.len(), 2);
408 assert!(matches!(&q.clauses[0], Clause::Match(_)));
409 assert!(matches!(&q.clauses[1], Clause::Return(_)));
410
411 if let Clause::Match(mc) = &q.clauses[0] {
412 assert!(!mc.optional);
413 let node = match &mc.pattern.chains[0].elements[0] {
414 PatternElement::Node(n) => n,
415 _ => panic!("expected node"),
416 };
417 assert_eq!(node.labels, vec!["Person".to_string()]);
418 }
419 }
420
421 #[test]
422 fn query_create_with_properties() {
423 let q = parse_query("CREATE (n:Person {name: 'Alice'})").expect("should parse");
424 assert_eq!(q.clauses.len(), 1);
425 assert!(matches!(&q.clauses[0], Clause::Create(_)));
426
427 if let Clause::Create(cc) = &q.clauses[0] {
428 let node = match &cc.pattern.chains[0].elements[0] {
429 PatternElement::Node(n) => n,
430 _ => panic!("expected node"),
431 };
432 assert_eq!(node.labels, vec!["Person".to_string()]);
433 assert!(node.properties.is_some());
434 }
435 }
436
437 #[test]
438 fn query_match_relationship_return() {
439 let q = parse_query("MATCH (a)-[:KNOWS]->(b) RETURN b.name").expect("should parse");
440 assert_eq!(q.clauses.len(), 2);
441
442 if let Clause::Match(mc) = &q.clauses[0] {
443 assert_eq!(mc.pattern.chains[0].elements.len(), 3);
444 } else {
445 panic!("expected MATCH clause");
446 }
447
448 if let Clause::Return(rc) = &q.clauses[1] {
449 assert_eq!(
450 rc.items[0].expr,
451 Expression::Property(
452 Box::new(Expression::Variable("b".to_string())),
453 "name".to_string(),
454 )
455 );
456 } else {
457 panic!("expected RETURN clause");
458 }
459 }
460
461 #[test]
462 fn query_match_where_return() {
463 let q =
464 parse_query("MATCH (n:Person) WHERE n.age > 30 RETURN n.name").expect("should parse");
465 assert_eq!(q.clauses.len(), 2);
466
467 if let Clause::Match(mc) = &q.clauses[0] {
468 assert!(mc.where_clause.is_some());
469 } else {
470 panic!("expected MATCH clause");
471 }
472 }
473
474 #[test]
475 fn query_return_order_limit() {
476 let q = parse_query("MATCH (n:Person) RETURN n.name ORDER BY n.name ASC LIMIT 10")
477 .expect("should parse");
478 assert_eq!(q.clauses.len(), 2);
479
480 if let Clause::Return(rc) = &q.clauses[1] {
481 let order = rc.order_by.as_ref().expect("should have ORDER BY");
482 assert_eq!(order.len(), 1);
483 assert!(order[0].ascending);
484 assert_eq!(
485 rc.limit.as_ref().expect("should have LIMIT"),
486 &Expression::Literal(Literal::Integer(10))
487 );
488 } else {
489 panic!("expected RETURN clause");
490 }
491 }
492
493 #[test]
494 fn query_match_set() {
495 let q = parse_query("MATCH (n:Person) SET n.age = 30").expect("should parse");
496 assert_eq!(q.clauses.len(), 2);
497 assert!(matches!(&q.clauses[0], Clause::Match(_)));
498 assert!(matches!(&q.clauses[1], Clause::Set(_)));
499 }
500
501 #[test]
502 fn query_detach_delete() {
503 let q = parse_query("DETACH DELETE n").expect("should parse");
504 assert_eq!(q.clauses.len(), 1);
505
506 if let Clause::Delete(dc) = &q.clauses[0] {
507 assert!(dc.detach);
508 assert_eq!(dc.exprs[0], Expression::Variable("n".to_string()));
509 } else {
510 panic!("expected DELETE clause");
511 }
512 }
513
514 #[test]
515 fn query_optional_match() {
516 let q = parse_query("MATCH (n:Person) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n, m")
517 .expect("should parse");
518 assert_eq!(q.clauses.len(), 3);
519
520 if let Clause::Match(mc) = &q.clauses[0] {
521 assert!(!mc.optional);
522 } else {
523 panic!("expected MATCH clause");
524 }
525
526 if let Clause::Match(mc) = &q.clauses[1] {
527 assert!(mc.optional);
528 } else {
529 panic!("expected OPTIONAL MATCH clause");
530 }
531 }
532
533 #[test]
534 fn query_count_star() {
535 let q = parse_query("MATCH (n) RETURN count(*)").expect("should parse");
536 assert_eq!(q.clauses.len(), 2);
537
538 if let Clause::Return(rc) = &q.clauses[1] {
539 assert_eq!(rc.items[0].expr, Expression::CountStar);
540 } else {
541 panic!("expected RETURN clause");
542 }
543 }
544
545 #[test]
546 fn query_count_distinct() {
547 let q = parse_query("MATCH (n) RETURN count(DISTINCT n.name)").expect("should parse");
548
549 if let Clause::Return(rc) = &q.clauses[1] {
550 assert_eq!(
551 rc.items[0].expr,
552 Expression::FunctionCall {
553 name: "count".to_string(),
554 distinct: true,
555 args: vec![Expression::Property(
556 Box::new(Expression::Variable("n".to_string())),
557 "name".to_string(),
558 )],
559 }
560 );
561 } else {
562 panic!("expected RETURN clause");
563 }
564 }
565
566 #[test]
567 fn query_create_relationship() {
568 let q = parse_query("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
569 .expect("should parse");
570 assert_eq!(q.clauses.len(), 1);
571
572 if let Clause::Create(cc) = &q.clauses[0] {
573 assert_eq!(cc.pattern.chains[0].elements.len(), 3);
574 if let PatternElement::Node(n) = &cc.pattern.chains[0].elements[0] {
576 assert_eq!(n.variable, Some("a".to_string()));
577 assert_eq!(n.labels, vec!["Person".to_string()]);
578 }
579 if let PatternElement::Relationship(r) = &cc.pattern.chains[0].elements[1] {
581 assert_eq!(r.rel_types, vec!["KNOWS".to_string()]);
582 assert_eq!(r.direction, RelDirection::Outgoing);
583 }
584 if let PatternElement::Node(n) = &cc.pattern.chains[0].elements[2] {
586 assert_eq!(n.variable, Some("b".to_string()));
587 }
588 } else {
589 panic!("expected CREATE clause");
590 }
591 }
592
593 #[test]
596 fn query_error_empty() {
597 let result = parse_query("");
598 assert!(result.is_err());
599 assert!(result
600 .expect_err("should fail")
601 .message
602 .contains("empty query"));
603 }
604
605 #[test]
606 fn query_error_where_without_match() {
607 let result = parse_query("WHERE n.age > 30");
608 assert!(result.is_err());
609 assert!(result.expect_err("should fail").message.contains("WHERE"));
610 }
611
612 #[test]
613 fn query_error_order_without_return() {
614 let result = parse_query("ORDER BY n.name");
615 assert!(result.is_err());
616 }
617
618 #[test]
619 fn query_error_unexpected_token() {
620 let result = parse_query("42");
621 assert!(result.is_err());
622 }
623
624 #[test]
625 fn query_error_lex_error() {
626 let result = parse_query("MATCH @");
627 assert!(result.is_err());
628 }
629
630 #[test]
631 fn query_with_clause() {
632 let q = parse_query("MATCH (n) WITH n WHERE n.age > 30 RETURN n").expect("should parse");
633 assert_eq!(q.clauses.len(), 3);
634 assert!(matches!(&q.clauses[0], Clause::Match(_)));
635 assert!(matches!(&q.clauses[1], Clause::With(_)));
636 assert!(matches!(&q.clauses[2], Clause::Return(_)));
637
638 if let Clause::With(wc) = &q.clauses[1] {
639 assert!(wc.where_clause.is_some());
640 }
641 }
642
643 #[test]
644 fn query_merge() {
645 let q = parse_query("MERGE (n:Person {name: 'Alice'})").expect("should parse");
646 assert_eq!(q.clauses.len(), 1);
647 assert!(matches!(&q.clauses[0], Clause::Merge(_)));
648 }
649
650 #[test]
651 fn query_match_remove() {
652 let q = parse_query("MATCH (n:Person) REMOVE n.email, n:Temp").expect("should parse");
653 assert_eq!(q.clauses.len(), 2);
654 assert!(matches!(&q.clauses[1], Clause::Remove(_)));
655 }
656
657 #[test]
658 fn query_match_delete() {
659 let q = parse_query("MATCH (n:Person) DELETE n").expect("should parse");
660 assert_eq!(q.clauses.len(), 2);
661
662 if let Clause::Delete(dc) = &q.clauses[1] {
663 assert!(!dc.detach);
664 }
665 }
666
667 #[test]
668 fn query_return_skip_limit() {
669 let q = parse_query("MATCH (n) RETURN n SKIP 5 LIMIT 10").expect("should parse");
670
671 if let Clause::Return(rc) = &q.clauses[1] {
672 assert_eq!(
673 rc.skip.as_ref().expect("should have SKIP"),
674 &Expression::Literal(Literal::Integer(5))
675 );
676 assert_eq!(
677 rc.limit.as_ref().expect("should have LIMIT"),
678 &Expression::Literal(Literal::Integer(10))
679 );
680 }
681 }
682
683 #[test]
684 fn query_case_insensitive() {
685 let q = parse_query("match (n:Person) return n").expect("should parse");
686 assert_eq!(q.clauses.len(), 2);
687 }
688
689 #[test]
694 fn query_create_index_with_name() {
695 let q = parse_query("CREATE INDEX idx_person_name ON :Person(name)").expect("should parse");
696 assert_eq!(q.clauses.len(), 1);
697
698 if let Clause::CreateIndex(ci) = &q.clauses[0] {
699 assert_eq!(ci.name, Some("idx_person_name".to_string()));
700 assert_eq!(ci.target, IndexTarget::NodeLabel("Person".to_string()));
701 assert_eq!(ci.property, "name");
702 } else {
703 panic!("expected CreateIndex clause");
704 }
705 }
706
707 #[test]
708 fn query_create_index_without_name() {
709 let q = parse_query("CREATE INDEX ON :Person(name)").expect("should parse");
710 assert_eq!(q.clauses.len(), 1);
711
712 if let Clause::CreateIndex(ci) = &q.clauses[0] {
713 assert_eq!(ci.name, None);
714 assert_eq!(ci.target, IndexTarget::NodeLabel("Person".to_string()));
715 assert_eq!(ci.property, "name");
716 } else {
717 panic!("expected CreateIndex clause");
718 }
719 }
720
721 #[test]
722 fn query_drop_index() {
723 let q = parse_query("DROP INDEX idx_person_name").expect("should parse");
724 assert_eq!(q.clauses.len(), 1);
725
726 if let Clause::DropIndex(di) = &q.clauses[0] {
727 assert_eq!(di.name, "idx_person_name");
728 } else {
729 panic!("expected DropIndex clause");
730 }
731 }
732
733 #[test]
735 fn query_create_edge_index_with_name() {
736 let q = parse_query("CREATE EDGE INDEX eidx_knows_since ON :KNOWS(since)")
737 .expect("should parse");
738 assert_eq!(q.clauses.len(), 1);
739
740 if let Clause::CreateIndex(ci) = &q.clauses[0] {
741 assert_eq!(ci.name, Some("eidx_knows_since".to_string()));
742 assert_eq!(
743 ci.target,
744 IndexTarget::RelationshipType("KNOWS".to_string())
745 );
746 assert_eq!(ci.property, "since");
747 } else {
748 panic!("expected CreateIndex clause with RelationshipType target");
749 }
750 }
751
752 #[test]
753 fn query_create_edge_index_without_name() {
754 let q = parse_query("CREATE EDGE INDEX ON :LIKES(weight)").expect("should parse");
755 assert_eq!(q.clauses.len(), 1);
756
757 if let Clause::CreateIndex(ci) = &q.clauses[0] {
758 assert_eq!(ci.name, None);
759 assert_eq!(
760 ci.target,
761 IndexTarget::RelationshipType("LIKES".to_string())
762 );
763 assert_eq!(ci.property, "weight");
764 } else {
765 panic!("expected CreateIndex clause with RelationshipType target");
766 }
767 }
768
769 #[test]
774 fn query_unwind_list_return() {
775 let q = parse_query("UNWIND [1, 2, 3] AS x RETURN x").expect("should parse");
776 assert_eq!(q.clauses.len(), 2);
777 assert!(matches!(&q.clauses[0], Clause::Unwind(_)));
778 assert!(matches!(&q.clauses[1], Clause::Return(_)));
779
780 if let Clause::Unwind(uc) = &q.clauses[0] {
781 assert_eq!(uc.variable, "x");
782 assert!(matches!(&uc.expr, Expression::ListLiteral(_)));
783 }
784 }
785
786 #[test]
787 fn query_match_unwind_return() {
788 let q =
789 parse_query("MATCH (n:Person) UNWIND n.hobbies AS h RETURN h").expect("should parse");
790 assert_eq!(q.clauses.len(), 3);
791 assert!(matches!(&q.clauses[0], Clause::Match(_)));
792 assert!(matches!(&q.clauses[1], Clause::Unwind(_)));
793 assert!(matches!(&q.clauses[2], Clause::Return(_)));
794 }
795
796 #[cfg(feature = "subgraph")]
801 mod snapshot_integration_tests {
802 use super::*;
803
804 #[test]
806 fn query_create_snapshot_basic() {
807 let q = parse_query("CREATE SNAPSHOT (s:Snap) FROM MATCH (n:Person) RETURN n")
808 .expect("should parse");
809 assert_eq!(q.clauses.len(), 1);
810 assert!(matches!(&q.clauses[0], Clause::CreateSnapshot(_)));
811
812 if let Clause::CreateSnapshot(sc) = &q.clauses[0] {
813 assert_eq!(sc.variable, Some("s".to_string()));
814 assert_eq!(sc.labels, vec!["Snap".to_string()]);
815 assert!(sc.properties.is_none());
816 assert!(sc.temporal_anchor.is_none());
817 } else {
818 panic!("expected CreateSnapshot clause");
819 }
820 }
821
822 #[test]
824 fn query_create_snapshot_with_at_time() {
825 let q =
826 parse_query("CREATE SNAPSHOT (s:Snap) AT TIME 1000 FROM MATCH (n:Person) RETURN n")
827 .expect("should parse");
828 assert_eq!(q.clauses.len(), 1);
829
830 if let Clause::CreateSnapshot(sc) = &q.clauses[0] {
831 assert!(sc.temporal_anchor.is_some());
832 } else {
833 panic!("expected CreateSnapshot clause");
834 }
835 }
836
837 #[test]
839 fn query_create_snapshot_with_where() {
840 let q = parse_query(
841 "CREATE SNAPSHOT (s:Snap) FROM MATCH (n:Person) WHERE n.age > 30 RETURN n",
842 )
843 .expect("should parse");
844
845 if let Clause::CreateSnapshot(sc) = &q.clauses[0] {
846 assert!(sc.from_match.where_clause.is_some());
847 } else {
848 panic!("expected CreateSnapshot clause");
849 }
850 }
851
852 #[test]
854 fn query_create_snapshot_with_props_and_rel() {
855 let q = parse_query(
856 "CREATE SNAPSHOT (s:Snap {name: 'team'}) FROM MATCH (n:Person)-[:KNOWS]->(m) RETURN n, m",
857 )
858 .expect("should parse");
859
860 if let Clause::CreateSnapshot(sc) = &q.clauses[0] {
861 assert!(sc.properties.is_some());
862 assert_eq!(sc.from_return.len(), 2);
863 assert_eq!(sc.from_match.pattern.chains[0].elements.len(), 3);
865 } else {
866 panic!("expected CreateSnapshot clause");
867 }
868 }
869 }
870}