1pub mod ast;
2mod grammar;
3pub mod locy_ast;
4
5pub use grammar::{ParseError, parse, parse_expression, parse_locy};
6
7#[cfg(test)]
8mod tests {
9 use super::*;
10
11 #[test]
12 fn test_comprehension_with_complex_where() {
13 let test_cases = vec![
14 (
16 "AND operator",
17 "RETURN [x IN range(1,100) WHERE x > 10 AND x < 50 | x * 2] AS result",
18 ),
19 (
20 "OR operator",
21 "RETURN [x IN nodes WHERE x.active OR x.admin | x.name] AS result",
22 ),
23 (
24 "XOR operator",
25 "RETURN [x IN items WHERE x.flag1 XOR x.flag2 | x.id] AS result",
26 ),
27 (
29 "Parenthesized OR with AND",
30 "RETURN [x IN list WHERE (x > 0 AND x < 10) OR x = 100 | x] AS result",
31 ),
32 (
33 "Complex nested",
34 "RETURN [x IN data WHERE (x.a AND x.b) OR (x.c AND NOT x.d) | x.value] AS result",
35 ),
36 (
37 "Triple nesting",
38 "RETURN [x IN items WHERE ((x.a OR x.b) AND x.c) OR (x.d AND NOT x.e) | x] AS result",
39 ),
40 (
42 "NOT with AND",
43 "RETURN [x IN list WHERE NOT x.deleted AND x.active | x] AS result",
44 ),
45 (
46 "NOT with OR",
47 "RETURN [x IN list WHERE NOT (x.a OR x.b) | x] AS result",
48 ),
49 (
50 "Multiple NOT",
51 "RETURN [x IN list WHERE NOT x.a AND NOT x.b | x] AS result",
52 ),
53 (
55 "Filter-only with AND",
56 "RETURN [x IN list WHERE x > 5 AND x < 10] AS filtered",
57 ),
58 (
59 "Filter-only with OR",
60 "RETURN [x IN list WHERE x < 0 OR x > 100] AS outliers",
61 ),
62 (
63 "Filter-only complex",
64 "RETURN [x IN data WHERE (x.status = 'active' AND x.verified) OR x.admin] AS users",
65 ),
66 (
68 "Pattern with AND",
69 "RETURN [(a)-[:KNOWS]->(b) WHERE b.age > 21 AND b.active | b.name] AS friends",
70 ),
71 (
72 "Pattern with OR",
73 "RETURN [(n)-[:LIKES|LOVES]->(m) WHERE m.public OR n.friend | m] AS items",
74 ),
75 (
76 "Pattern complex",
77 "RETURN [p = (a)-[r]->(b) WHERE (r.weight > 5 AND b.score > 10) OR a.vip | p] AS paths",
78 ),
79 (
81 "Multiple comparisons",
82 "RETURN [x IN items WHERE x.price > 10 AND x.price < 100 AND x.inStock | x] AS affordable",
83 ),
84 (
85 "String operators",
86 "RETURN [x IN names WHERE x STARTS WITH 'A' AND NOT x ENDS WITH 'z' | x] AS filtered",
87 ),
88 (
89 "IN with AND",
90 "RETURN [x IN numbers WHERE x IN [1,2,3] AND x % 2 = 0 | x * 10] AS even",
91 ),
92 (
94 "Nested properties",
95 "RETURN [x IN items WHERE x.meta.active AND (x.meta.score > 5 OR x.priority) | x.id] AS result",
96 ),
97 (
98 "Property with NULL",
99 "RETURN [x IN items WHERE x.prop IS NOT NULL AND x.prop > 0 | x] AS valid",
100 ),
101 (
103 "AND OR XOR mix",
104 "RETURN [x IN list WHERE (x.a AND x.b) OR (x.c XOR x.d) | x] AS result",
105 ),
106 (
107 "Complex mix",
108 "RETURN [x IN data WHERE (x.flag1 OR x.flag2) AND NOT (x.flag3 XOR x.flag4) | x.value] AS result",
109 ),
110 ];
111
112 println!("\n=== Testing Complex Comprehension WHERE Clauses ===\n");
113
114 for (name, query) in test_cases.iter() {
115 match parse(query) {
116 Ok(_) => println!("✅ {}: PASSED", name),
117 Err(e) => panic!("❌ {} FAILED: {:?}\nQuery: {}", name, e, query),
118 }
119 }
120
121 println!(
122 "\n✅ All {} complex comprehension tests passed!",
123 test_cases.len()
124 );
125 }
126
127 #[test]
128 fn test_parse_version_as_of() {
129 let q = parse("MATCH (n) RETURN n VERSION AS OF 'snap123'").unwrap();
130 match q {
131 ast::Query::TimeTravel { query, spec } => {
132 assert!(matches!(*query, ast::Query::Single(_)));
133 assert_eq!(spec, ast::TimeTravelSpec::Version("snap123".to_string()));
134 }
135 _ => panic!("Expected TimeTravel query, got {:?}", q),
136 }
137 }
138
139 #[test]
140 fn test_parse_timestamp_as_of() {
141 let q = parse("MATCH (n) RETURN n TIMESTAMP AS OF '2025-02-01T12:00:00Z'").unwrap();
142 match q {
143 ast::Query::TimeTravel { query, spec } => {
144 assert!(matches!(*query, ast::Query::Single(_)));
145 assert_eq!(
146 spec,
147 ast::TimeTravelSpec::Timestamp("2025-02-01T12:00:00Z".to_string())
148 );
149 }
150 _ => panic!("Expected TimeTravel query, got {:?}", q),
151 }
152 }
153
154 #[test]
155 fn test_parse_version_as_of_with_union() {
156 let q =
157 parse("MATCH (n:A) RETURN n UNION MATCH (m:B) RETURN m VERSION AS OF 'snap1'").unwrap();
158 match q {
159 ast::Query::TimeTravel { query, spec } => {
160 assert!(matches!(*query, ast::Query::Union { .. }));
161 assert_eq!(spec, ast::TimeTravelSpec::Version("snap1".to_string()));
162 }
163 _ => panic!("Expected TimeTravel query, got {:?}", q),
164 }
165 }
166
167 #[test]
168 fn test_parse_no_time_travel() {
169 let q = parse("MATCH (n) RETURN n").unwrap();
170 assert!(matches!(q, ast::Query::Single(_)));
171 }
172
173 #[test]
174 fn test_parse_or_relationship_types() {
175 let q = parse("MATCH (n)-[r:KNOWS|HATES]->(x) RETURN r").unwrap();
176 if let ast::Query::Single(single) = q
177 && let ast::Clause::Match(match_clause) = &single.clauses[0]
178 && let ast::PatternElement::Relationship(rel) =
179 &match_clause.pattern.paths[0].elements[1]
180 {
181 assert_eq!(rel.types, vec!["KNOWS", "HATES"]);
182 println!("Parsed types: {:?}", rel.types);
183 return;
184 }
185 panic!("Could not find relationship pattern with OR types");
186 }
187
188 #[test]
189 fn test_parse_vlp_relationship_variable() {
190 let q = parse("MATCH (a)-[r*1..1]->(b) RETURN r").unwrap();
192 if let ast::Query::Single(single) = q
193 && let ast::Clause::Match(match_clause) = &single.clauses[0]
194 && let ast::PatternElement::Relationship(rel) =
195 &match_clause.pattern.paths[0].elements[1]
196 {
197 assert_eq!(
198 rel.variable,
199 Some("r".to_string()),
200 "VLP should preserve relationship variable 'r'"
201 );
202 assert!(rel.range.is_some(), "VLP should have range");
203 let range = rel.range.as_ref().unwrap();
204 assert_eq!(range.min, Some(1));
205 assert_eq!(range.max, Some(1));
206 println!(
207 "VLP relationship: variable={:?}, range={:?}",
208 rel.variable, rel.range
209 );
210 return;
211 }
212 panic!("Could not find VLP relationship pattern");
213 }
214}
215
216#[cfg(test)]
217mod locy_tests {
218 use super::*;
219
220 #[test]
225 fn test_locy_cypher_passthrough_match_return() {
226 let program = parse_locy("MATCH (n) RETURN n").unwrap();
227 assert!(program.module.is_none());
228 assert!(program.uses.is_empty());
229 assert_eq!(program.statements.len(), 1);
230 assert!(
231 matches!(&program.statements[0], locy_ast::LocyStatement::Cypher(_)),
232 "Expected Cypher passthrough, got: {:?}",
233 program.statements[0]
234 );
235 }
236
237 #[test]
238 fn test_locy_cypher_passthrough_create() {
239 let program = parse_locy("CREATE (n:Person {name: 'Alice'})").unwrap();
240 assert_eq!(program.statements.len(), 1);
241 assert!(matches!(
242 &program.statements[0],
243 locy_ast::LocyStatement::Cypher(_)
244 ));
245 }
246
247 #[test]
248 fn test_locy_cypher_passthrough_union() {
249 let program = parse_locy("MATCH (n:A) RETURN n UNION MATCH (m:B) RETURN m").unwrap();
250 assert_eq!(program.statements.len(), 1);
251 if let locy_ast::LocyStatement::Cypher(q) = &program.statements[0] {
252 assert!(matches!(q, ast::Query::Union { .. }));
253 } else {
254 panic!("Expected Cypher union");
255 }
256 }
257
258 #[test]
259 fn test_locy_cypher_passthrough_multi_clause() {
260 let program = parse_locy("MATCH (n) WHERE n.age > 21 WITH n RETURN n.name").unwrap();
261 assert_eq!(program.statements.len(), 1);
262 if let locy_ast::LocyStatement::Cypher(ast::Query::Single(stmt)) = &program.statements[0] {
263 assert_eq!(stmt.clauses.len(), 3); } else {
265 panic!("Expected Cypher single query with 3 clauses");
266 }
267 }
268
269 #[test]
274 fn test_locy_create_rule_minimal() {
275 let program =
276 parse_locy("CREATE RULE reachable AS MATCH (a)-[:KNOWS]->(b) YIELD a, b").unwrap();
277 assert_eq!(program.statements.len(), 1);
278 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
279 assert_eq!(rule.name.parts, vec!["reachable"]);
280 assert!(rule.priority.is_none());
281 assert!(!rule.match_pattern.paths.is_empty());
282 assert!(rule.where_conditions.is_empty());
283 if let locy_ast::RuleOutput::Yield(yc) = &rule.output {
284 let items = &yc.items;
285 assert_eq!(items.len(), 2);
286 assert!(!items[0].is_key);
287 assert!(!items[1].is_key);
288 } else {
289 panic!("Expected Yield output");
290 }
291 } else {
292 panic!("Expected Rule statement");
293 }
294 }
295
296 #[test]
301 fn test_locy_rule_priority() {
302 let program =
303 parse_locy("CREATE RULE r PRIORITY 2 AS MATCH (a)-[:E]->(b) YIELD a").unwrap();
304 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
305 assert_eq!(rule.priority, Some(2));
306 } else {
307 panic!("Expected Rule");
308 }
309 }
310
311 #[test]
316 fn test_locy_is_reference_unary() {
317 let program =
318 parse_locy("CREATE RULE test AS MATCH (n) WHERE n IS suspicious YIELD n").unwrap();
319 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
320 assert_eq!(rule.where_conditions.len(), 1);
321 if let locy_ast::RuleCondition::IsReference(is_ref) = &rule.where_conditions[0] {
322 assert_eq!(is_ref.subjects, vec!["n"]);
323 assert_eq!(is_ref.rule_name.parts, vec!["suspicious"]);
324 assert!(is_ref.target.is_none());
325 assert!(!is_ref.negated);
326 } else {
327 panic!("Expected IsReference");
328 }
329 } else {
330 panic!("Expected Rule");
331 }
332 }
333
334 #[test]
339 fn test_locy_is_not_reference() {
340 let program =
341 parse_locy("CREATE RULE test AS MATCH (n) WHERE n IS NOT clean YIELD n").unwrap();
342 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
343 if let locy_ast::RuleCondition::IsReference(is_ref) = &rule.where_conditions[0] {
344 assert!(is_ref.negated);
345 assert_eq!(is_ref.rule_name.parts, vec!["clean"]);
346 } else {
347 panic!("Expected IsReference");
348 }
349 } else {
350 panic!("Expected Rule");
351 }
352 }
353
354 #[test]
355 fn test_locy_not_is_reference_prefix() {
356 let program =
357 parse_locy("CREATE RULE test AS MATCH (n) WHERE NOT n IS clean YIELD n").unwrap();
358 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
359 if let locy_ast::RuleCondition::IsReference(is_ref) = &rule.where_conditions[0] {
360 assert!(is_ref.negated);
361 assert_eq!(is_ref.rule_name.parts, vec!["clean"]);
362 } else {
363 panic!("Expected IsReference");
364 }
365 } else {
366 panic!("Expected Rule");
367 }
368 }
369
370 #[test]
375 fn test_locy_is_reference_binary() {
376 let program = parse_locy(
377 "CREATE RULE test AS MATCH (a)-[:E]->(b) WHERE a IS reachable TO b YIELD a, b",
378 )
379 .unwrap();
380 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
381 if let locy_ast::RuleCondition::IsReference(is_ref) = &rule.where_conditions[0] {
382 assert_eq!(is_ref.subjects, vec!["a"]);
383 assert_eq!(is_ref.rule_name.parts, vec!["reachable"]);
384 assert_eq!(is_ref.target, Some("b".to_string()));
385 assert!(!is_ref.negated);
386 } else {
387 panic!("Expected IsReference");
388 }
389 } else {
390 panic!("Expected Rule");
391 }
392 }
393
394 #[test]
399 fn test_locy_is_reference_tuple() {
400 let program = parse_locy(
401 "CREATE RULE test AS MATCH (x)-[:E]->(y) WHERE (x, y, cost) IS control YIELD x",
402 )
403 .unwrap();
404 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
405 if let locy_ast::RuleCondition::IsReference(is_ref) = &rule.where_conditions[0] {
406 assert_eq!(is_ref.subjects, vec!["x", "y", "cost"]);
407 assert_eq!(is_ref.rule_name.parts, vec!["control"]);
408 assert!(is_ref.target.is_none());
409 } else {
410 panic!("Expected IsReference");
411 }
412 } else {
413 panic!("Expected Rule");
414 }
415 }
416
417 #[test]
422 fn test_locy_mixed_where_conditions() {
423 let program =
424 parse_locy("CREATE RULE test AS MATCH (n) WHERE n IS reachable, n.age > 18 YIELD n")
425 .unwrap();
426 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
427 assert_eq!(rule.where_conditions.len(), 2);
428 assert!(matches!(
429 &rule.where_conditions[0],
430 locy_ast::RuleCondition::IsReference(_)
431 ));
432 assert!(matches!(
433 &rule.where_conditions[1],
434 locy_ast::RuleCondition::Expression(_)
435 ));
436 } else {
437 panic!("Expected Rule");
438 }
439 }
440
441 #[test]
446 fn test_locy_along_clause() {
447 let program = parse_locy(
448 "CREATE RULE test AS MATCH (a)-[:E]->(b) ALONG hops = prev.hops + 1 YIELD a, b, hops",
449 )
450 .unwrap();
451 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
452 assert_eq!(rule.along.len(), 1);
453 assert_eq!(rule.along[0].name, "hops");
454 if let locy_ast::LocyExpr::BinaryOp { left, op, right } = &rule.along[0].expr {
456 assert!(matches!(left.as_ref(), locy_ast::LocyExpr::PrevRef(f) if f == "hops"));
457 assert_eq!(*op, locy_ast::LocyBinaryOp::Add);
458 assert!(matches!(right.as_ref(), locy_ast::LocyExpr::Cypher(_)));
459 } else {
460 panic!("Expected BinaryOp, got: {:?}", rule.along[0].expr);
461 }
462 } else {
463 panic!("Expected Rule");
464 }
465 }
466
467 #[test]
472 fn test_locy_fold_clause() {
473 let program = parse_locy(
474 "CREATE RULE test AS MATCH (a)-[:E]->(b) FOLD total = SUM(s) YIELD a, total",
475 )
476 .unwrap();
477 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
478 assert_eq!(rule.fold.len(), 1);
479 assert_eq!(rule.fold[0].name, "total");
480 if let ast::Expr::FunctionCall { name, .. } = &rule.fold[0].aggregate {
481 assert_eq!(name.to_uppercase(), "SUM");
482 } else {
483 panic!("Expected FunctionCall, got: {:?}", rule.fold[0].aggregate);
484 }
485 } else {
486 panic!("Expected Rule");
487 }
488 }
489
490 #[test]
495 fn test_locy_best_by_clause() {
496 let program =
497 parse_locy("CREATE RULE test AS MATCH (a)-[:E]->(b) BEST BY cost ASC YIELD a, b")
498 .unwrap();
499 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
500 let best_by = rule.best_by.as_ref().unwrap();
501 assert_eq!(best_by.items.len(), 1);
502 assert!(best_by.items[0].ascending);
503 } else {
504 panic!("Expected Rule");
505 }
506 }
507
508 #[test]
509 fn test_locy_best_by_desc() {
510 let program =
511 parse_locy("CREATE RULE test AS MATCH (a)-[:E]->(b) BEST BY cost DESC YIELD a, b")
512 .unwrap();
513 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
514 let best_by = rule.best_by.as_ref().unwrap();
515 assert!(!best_by.items[0].ascending);
516 } else {
517 panic!("Expected Rule");
518 }
519 }
520
521 #[test]
526 fn test_locy_derive_forward() {
527 let program =
528 parse_locy("CREATE RULE test AS MATCH (a)-[:KNOWS]->(b) DERIVE (a)-[:FRIEND]->(b)")
529 .unwrap();
530 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
531 if let locy_ast::RuleOutput::Derive(locy_ast::DeriveClause::Patterns(pats)) =
532 &rule.output
533 {
534 assert_eq!(pats.len(), 1);
535 assert_eq!(pats[0].direction, ast::Direction::Outgoing);
536 } else {
537 panic!("Expected Derive Patterns terminal");
538 }
539 } else {
540 panic!("Expected Rule");
541 }
542 }
543
544 #[test]
549 fn test_locy_derive_merge() {
550 let program =
551 parse_locy("CREATE RULE test AS MATCH (a)-[:SAME]->(b) DERIVE MERGE a, b").unwrap();
552 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
553 if let locy_ast::RuleOutput::Derive(locy_ast::DeriveClause::Merge(a, b)) = &rule.output
554 {
555 assert_eq!(a, "a");
556 assert_eq!(b, "b");
557 } else {
558 panic!("Expected Derive Merge");
559 }
560 } else {
561 panic!("Expected Rule");
562 }
563 }
564
565 #[test]
570 fn test_locy_derive_new_backward() {
571 let program =
572 parse_locy("CREATE RULE test AS MATCH (c) DERIVE (NEW x:Country)<-[:IN]-(c)").unwrap();
573 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
574 if let locy_ast::RuleOutput::Derive(locy_ast::DeriveClause::Patterns(pats)) =
575 &rule.output
576 {
577 assert_eq!(pats[0].direction, ast::Direction::Incoming);
578 assert!(pats[0].source.is_new);
579 assert_eq!(pats[0].source.variable, "x");
580 assert_eq!(pats[0].source.labels, vec!["Country"]);
581 } else {
582 panic!("Expected Derive Patterns");
583 }
584 } else {
585 panic!("Expected Rule");
586 }
587 }
588
589 #[test]
594 fn test_locy_yield_with_key() {
595 let program =
596 parse_locy("CREATE RULE test AS MATCH (a)-[:E]->(b) YIELD KEY a, KEY b, cost").unwrap();
597 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
598 if let locy_ast::RuleOutput::Yield(yc) = &rule.output {
599 let items = &yc.items;
600 assert_eq!(items.len(), 3);
601 assert!(items[0].is_key);
602 assert!(items[1].is_key);
603 assert!(!items[2].is_key);
604 } else {
605 panic!("Expected Yield output");
606 }
607 } else {
608 panic!("Expected Rule");
609 }
610 }
611
612 #[test]
613 fn test_locy_yield_key_with_property() {
614 let program = parse_locy(
615 "CREATE RULE r AS MATCH (e:Event) YIELD KEY e.action, KEY e.outcome, n AS support",
616 )
617 .unwrap();
618 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
619 if let locy_ast::RuleOutput::Yield(yc) = &rule.output {
620 let items = &yc.items;
621 assert_eq!(items.len(), 3);
622 assert!(items[0].is_key);
623 assert_eq!(
624 items[0].expr,
625 ast::Expr::Property(
626 Box::new(ast::Expr::Variable("e".to_string())),
627 "action".to_string()
628 )
629 );
630 assert!(items[1].is_key);
631 assert_eq!(
632 items[1].expr,
633 ast::Expr::Property(
634 Box::new(ast::Expr::Variable("e".to_string())),
635 "outcome".to_string()
636 )
637 );
638 assert!(!items[2].is_key);
639 assert_eq!(items[2].alias, Some("support".to_string()));
640 } else {
641 panic!("Expected Yield output");
642 }
643 } else {
644 panic!("Expected Rule");
645 }
646 }
647
648 #[test]
649 fn test_locy_yield_key_with_alias() {
650 let program =
651 parse_locy("CREATE RULE r AS MATCH (e:Event) YIELD KEY e.action AS act").unwrap();
652 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
653 if let locy_ast::RuleOutput::Yield(yc) = &rule.output {
654 assert!(yc.items[0].is_key);
655 assert_eq!(yc.items[0].alias, Some("act".to_string()));
656 } else {
657 panic!("Expected Yield output");
658 }
659 } else {
660 panic!("Expected Rule");
661 }
662 }
663
664 #[test]
669 fn test_locy_goal_query() {
670 let program = parse_locy("QUERY reachable WHERE a.name = 'Alice' RETURN b").unwrap();
671 assert_eq!(program.statements.len(), 1);
672 if let locy_ast::LocyStatement::GoalQuery(gq) = &program.statements[0] {
673 assert_eq!(gq.rule_name.parts, vec!["reachable"]);
674 assert!(gq.return_clause.is_some());
675 } else {
676 panic!("Expected GoalQuery, got: {:?}", program.statements[0]);
677 }
678 }
679
680 #[test]
681 fn test_locy_goal_query_no_return() {
682 let program = parse_locy("QUERY reachable WHERE a.name = 'Alice'").unwrap();
683 if let locy_ast::LocyStatement::GoalQuery(gq) = &program.statements[0] {
684 assert!(gq.return_clause.is_none());
685 } else {
686 panic!("Expected GoalQuery");
687 }
688 }
689
690 #[test]
695 fn test_locy_assume_block() {
696 let program = parse_locy("ASSUME { CREATE (x:Temp) } THEN { MATCH (n) RETURN n }").unwrap();
697 assert_eq!(program.statements.len(), 1);
698 if let locy_ast::LocyStatement::AssumeBlock(ab) = &program.statements[0] {
699 assert_eq!(ab.mutations.len(), 1);
700 assert!(matches!(&ab.mutations[0], ast::Clause::Create(_)));
701 assert_eq!(ab.body.len(), 1);
702 assert!(matches!(&ab.body[0], locy_ast::LocyStatement::Cypher(_)));
703 } else {
704 panic!("Expected AssumeBlock, got: {:?}", program.statements[0]);
705 }
706 }
707
708 #[test]
713 fn test_locy_abduce_query() {
714 let program = parse_locy("ABDUCE NOT reachable WHERE a.name = 'Alice' RETURN b").unwrap();
715 assert_eq!(program.statements.len(), 1);
716 if let locy_ast::LocyStatement::AbduceQuery(aq) = &program.statements[0] {
717 assert!(aq.negated);
718 assert_eq!(aq.rule_name.parts, vec!["reachable"]);
719 assert!(aq.return_clause.is_some());
720 } else {
721 panic!("Expected AbduceQuery, got: {:?}", program.statements[0]);
722 }
723 }
724
725 #[test]
726 fn test_locy_abduce_query_positive() {
727 let program = parse_locy("ABDUCE reachable WHERE a.name = 'Bob'").unwrap();
728 if let locy_ast::LocyStatement::AbduceQuery(aq) = &program.statements[0] {
729 assert!(!aq.negated);
730 assert!(aq.return_clause.is_none());
731 } else {
732 panic!("Expected AbduceQuery");
733 }
734 }
735
736 #[test]
741 fn test_locy_explain_rule() {
742 let program = parse_locy("EXPLAIN RULE reachable WHERE a.name = 'Alice'").unwrap();
743 assert_eq!(program.statements.len(), 1);
744 if let locy_ast::LocyStatement::ExplainRule(eq) = &program.statements[0] {
745 assert_eq!(eq.rule_name.parts, vec!["reachable"]);
746 assert!(eq.return_clause.is_none());
747 } else {
748 panic!("Expected ExplainRule, got: {:?}", program.statements[0]);
749 }
750 }
751
752 #[test]
757 fn test_locy_module_use() {
758 let program =
759 parse_locy("MODULE acme.compliance\nUSE acme.common\nMATCH (n) RETURN n").unwrap();
760 assert!(program.module.is_some());
761 assert_eq!(
762 program.module.as_ref().unwrap().name.parts,
763 vec!["acme", "compliance"]
764 );
765 assert_eq!(program.uses.len(), 1);
766 assert_eq!(program.uses[0].name.parts, vec!["acme", "common"]);
767 assert_eq!(program.statements.len(), 1);
768 assert!(matches!(
769 &program.statements[0],
770 locy_ast::LocyStatement::Cypher(_)
771 ));
772 }
773
774 #[test]
775 fn test_locy_module_multiple_uses() {
776 let program =
777 parse_locy("MODULE mymod\nUSE dep1\nUSE dep2.sub\nMATCH (n) RETURN n").unwrap();
778 assert_eq!(program.uses.len(), 2);
779 assert_eq!(program.uses[0].name.parts, vec!["dep1"]);
780 assert_eq!(program.uses[1].name.parts, vec!["dep2", "sub"]);
781 }
782
783 #[test]
788 fn test_locy_complex_rule_all_clauses() {
789 let program = parse_locy(
790 "CREATE RULE shortest_path PRIORITY 1 AS \
791 MATCH (a)-[:EDGE {weight: w}]->(b) \
792 WHERE a IS reachable TO b, w > 0 \
793 ALONG dist = prev.dist + w \
794 FOLD total = SUM(dist) \
795 BEST BY dist ASC \
796 YIELD KEY a, KEY b, dist",
797 )
798 .unwrap();
799 if let locy_ast::LocyStatement::Rule(rule) = &program.statements[0] {
800 assert_eq!(rule.name.parts, vec!["shortest_path"]);
801 assert_eq!(rule.priority, Some(1));
802 assert_eq!(rule.where_conditions.len(), 2);
803 assert_eq!(rule.along.len(), 1);
804 assert_eq!(rule.fold.len(), 1);
805 let best_by = rule.best_by.as_ref().unwrap();
806 assert_eq!(best_by.items.len(), 1);
807 assert!(best_by.items[0].ascending);
808 if let locy_ast::RuleOutput::Yield(yc) = &rule.output {
809 let items = &yc.items;
810 assert_eq!(items.len(), 3);
811 assert!(items[0].is_key);
812 assert!(items[1].is_key);
813 assert!(!items[2].is_key);
814 } else {
815 panic!("Expected Yield");
816 }
817 } else {
818 panic!("Expected Rule");
819 }
820 }
821
822 #[test]
827 fn test_locy_derive_command() {
828 let program = parse_locy("DERIVE reachable WHERE a.name = 'Alice'").unwrap();
829 assert_eq!(program.statements.len(), 1);
830 if let locy_ast::LocyStatement::DeriveCommand(dc) = &program.statements[0] {
831 assert_eq!(dc.rule_name.parts, vec!["reachable"]);
832 assert!(dc.where_expr.is_some());
833 } else {
834 panic!("Expected DeriveCommand, got: {:?}", program.statements[0]);
835 }
836 }
837
838 #[test]
839 fn test_locy_derive_command_no_where() {
840 let program = parse_locy("DERIVE reachable").unwrap();
841 if let locy_ast::LocyStatement::DeriveCommand(dc) = &program.statements[0] {
842 assert!(dc.where_expr.is_none());
843 } else {
844 panic!("Expected DeriveCommand");
845 }
846 }
847}