gqlite 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/db-index/Cargo.toml +2 -2
  4. data/ext/gqlitedb/Cargo.toml +0 -2
  5. data/ext/gqlitedb/src/aggregators.rs +28 -0
  6. data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
  7. data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
  8. data/ext/gqlitedb/src/compiler.rs +506 -48
  9. data/ext/gqlitedb/src/connection.rs +14 -2
  10. data/ext/gqlitedb/src/consts.rs +1 -1
  11. data/ext/gqlitedb/src/error.rs +31 -0
  12. data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
  13. data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
  14. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  15. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  16. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  17. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  18. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  19. data/ext/gqlitedb/src/planner.rs +231 -6
  20. data/ext/gqlitedb/src/query_result.rs +19 -4
  21. data/ext/gqlitedb/src/store/redb.rs +1 -1
  22. data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
  23. data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
  24. data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
  25. data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
  26. data/ext/gqlitedb/src/tests/connection.rs +101 -0
  27. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  28. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  29. data/ext/gqlitedb/src/tests/planner.rs +433 -2
  30. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  31. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  32. data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
  33. data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
  34. data/ext/gqliterb/Cargo.toml +0 -1
  35. data/ext/gqlparser/src/oc/ast.rs +72 -3
  36. data/ext/gqlparser/src/oc/error.rs +18 -0
  37. data/ext/gqlparser/src/oc/lexer.rs +157 -2
  38. data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
  39. data/ext/gqlparser/src/oc/parser.rs +1312 -182
  40. data/ext/gqlparser/src/oc/string.rs +172 -0
  41. data/ext/gqlparser/src/oc.rs +1 -0
  42. data/ext/graphcore/src/value/value_map.rs +2 -2
  43. data/ext/graphcore/src/value.rs +13 -1
  44. metadata +6 -2
  45. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -110,3 +110,1228 @@ fn test_parse_multiple_vector_index_creation()
110
110
  expected.extend(ast::create_vector_index("idx2"));
111
111
  compare_ast(ast, expected)
112
112
  }
113
+
114
+ #[test]
115
+ fn test_parse_edge_pattern_with_predicate()
116
+ {
117
+ let ast = parse_query("MATCH (a)-[l:LINK WHERE l.distance < 10]->(b) RETURN l");
118
+ // Find the MATCH statement
119
+ let match_stmt = ast
120
+ .iter()
121
+ .find_map(|stmt| {
122
+ if let oc::ast::Statement::Match(m) = stmt
123
+ {
124
+ Some(m)
125
+ }
126
+ else
127
+ {
128
+ None
129
+ }
130
+ })
131
+ .expect("Expected to find a MATCH statement");
132
+
133
+ assert_eq!(match_stmt.patterns.len(), 1);
134
+ if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
135
+ {
136
+ assert!(
137
+ edge_pattern.predicate.is_some(),
138
+ "Expected predicate to be Some"
139
+ );
140
+ }
141
+ else
142
+ {
143
+ panic!("Expected Pattern::Edge");
144
+ }
145
+ }
146
+
147
+ #[test]
148
+ fn test_parse_edge_pattern_with_properties_and_predicate()
149
+ {
150
+ let ast = parse_query("MATCH (a)-[l:LINK {weight: 1} WHERE l.distance < 10]->(b) RETURN l");
151
+ // Find the MATCH statement
152
+ let match_stmt = ast
153
+ .iter()
154
+ .find_map(|stmt| {
155
+ if let oc::ast::Statement::Match(m) = stmt
156
+ {
157
+ Some(m)
158
+ }
159
+ else
160
+ {
161
+ None
162
+ }
163
+ })
164
+ .expect("Expected to find a MATCH statement");
165
+
166
+ assert_eq!(match_stmt.patterns.len(), 1);
167
+ if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
168
+ {
169
+ assert!(
170
+ edge_pattern.properties.is_some(),
171
+ "Expected properties to be Some"
172
+ );
173
+ assert!(
174
+ edge_pattern.predicate.is_some(),
175
+ "Expected predicate to be Some"
176
+ );
177
+ }
178
+ else
179
+ {
180
+ panic!("Expected Pattern::Edge");
181
+ }
182
+ }
183
+
184
+ #[test]
185
+ fn test_parse_edge_pattern_without_predicate()
186
+ {
187
+ let ast = parse_query("MATCH (a)-[l]->(b) RETURN l");
188
+ // Find the MATCH statement
189
+ let match_stmt = ast
190
+ .iter()
191
+ .find_map(|stmt| {
192
+ if let oc::ast::Statement::Match(m) = stmt
193
+ {
194
+ Some(m)
195
+ }
196
+ else
197
+ {
198
+ None
199
+ }
200
+ })
201
+ .expect("Expected to find a MATCH statement");
202
+
203
+ assert_eq!(match_stmt.patterns.len(), 1);
204
+ if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
205
+ {
206
+ assert!(
207
+ edge_pattern.predicate.is_none(),
208
+ "Expected predicate to be None"
209
+ );
210
+ }
211
+ else
212
+ {
213
+ panic!("Expected Pattern::Edge");
214
+ }
215
+ }
216
+
217
+ #[test]
218
+ fn test_parse_path_pattern_with_predicate()
219
+ {
220
+ let ast = parse_query("MATCH p = (a)-[l:LINK WHERE l.distance < 10]->(b) RETURN p");
221
+ // Find the MATCH statement
222
+ let match_stmt = ast
223
+ .iter()
224
+ .find_map(|stmt| {
225
+ if let oc::ast::Statement::Match(m) = stmt
226
+ {
227
+ Some(m)
228
+ }
229
+ else
230
+ {
231
+ None
232
+ }
233
+ })
234
+ .expect("Expected to find a MATCH statement");
235
+
236
+ assert_eq!(match_stmt.patterns.len(), 1);
237
+ if let oc::ast::Pattern::Path(path_pattern) = &match_stmt.patterns[0]
238
+ {
239
+ assert!(
240
+ path_pattern.edge.predicate.is_some(),
241
+ "Expected path edge predicate to be Some"
242
+ );
243
+ }
244
+ else
245
+ {
246
+ panic!("Expected Pattern::Path");
247
+ }
248
+ }
249
+
250
+ #[test]
251
+ fn test_parse_create_with_edge_predicate_fails()
252
+ {
253
+ let result = parse("CREATE (a)-[l:LINK WHERE l.x = 1]->(b)");
254
+ assert!(
255
+ result.is_err(),
256
+ "Expected parsing CREATE with edge predicate to fail"
257
+ );
258
+ if let Err(ref err) = result
259
+ {
260
+ // Verify the error is CreatingWithPredicate
261
+ assert!(
262
+ err.to_string().contains("CreatingWithPredicate"),
263
+ "Expected CreatingWithPredicate error, got: {}",
264
+ err
265
+ );
266
+ }
267
+ }
268
+
269
+ #[test]
270
+ fn test_parse_create_with_destination_node_predicate_fails()
271
+ {
272
+ let result = parse("CREATE (a)-[:LINK]->(b WHERE b.x = 1)");
273
+ assert!(
274
+ result.is_err(),
275
+ "Expected parsing CREATE with node predicate to fail"
276
+ );
277
+ if let Err(ref err) = result
278
+ {
279
+ // Verify the error is CreatingWithPredicate
280
+ assert!(
281
+ err.to_string().contains("CreatingWithPredicate"),
282
+ "Expected CreatingWithPredicate error, got: {}",
283
+ err
284
+ );
285
+ }
286
+ }
287
+
288
+ #[test]
289
+ fn test_parse_conflicting_quantifiers_fails()
290
+ {
291
+ let result = parse("MATCH (a)-[:R*1..3]->{4,5}(b) RETURN a");
292
+ assert!(
293
+ result.is_err(),
294
+ "Expected parsing with both legacy and new quantifiers to fail"
295
+ );
296
+ if let Err(ref err) = result
297
+ {
298
+ // Verify the error is ConflictingQuantifiers
299
+ assert!(
300
+ err.to_string().contains("ConflictingQuantifiers"),
301
+ "Expected ConflictingQuantifiers error, got: {}",
302
+ err
303
+ );
304
+ }
305
+ }
306
+
307
+ #[test]
308
+ fn test_parse_legacy_quantifier_only_succeeds()
309
+ {
310
+ let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
311
+ assert!(
312
+ result.is_ok(),
313
+ "Expected parsing with legacy quantifier only to succeed, got: {:?}",
314
+ result.err()
315
+ );
316
+ }
317
+
318
+ #[test]
319
+ fn test_parse_new_quantifier_only_succeeds()
320
+ {
321
+ let result = parse("MATCH (a)-[:R]->{4,5}(b) RETURN a");
322
+ assert!(
323
+ result.is_ok(),
324
+ "Expected parsing with new quantifier only to succeed, got: {:?}",
325
+ result.err()
326
+ );
327
+ }
328
+
329
+ #[test]
330
+ fn test_parse_legacy_quantifier_emits_quantified_path()
331
+ {
332
+ let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
333
+ assert!(
334
+ result.is_ok(),
335
+ "Expected parsing legacy quantifier to succeed, got: {:?}",
336
+ result.err()
337
+ );
338
+ if let Ok(queries) = result
339
+ {
340
+ assert!(!queries.is_empty(), "Expected at least one query");
341
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
342
+ {
343
+ assert!(!statements.is_empty(), "Expected at least one statement");
344
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
345
+ {
346
+ assert!(
347
+ !match_clause.patterns.is_empty(),
348
+ "Expected patterns in MATCH"
349
+ );
350
+ // Find the QuantifiedPath pattern
351
+ let has_quantified_path = match_clause.patterns.iter().any(|p| {
352
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
353
+ if qp.variable.is_none()
354
+ && qp.quantifier == Some(1..3)
355
+ && qp.group.len() == 1
356
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
357
+ });
358
+ assert!(
359
+ has_quantified_path,
360
+ "Expected QuantifiedPath pattern with quantifier Some(1..3) and group containing single Edge"
361
+ );
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ #[test]
368
+ fn test_parse_new_quantifier_emits_quantified_path()
369
+ {
370
+ let result = parse("MATCH (a)-[:R]->{1,3}(b) RETURN a");
371
+ assert!(
372
+ result.is_ok(),
373
+ "Expected parsing new quantifier to succeed, got: {:?}",
374
+ result.err()
375
+ );
376
+ if let Ok(queries) = result
377
+ {
378
+ assert!(!queries.is_empty(), "Expected at least one query");
379
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
380
+ {
381
+ assert!(!statements.is_empty(), "Expected at least one statement");
382
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
383
+ {
384
+ assert!(
385
+ !match_clause.patterns.is_empty(),
386
+ "Expected patterns in MATCH"
387
+ );
388
+ // Find the QuantifiedPath pattern
389
+ let has_quantified_path = match_clause.patterns.iter().any(|p| {
390
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
391
+ if qp.variable.is_none()
392
+ && qp.quantifier == Some(1..3)
393
+ && qp.group.len() == 1
394
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
395
+ });
396
+ assert!(
397
+ has_quantified_path,
398
+ "Expected QuantifiedPath pattern with quantifier Some(1..3) and group containing single Edge"
399
+ );
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ #[test]
406
+ fn test_parse_quantified_edge_chain_preserves_continuity()
407
+ {
408
+ let result = parse("MATCH (a)-[:R]->(b)-[:S*1..2]->(c) RETURN a");
409
+ assert!(
410
+ result.is_ok(),
411
+ "Expected parsing edge chain with quantified edge to succeed, got: {:?}",
412
+ result.err()
413
+ );
414
+ if let Ok(queries) = result
415
+ {
416
+ assert!(!queries.is_empty(), "Expected at least one query");
417
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
418
+ {
419
+ assert!(!statements.is_empty(), "Expected at least one statement");
420
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
421
+ {
422
+ assert!(
423
+ !match_clause.patterns.is_empty(),
424
+ "Expected patterns in MATCH"
425
+ );
426
+ // Count patterns: should have Edge(R), QuantifiedPath(S)
427
+ let has_edge = match_clause
428
+ .patterns
429
+ .iter()
430
+ .any(|p| matches!(p, gqlparser::oc::ast::Pattern::Edge(_)));
431
+ let has_quantified_path = match_clause.patterns.iter().any(|p| {
432
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
433
+ if qp.variable.is_none()
434
+ && qp.quantifier == Some(1..2)
435
+ && qp.group.len() == 1
436
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
437
+ });
438
+ assert!(
439
+ has_edge && has_quantified_path,
440
+ "Expected chain to contain ordinary Edge and QuantifiedPath pattern"
441
+ );
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ #[test]
448
+ fn test_parse_path_variable_quantified_legacy_quantifier()
449
+ {
450
+ let result = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p");
451
+ assert!(
452
+ result.is_ok(),
453
+ "Expected parsing path variable with legacy quantifier to succeed, got: {:?}",
454
+ result.err()
455
+ );
456
+ if let Ok(queries) = result
457
+ {
458
+ assert!(!queries.is_empty(), "Expected at least one query");
459
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
460
+ {
461
+ assert!(!statements.is_empty(), "Expected at least one statement");
462
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
463
+ {
464
+ assert!(
465
+ !match_clause.patterns.is_empty(),
466
+ "Expected patterns in MATCH"
467
+ );
468
+ // Find the QuantifiedPath pattern with variable p
469
+ let has_quantified_path_with_variable = match_clause.patterns.iter().any(|p| {
470
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
471
+ if qp.variable.is_some()
472
+ && qp.variable.as_ref().unwrap().name() == "p"
473
+ && qp.quantifier == Some(1..3)
474
+ && qp.group.len() == 1
475
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
476
+ });
477
+ assert!(
478
+ has_quantified_path_with_variable,
479
+ "Expected QuantifiedPath pattern with variable Some(p) and quantifier Some(1..3)"
480
+ );
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ #[test]
487
+ fn test_parse_path_variable_quantified_new_quantifier()
488
+ {
489
+ let result = parse("MATCH p = (a)-[:R]->{1,3}(b) RETURN p");
490
+ assert!(
491
+ result.is_ok(),
492
+ "Expected parsing path variable with new quantifier to succeed, got: {:?}",
493
+ result.err()
494
+ );
495
+ if let Ok(queries) = result
496
+ {
497
+ assert!(!queries.is_empty(), "Expected at least one query");
498
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
499
+ {
500
+ assert!(!statements.is_empty(), "Expected at least one statement");
501
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
502
+ {
503
+ assert!(
504
+ !match_clause.patterns.is_empty(),
505
+ "Expected patterns in MATCH"
506
+ );
507
+ // Find the QuantifiedPath pattern with variable p
508
+ let has_quantified_path_with_variable = match_clause.patterns.iter().any(|p| {
509
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
510
+ if qp.variable.is_some()
511
+ && qp.variable.as_ref().unwrap().name() == "p"
512
+ && qp.quantifier == Some(1..3)
513
+ && qp.group.len() == 1
514
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
515
+ });
516
+ assert!(
517
+ has_quantified_path_with_variable,
518
+ "Expected QuantifiedPath pattern with variable Some(p) and quantifier Some(1..3)"
519
+ );
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ #[test]
526
+ fn test_parse_quantified_path_source_destination_populated()
527
+ {
528
+ let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
529
+ assert!(
530
+ result.is_ok(),
531
+ "Expected parsing quantified path to succeed, got: {:?}",
532
+ result.err()
533
+ );
534
+ if let Ok(queries) = result
535
+ {
536
+ assert!(!queries.is_empty(), "Expected at least one query");
537
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
538
+ {
539
+ assert!(!statements.is_empty(), "Expected at least one statement");
540
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
541
+ {
542
+ assert!(
543
+ !match_clause.patterns.is_empty(),
544
+ "Expected patterns in MATCH"
545
+ );
546
+ // Find the QuantifiedPath pattern and verify source/destination are populated
547
+ let found_qp_with_endpoints = match_clause.patterns.iter().any(|p| {
548
+ if let gqlparser::oc::ast::Pattern::QuantifiedPath(qp) = p
549
+ {
550
+ // Verify that source and destination are populated
551
+ let qp_source_var = &qp.source.variable;
552
+ let qp_destination_var = &qp.destination.variable;
553
+
554
+ // Get the edge from the group to compare
555
+ if let Some(gqlparser::oc::ast::Pattern::Edge(edge)) = qp.group.first()
556
+ {
557
+ // The source of the quantified path should match the edge's source
558
+ // The destination of the quantified path should match the edge's destination
559
+ let source_matches = qp_source_var == &edge.source.variable;
560
+ let destination_matches = qp_destination_var == &edge.destination.variable;
561
+ return source_matches && destination_matches;
562
+ }
563
+ }
564
+ false
565
+ });
566
+ assert!(
567
+ found_qp_with_endpoints,
568
+ "Expected QuantifiedPath with populated source/destination matching edge endpoints"
569
+ );
570
+ }
571
+ }
572
+ }
573
+ }
574
+
575
+ #[test]
576
+ fn test_parse_path_variable_quantified_group_binding()
577
+ {
578
+ let result = parse("MATCH p = (a) ((n)-[r:KNOWS]->(m)){1,5} (b) RETURN p");
579
+ assert!(
580
+ result.is_ok(),
581
+ "Expected parsing path variable with quantified group to succeed, got: {:?}",
582
+ result.err()
583
+ );
584
+ if let Ok(queries) = result
585
+ {
586
+ assert!(!queries.is_empty(), "Expected at least one query");
587
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
588
+ {
589
+ assert!(!statements.is_empty(), "Expected at least one statement");
590
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
591
+ {
592
+ assert!(
593
+ !match_clause.patterns.is_empty(),
594
+ "Expected patterns in MATCH"
595
+ );
596
+ let has_group_path_variable = match_clause.patterns.iter().any(|p| {
597
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
598
+ if qp.variable.as_ref().is_some_and(|v| v.name() == "p")
599
+ && qp.group.len() == 1
600
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_))
601
+ && qp.source.variable.as_ref().is_some_and(|v| v.name() == "a")
602
+ && qp.destination.variable.as_ref().is_some_and(|v| v.name() == "b"))
603
+ });
604
+ assert!(
605
+ has_group_path_variable,
606
+ "Expected QuantifiedPath pattern with variable Some(p), one inner edge, and anchors a/b"
607
+ );
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ #[test]
614
+ fn test_parse_group_pattern_without_path_variable()
615
+ {
616
+ let result = parse("MATCH (a) ((n)-[r:KNOWS]->(m)){1,5} (b) RETURN a");
617
+ assert!(
618
+ result.is_ok(),
619
+ "Expected parsing quantified group to succeed, got: {:?}",
620
+ result.err()
621
+ );
622
+ if let Ok(queries) = result
623
+ {
624
+ assert!(!queries.is_empty(), "Expected at least one query");
625
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
626
+ {
627
+ assert!(!statements.is_empty(), "Expected at least one statement");
628
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
629
+ {
630
+ assert!(
631
+ !match_clause.patterns.is_empty(),
632
+ "Expected patterns in MATCH"
633
+ );
634
+ let has_group = match_clause.patterns.iter().any(|p| {
635
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
636
+ if qp.variable.is_none()
637
+ && qp.group.len() == 1
638
+ && qp.source.variable.as_ref().is_some_and(|v| v.name() == "a")
639
+ && qp.destination.variable.as_ref().is_some_and(|v| v.name() == "b")
640
+ && matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
641
+ });
642
+ assert!(
643
+ has_group,
644
+ "Expected QuantifiedPath pattern for quantified group with anchors a/b and one inner edge"
645
+ );
646
+ }
647
+ }
648
+ }
649
+ }
650
+
651
+ #[test]
652
+ fn test_parse_group_pattern_with_where_clause()
653
+ {
654
+ let result = parse("MATCH (a) ((n)-[r:KNOWS]->(m) WHERE n.x > 1){1,5} (b) RETURN a");
655
+ assert!(
656
+ result.is_ok(),
657
+ "Expected parsing quantified group with WHERE to succeed, got: {:?}",
658
+ result.err()
659
+ );
660
+ if let Ok(queries) = result
661
+ {
662
+ assert!(!queries.is_empty(), "Expected at least one query");
663
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
664
+ {
665
+ assert!(!statements.is_empty(), "Expected at least one statement");
666
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
667
+ {
668
+ let has_group_where = match_clause.patterns.iter().any(|p| {
669
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
670
+ if qp.group_where.is_some())
671
+ });
672
+ assert!(
673
+ has_group_where,
674
+ "Expected QuantifiedPath pattern with group_where Some(_)"
675
+ );
676
+ }
677
+ }
678
+ }
679
+ }
680
+
681
+ #[test]
682
+ fn test_parse_group_pattern_multiple_inner_edges()
683
+ {
684
+ let result = parse("MATCH (a) ((n)-[:R]->(m)-[:S]->(k)){1,3} (b) RETURN a");
685
+ assert!(
686
+ result.is_ok(),
687
+ "Expected parsing quantified group with multiple inner edges to succeed, got: {:?}",
688
+ result.err()
689
+ );
690
+ if let Ok(queries) = result
691
+ {
692
+ assert!(!queries.is_empty(), "Expected at least one query");
693
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
694
+ {
695
+ assert!(!statements.is_empty(), "Expected at least one statement");
696
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
697
+ {
698
+ let has_two_edge_group = match_clause.patterns.iter().any(|p| {
699
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
700
+ if qp.group.len() == 2)
701
+ });
702
+ assert!(
703
+ has_two_edge_group,
704
+ "Expected QuantifiedPath pattern whose inner group contains two edges"
705
+ );
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ #[test]
712
+ fn test_parse_group_pattern_chain_continuation()
713
+ {
714
+ let result = parse("MATCH (a) ((n)-[:R]->(m)){1,3} (b)-[:S]->(c) RETURN a");
715
+ assert!(
716
+ result.is_ok(),
717
+ "Expected parsing quantified group followed by edge continuation to succeed, got: {:?}",
718
+ result.err()
719
+ );
720
+ if let Ok(queries) = result
721
+ {
722
+ assert!(!queries.is_empty(), "Expected at least one query");
723
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
724
+ {
725
+ assert!(!statements.is_empty(), "Expected at least one statement");
726
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
727
+ {
728
+ assert_eq!(
729
+ match_clause.patterns.len(),
730
+ 2,
731
+ "Expected exactly two top-level patterns: quantified group and following edge"
732
+ );
733
+ let quant = match_clause.patterns.first().and_then(|p| match p
734
+ {
735
+ gqlparser::oc::ast::Pattern::QuantifiedPath(qp) => Some(qp),
736
+ _ => None,
737
+ });
738
+ let edge = match_clause.patterns.get(1).and_then(|p| match p
739
+ {
740
+ gqlparser::oc::ast::Pattern::Edge(e) => Some(e),
741
+ _ => None,
742
+ });
743
+ assert!(
744
+ quant.is_some(),
745
+ "Expected first top-level pattern to be QuantifiedPath"
746
+ );
747
+ assert!(
748
+ edge.is_some(),
749
+ "Expected second top-level pattern to be Edge"
750
+ );
751
+ let quant = quant.unwrap();
752
+ let edge = edge.unwrap();
753
+ assert_eq!(
754
+ quant
755
+ .destination
756
+ .variable
757
+ .as_ref()
758
+ .map(|v| v.name().clone()),
759
+ Some("b".to_string()),
760
+ "Expected quantified group destination to be b"
761
+ );
762
+ assert_eq!(
763
+ edge.source.variable.as_ref().map(|v| v.name().clone()),
764
+ Some("b".to_string()),
765
+ "Expected following edge source to continue from group destination b"
766
+ );
767
+ }
768
+ }
769
+ }
770
+ }
771
+
772
+ #[test]
773
+ fn test_parse_group_pattern_quantifier_variants()
774
+ {
775
+ for (suffix, expected) in [
776
+ ("{1,5}", Some(1..5)),
777
+ ("{3}", Some(3..3)),
778
+ ("{2,}", Some(2..usize::MAX)),
779
+ ("{,4}", Some(0..4)),
780
+ ("+", Some(1..usize::MAX)),
781
+ ("*", Some(0..usize::MAX)),
782
+ ]
783
+ {
784
+ let query = format!("MATCH (a) ((n)-[:R]->(m)){suffix} (b) RETURN a");
785
+ let result = parse(&query);
786
+ assert!(
787
+ result.is_ok(),
788
+ "Expected parsing quantified group with quantifier {suffix} to succeed, got: {:?}",
789
+ result.err()
790
+ );
791
+ if let Ok(queries) = result
792
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
793
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
794
+ {
795
+ let found = match_clause.patterns.iter().any(|p| {
796
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
797
+ if qp.quantifier == expected)
798
+ });
799
+ assert!(
800
+ found,
801
+ "Expected QuantifiedPath pattern with quantifier {:?} for suffix {suffix}",
802
+ expected
803
+ );
804
+ }
805
+ }
806
+ }
807
+
808
+ #[test]
809
+ fn test_parse_path_variable_unquantified_edge_regression()
810
+ {
811
+ let result = parse("MATCH p = (a)-[:R]->(b) RETURN p");
812
+ assert!(
813
+ result.is_ok(),
814
+ "Expected parsing path variable with unquantified edge to succeed, got: {:?}",
815
+ result.err()
816
+ );
817
+ if let Ok(queries) = result
818
+ {
819
+ assert!(!queries.is_empty(), "Expected at least one query");
820
+ if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
821
+ {
822
+ assert!(!statements.is_empty(), "Expected at least one statement");
823
+ if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
824
+ {
825
+ assert!(
826
+ !match_clause.patterns.is_empty(),
827
+ "Expected patterns in MATCH"
828
+ );
829
+ // Find the Path pattern (not QuantifiedPath) with variable p
830
+ let has_path_pattern = match_clause.patterns.iter().any(|p| {
831
+ matches!(p, gqlparser::oc::ast::Pattern::Path(path_pattern)
832
+ if path_pattern.variable.name() == "p")
833
+ });
834
+ assert!(
835
+ has_path_pattern,
836
+ "Expected Path pattern with variable p (unquantified edges still use PathPattern)"
837
+ );
838
+ }
839
+ }
840
+ }
841
+ }
842
+
843
+ #[test]
844
+ fn test_parse_path_variable_mixed_quantified_unquantified_chain()
845
+ {
846
+ let result = parse("MATCH p = (a)-[:R]->(b)-[:S*1..3]->(c) RETURN p");
847
+ assert!(
848
+ result.is_ok(),
849
+ "Expected parsing mixed quantified/unquantified chain to succeed, got: {:?}",
850
+ result.err()
851
+ );
852
+ // Mixed chains with quantified and unquantified edges are now accepted
853
+ // and combined into a single PathPattern (quantifiers on individual edges are
854
+ // preserved as QuantifiedPath nodes internally, but the path variable combines
855
+ // them). Proper semantic handling of mixed chains is deferred to Milestone 3.
856
+ }
857
+
858
+ // =============================================================================
859
+ // Path Selector Tests (WI #655)
860
+ // =============================================================================
861
+
862
+ // Test 1: Every PathSelector variant applied to a quantified pattern
863
+ // (confirms the qp.selector = selector branch)
864
+
865
+ #[test]
866
+ fn test_parse_selector_all_with_quantified_pattern()
867
+ {
868
+ let result = parse("MATCH p = ALL (a)-[:R*1..3]->(b) RETURN p");
869
+ assert!(
870
+ result.is_ok(),
871
+ "Expected ALL selector with quantified pattern to succeed, got: {:?}",
872
+ result.err()
873
+ );
874
+ if let Ok(queries) = result
875
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
876
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
877
+ {
878
+ let has_all_selector = match_clause.patterns.iter().any(|p| {
879
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
880
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::All))
881
+ });
882
+ assert!(
883
+ has_all_selector,
884
+ "Expected QuantifiedPath with PathSelector::All"
885
+ );
886
+ }
887
+ }
888
+
889
+ #[test]
890
+ fn test_parse_selector_any_with_quantified_pattern()
891
+ {
892
+ let result = parse("MATCH p = ANY (a)-[:R*1..3]->(b) RETURN p");
893
+ assert!(
894
+ result.is_ok(),
895
+ "Expected ANY selector with quantified pattern to succeed, got: {:?}",
896
+ result.err()
897
+ );
898
+ if let Ok(queries) = result
899
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
900
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
901
+ {
902
+ let has_any_selector = match_clause.patterns.iter().any(|p| {
903
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
904
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Any))
905
+ });
906
+ assert!(
907
+ has_any_selector,
908
+ "Expected QuantifiedPath with PathSelector::Any"
909
+ );
910
+ }
911
+ }
912
+
913
+ #[test]
914
+ fn test_parse_selector_all_shortest_with_quantified_pattern()
915
+ {
916
+ let result = parse("MATCH p = ALL SHORTEST (a)-[:R*1..3]->(b) RETURN p");
917
+ assert!(
918
+ result.is_ok(),
919
+ "Expected ALL SHORTEST selector with quantified pattern to succeed, got: {:?}",
920
+ result.err()
921
+ );
922
+ if let Ok(queries) = result
923
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
924
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
925
+ {
926
+ let has_all_shortest_selector = match_clause.patterns.iter().any(|p| {
927
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
928
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::AllShortest))
929
+ });
930
+ assert!(
931
+ has_all_shortest_selector,
932
+ "Expected QuantifiedPath with PathSelector::AllShortest"
933
+ );
934
+ }
935
+ }
936
+
937
+ #[test]
938
+ fn test_parse_selector_shortest_k_with_quantified_pattern()
939
+ {
940
+ let result = parse("MATCH p = SHORTEST 2 (a)-[:R*1..3]->(b) RETURN p");
941
+ assert!(
942
+ result.is_ok(),
943
+ "Expected SHORTEST 2 selector with quantified pattern to succeed, got: {:?}",
944
+ result.err()
945
+ );
946
+ if let Ok(queries) = result
947
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
948
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
949
+ {
950
+ let has_shortest_selector = match_clause.patterns.iter().any(|p| {
951
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
952
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Shortest(_)))
953
+ });
954
+ assert!(
955
+ has_shortest_selector,
956
+ "Expected QuantifiedPath with PathSelector::Shortest(_)"
957
+ );
958
+ }
959
+ }
960
+
961
+ #[test]
962
+ fn test_parse_selector_shortest_groups_with_quantified_pattern()
963
+ {
964
+ let result = parse("MATCH p = SHORTEST 3 GROUPS (a)-[:R*1..3]->(b) RETURN p");
965
+ assert!(
966
+ result.is_ok(),
967
+ "Expected SHORTEST 3 GROUPS selector with quantified pattern to succeed, got: {:?}",
968
+ result.err()
969
+ );
970
+ if let Ok(queries) = result
971
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
972
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
973
+ {
974
+ let has_shortest_groups_selector = match_clause.patterns.iter().any(|p| {
975
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
976
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::ShortestGroups(_)))
977
+ });
978
+ assert!(
979
+ has_shortest_groups_selector,
980
+ "Expected QuantifiedPath with PathSelector::ShortestGroups(_)"
981
+ );
982
+ }
983
+ }
984
+
985
+ // Test 2: Every PathSelector variant applied to a plain, non-quantified edge
986
+ // (confirms the scoping-wrapper branch, quantifier: None)
987
+
988
+ #[test]
989
+ fn test_parse_selector_all_with_plain_edge()
990
+ {
991
+ let result = parse("MATCH ALL (a)-[:R]->(b) RETURN a");
992
+ assert!(
993
+ result.is_ok(),
994
+ "Expected ALL selector with plain edge to succeed, got: {:?}",
995
+ result.err()
996
+ );
997
+ if let Ok(queries) = result
998
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
999
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
1000
+ {
1001
+ let has_all_selector = match_clause.patterns.iter().any(|p| {
1002
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
1003
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::All)
1004
+ && qp.quantifier.is_none())
1005
+ });
1006
+ assert!(
1007
+ has_all_selector,
1008
+ "Expected QuantifiedPath with PathSelector::All and quantifier: None"
1009
+ );
1010
+ }
1011
+ }
1012
+
1013
+ #[test]
1014
+ fn test_parse_selector_any_with_plain_edge()
1015
+ {
1016
+ let result = parse("MATCH ANY (a)-[:R]->(b) RETURN a");
1017
+ assert!(
1018
+ result.is_ok(),
1019
+ "Expected ANY selector with plain edge to succeed, got: {:?}",
1020
+ result.err()
1021
+ );
1022
+ if let Ok(queries) = result
1023
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
1024
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
1025
+ {
1026
+ let has_any_selector = match_clause.patterns.iter().any(|p| {
1027
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
1028
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Any)
1029
+ && qp.quantifier.is_none())
1030
+ });
1031
+ assert!(
1032
+ has_any_selector,
1033
+ "Expected QuantifiedPath with PathSelector::Any and quantifier: None"
1034
+ );
1035
+ }
1036
+ }
1037
+
1038
+ #[test]
1039
+ fn test_parse_selector_all_shortest_with_plain_edge()
1040
+ {
1041
+ let result = parse("MATCH ALL SHORTEST (a)-[:R]->(b) RETURN a");
1042
+ assert!(
1043
+ result.is_ok(),
1044
+ "Expected ALL SHORTEST selector with plain edge to succeed, got: {:?}",
1045
+ result.err()
1046
+ );
1047
+ if let Ok(queries) = result
1048
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
1049
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
1050
+ {
1051
+ let has_all_shortest_selector = match_clause.patterns.iter().any(|p| {
1052
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
1053
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::AllShortest)
1054
+ && qp.quantifier.is_none())
1055
+ });
1056
+ assert!(
1057
+ has_all_shortest_selector,
1058
+ "Expected QuantifiedPath with PathSelector::AllShortest and quantifier: None"
1059
+ );
1060
+ }
1061
+ }
1062
+
1063
+ #[test]
1064
+ fn test_parse_selector_shortest_k_with_plain_edge()
1065
+ {
1066
+ let result = parse("MATCH SHORTEST 1 (a)-[:R]->(b) RETURN a");
1067
+ assert!(
1068
+ result.is_ok(),
1069
+ "Expected SHORTEST 1 selector with plain edge to succeed, got: {:?}",
1070
+ result.err()
1071
+ );
1072
+ if let Ok(queries) = result
1073
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
1074
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
1075
+ {
1076
+ let has_shortest_selector = match_clause.patterns.iter().any(|p| {
1077
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
1078
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Shortest(_))
1079
+ && qp.quantifier.is_none())
1080
+ });
1081
+ assert!(
1082
+ has_shortest_selector,
1083
+ "Expected QuantifiedPath with PathSelector::Shortest(_) and quantifier: None"
1084
+ );
1085
+ }
1086
+ }
1087
+
1088
+ #[test]
1089
+ fn test_parse_selector_shortest_groups_with_plain_edge()
1090
+ {
1091
+ let result = parse("MATCH SHORTEST 5 GROUPS (a)-[:R]->(b) RETURN a");
1092
+ assert!(
1093
+ result.is_ok(),
1094
+ "Expected SHORTEST 5 GROUPS selector with plain edge to succeed, got: {:?}",
1095
+ result.err()
1096
+ );
1097
+ if let Ok(queries) = result
1098
+ && let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
1099
+ && let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
1100
+ {
1101
+ let has_shortest_groups_selector = match_clause.patterns.iter().any(|p| {
1102
+ matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
1103
+ if matches!(qp.selector, gqlparser::oc::ast::PathSelector::ShortestGroups(_))
1104
+ && qp.quantifier.is_none())
1105
+ });
1106
+ assert!(
1107
+ has_shortest_groups_selector,
1108
+ "Expected QuantifiedPath with PathSelector::ShortestGroups(_) and quantifier: None"
1109
+ );
1110
+ }
1111
+ }
1112
+
1113
+ // Test 3: Error cases - selector with no pattern following or with bare node
1114
+
1115
+ #[test]
1116
+ fn test_parse_selector_no_pattern_should_error()
1117
+ {
1118
+ let result = parse("MATCH SHORTEST 1 RETURN a");
1119
+ assert!(
1120
+ result.is_err(),
1121
+ "Expected SHORTEST 1 with no pattern to error, got: {:?}",
1122
+ result
1123
+ );
1124
+ }
1125
+
1126
+ #[test]
1127
+ fn test_parse_selector_with_bare_node_should_error()
1128
+ {
1129
+ let result = parse("MATCH SHORTEST 1 (a) RETURN a");
1130
+ assert!(
1131
+ result.is_err(),
1132
+ "Expected SHORTEST 1 with bare node to error, got: {:?}",
1133
+ result
1134
+ );
1135
+ }
1136
+
1137
+ // Test 4: Regression - verify selector-free tests still pass
1138
+ // (Each test individually verifies that existing functionality is not broken)
1139
+
1140
+ #[test]
1141
+ fn test_selector_regression_simple_match()
1142
+ {
1143
+ let result = parse("MATCH (a) RETURN a");
1144
+ assert!(
1145
+ result.is_ok(),
1146
+ "Regression: Expected simple MATCH to still parse, got: {:?}",
1147
+ result.err()
1148
+ );
1149
+ }
1150
+
1151
+ #[test]
1152
+ fn test_selector_regression_edge_match()
1153
+ {
1154
+ let result = parse("MATCH (a)-[:R]->(b) RETURN a");
1155
+ assert!(
1156
+ result.is_ok(),
1157
+ "Regression: Expected edge MATCH to still parse, got: {:?}",
1158
+ result.err()
1159
+ );
1160
+ }
1161
+
1162
+ #[test]
1163
+ fn test_selector_regression_quantified_pattern()
1164
+ {
1165
+ let result = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p");
1166
+ assert!(
1167
+ result.is_ok(),
1168
+ "Regression: Expected quantified pattern to still parse, got: {:?}",
1169
+ result.err()
1170
+ );
1171
+ }
1172
+
1173
+ #[test]
1174
+ fn test_selector_regression_path_variable()
1175
+ {
1176
+ let result = parse("MATCH p = (a)-[:R]->(b) RETURN p");
1177
+ assert!(
1178
+ result.is_ok(),
1179
+ "Regression: Expected path variable to still parse, got: {:?}",
1180
+ result.err()
1181
+ );
1182
+ }
1183
+
1184
+ // WI689 Tests: Empty quantified groups and terminal node tracking
1185
+
1186
+ #[test]
1187
+ fn test_empty_quantified_group_should_error()
1188
+ {
1189
+ let result = parse("MATCH (a) ((n)){1,5} (b) RETURN a");
1190
+ assert!(
1191
+ result.is_err(),
1192
+ "Expected empty quantified group to error, got: {:?}",
1193
+ result
1194
+ );
1195
+ }
1196
+
1197
+ #[test]
1198
+ fn test_quantified_group_with_unnamed_terminal_node()
1199
+ {
1200
+ let result = parse("MATCH (a) ((n)-[r]->()) {1,5} (b) RETURN a");
1201
+ assert!(
1202
+ result.is_ok(),
1203
+ "Expected quantified group with unnamed terminal node to parse, got: {:?}",
1204
+ result.err()
1205
+ );
1206
+
1207
+ // Verify that the edge's destination now has a variable assigned
1208
+ let queries = result.unwrap();
1209
+ let oc::ast::Query::Statements(ast) = queries.into_iter().next().unwrap()
1210
+ else
1211
+ {
1212
+ panic!("Expected Statements query");
1213
+ };
1214
+
1215
+ // The AST should contain a QuantifiedPath pattern
1216
+ let found_qp = ast.iter().any(|stmt| {
1217
+ if let oc::ast::Statement::Match(match_stmt) = stmt
1218
+ {
1219
+ match_stmt.patterns.iter().any(|p| {
1220
+ if let oc::ast::Pattern::QuantifiedPath(qp) = p
1221
+ {
1222
+ // The last edge in the group should have a destination variable
1223
+ qp.group
1224
+ .iter()
1225
+ .rev()
1226
+ .find_map(|p| {
1227
+ if let oc::ast::Pattern::Edge(e) = p
1228
+ {
1229
+ e.destination.variable.is_some().then_some(())
1230
+ }
1231
+ else
1232
+ {
1233
+ None
1234
+ }
1235
+ })
1236
+ .is_some()
1237
+ }
1238
+ else
1239
+ {
1240
+ false
1241
+ }
1242
+ })
1243
+ }
1244
+ else
1245
+ {
1246
+ false
1247
+ }
1248
+ });
1249
+
1250
+ assert!(
1251
+ found_qp,
1252
+ "Expected to find a quantified path with a terminal variable"
1253
+ );
1254
+ }
1255
+
1256
+ #[test]
1257
+ fn test_quantified_group_with_named_terminal_node_preserved()
1258
+ {
1259
+ let result = parse("MATCH (a) ((n)-[r]->(m)){1,5} (b) RETURN a");
1260
+ assert!(
1261
+ result.is_ok(),
1262
+ "Expected quantified group with named terminal node to parse, got: {:?}",
1263
+ result.err()
1264
+ );
1265
+
1266
+ // Verify that the named variable is preserved
1267
+ let queries = result.unwrap();
1268
+ let oc::ast::Query::Statements(ast) = queries.into_iter().next().unwrap()
1269
+ else
1270
+ {
1271
+ panic!("Expected Statements query");
1272
+ };
1273
+
1274
+ // The AST should contain a QuantifiedPath pattern with the correct variable name
1275
+ let found_qp = ast.iter().any(|stmt| {
1276
+ if let oc::ast::Statement::Match(match_stmt) = stmt
1277
+ {
1278
+ match_stmt.patterns.iter().any(|p| {
1279
+ if let oc::ast::Pattern::QuantifiedPath(qp) = p
1280
+ {
1281
+ // The last edge in the group should have a destination variable named "m"
1282
+ qp.group
1283
+ .iter()
1284
+ .rev()
1285
+ .find_map(|p| {
1286
+ if let oc::ast::Pattern::Edge(e) = p
1287
+ {
1288
+ e.destination
1289
+ .variable
1290
+ .as_ref()
1291
+ .and_then(|v| if v.name() == "m" { Some(()) } else { None })
1292
+ }
1293
+ else
1294
+ {
1295
+ None
1296
+ }
1297
+ })
1298
+ .is_some()
1299
+ }
1300
+ else
1301
+ {
1302
+ false
1303
+ }
1304
+ })
1305
+ }
1306
+ else
1307
+ {
1308
+ false
1309
+ }
1310
+ });
1311
+
1312
+ assert!(
1313
+ found_qp,
1314
+ "Expected to find a quantified path with terminal variable named 'm'"
1315
+ );
1316
+ }
1317
+
1318
+ #[test]
1319
+ fn test_parse_group_single_edge_range()
1320
+ {
1321
+ let ast = parse_query("MATCH (a) ((n)-[:R]->(m)){1,3} (b) RETURN a, b");
1322
+ compare_ast(ast, ast::group_single_edge_range())
1323
+ }
1324
+
1325
+ #[test]
1326
+ fn test_parse_group_two_edges_range()
1327
+ {
1328
+ let ast = parse_query("MATCH (a) ((n)-[:R1]->(x)-[:R2]->(m)){1,3} (b) RETURN a, b");
1329
+ compare_ast(ast, ast::group_two_edges_range())
1330
+ }
1331
+
1332
+ #[test]
1333
+ fn test_parse_shortest_single_edge_full_return()
1334
+ {
1335
+ let ast = parse_query("MATCH p = SHORTEST 1 (a)-[:R]-+(b) RETURN a, b, length(p)");
1336
+ compare_ast(ast, ast::shortest_single_edge_full_return())
1337
+ }