gqlite 1.8.1 → 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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/gqlitedb/Cargo.toml +0 -2
  4. data/ext/gqlitedb/src/compiler/variables_manager.rs +109 -10
  5. data/ext/gqlitedb/src/compiler.rs +481 -47
  6. data/ext/gqlitedb/src/connection.rs +12 -0
  7. data/ext/gqlitedb/src/consts.rs +1 -1
  8. data/ext/gqlitedb/src/error.rs +24 -0
  9. data/ext/gqlitedb/src/interpreter/evaluators.rs +91 -325
  10. data/ext/gqlitedb/src/interpreter/instructions.rs +72 -0
  11. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  12. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  13. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  14. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  15. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  16. data/ext/gqlitedb/src/planner.rs +218 -6
  17. data/ext/gqlitedb/src/query_result.rs +19 -4
  18. data/ext/gqlitedb/src/tests/compiler.rs +2453 -1
  19. data/ext/gqlitedb/src/tests/connection/redb.rs +8 -0
  20. data/ext/gqlitedb/src/tests/connection.rs +37 -0
  21. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  22. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  23. data/ext/gqlitedb/src/tests/planner.rs +432 -2
  24. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  25. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  26. data/ext/gqlitedb/src/tests/templates/ast.rs +284 -1
  27. data/ext/gqlitedb/src/tests/templates/programs.rs +306 -0
  28. data/ext/gqliterb/Cargo.toml +0 -1
  29. data/ext/gqlparser/src/oc/ast.rs +52 -3
  30. data/ext/gqlparser/src/oc/error.rs +18 -0
  31. data/ext/gqlparser/src/oc/lexer.rs +146 -0
  32. data/ext/gqlparser/src/oc/parser/tests.rs +365 -20
  33. data/ext/gqlparser/src/oc/parser.rs +1196 -143
  34. data/ext/graphcore/src/value.rs +12 -1
  35. metadata +5 -2
  36. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -124,11 +124,13 @@ fn test_compile_logicalplan_nodescan_filter()
124
124
  &function_manager,
125
125
  vec![LogicalPlan::Filter {
126
126
  source: Box::new(LogicalPlan::NodeScan {
127
- patterns: vec![gqlparser::oc::ast::Pattern::Node(
127
+ patterns: vec![planner::PlannedPattern::Node(
128
128
  gqlparser::oc::ast::NodePattern {
129
129
  variable: Some(node_identifier.clone()),
130
130
  labels: gqlparser::oc::ast::LabelExpression::None,
131
131
  properties: None,
132
+
133
+ predicate: None,
132
134
  },
133
135
  )],
134
136
  optional: false,
@@ -288,3 +290,2453 @@ fn test_compile_multiple_vector_indexes_produces_distinct_instructions()
288
290
  assert!(debug.contains("\"idx1\""));
289
291
  assert!(debug.contains("\"idx2\""));
290
292
  }
293
+
294
+ #[test]
295
+ fn test_compile_match_node_with_inline_predicate()
296
+ {
297
+ let function_manager = functions::Manager::new();
298
+ use gqlparser::oc::ast::*;
299
+
300
+ let identifiers = VariableIdentifiers::default();
301
+ let n_identifier = identifiers.create_variable_from_name("n");
302
+
303
+ // Construct the predicate: n.name = 'X'
304
+ let predicate = Expression::RelationalEqual(Box::new(RelationalEqual {
305
+ left: Expression::MemberAccess(Box::new(MemberAccess {
306
+ left: Expression::Variable(Variable {
307
+ identifier: n_identifier.clone(),
308
+ }),
309
+ path: vec!["name".to_string()],
310
+ })),
311
+ right: Expression::Value(Value { value: "X".into() }),
312
+ }));
313
+
314
+ // Construct the logical plan with predicate
315
+ let program = compile(
316
+ &function_manager,
317
+ vec![LogicalPlan::NodeScan {
318
+ patterns: vec![planner::PlannedPattern::Node(NodePattern {
319
+ variable: Some(n_identifier.clone()),
320
+ labels: LabelExpression::None,
321
+ properties: None,
322
+ predicate: Some(predicate),
323
+ })],
324
+ optional: false,
325
+ }],
326
+ )
327
+ .unwrap();
328
+
329
+ let debug = format!("{:?}", program);
330
+ // Verify that the predicate instructions are present
331
+ assert!(
332
+ debug.contains("GetVariable"),
333
+ "Expected GetVariable instruction for variable access in predicate"
334
+ );
335
+ assert!(
336
+ debug.contains("AndBinaryOperator"),
337
+ "Expected AndBinaryOperator to combine predicate with filter stack"
338
+ );
339
+ assert!(
340
+ debug.contains("EqualBinaryOperator"),
341
+ "Expected EqualBinaryOperator for '=' comparison in predicate"
342
+ );
343
+ }
344
+
345
+ #[test]
346
+ fn test_compile_match_edge_with_inline_predicate()
347
+ {
348
+ let function_manager = functions::Manager::new();
349
+ use gqlparser::oc::ast::*;
350
+
351
+ let identifiers = VariableIdentifiers::default();
352
+ let a_identifier = identifiers.create_variable_from_name("a");
353
+ let b_identifier = identifiers.create_variable_from_name("b");
354
+ let e_identifier = identifiers.create_variable_from_name("e");
355
+
356
+ // Construct the predicate: e.distance < 10
357
+ let predicate = Expression::RelationalInferior(Box::new(RelationalInferior {
358
+ left: Expression::MemberAccess(Box::new(MemberAccess {
359
+ left: Expression::Variable(Variable {
360
+ identifier: e_identifier.clone(),
361
+ }),
362
+ path: vec!["distance".to_string()],
363
+ })),
364
+ right: Expression::Value(Value {
365
+ value: 10i64.into(),
366
+ }),
367
+ }));
368
+
369
+ // Construct the logical plan with edge predicate
370
+ // Note: Edge patterns are represented as PlannedPattern::Edge within a NodeScan
371
+ let program = compile(
372
+ &function_manager,
373
+ vec![LogicalPlan::NodeScan {
374
+ patterns: vec![planner::PlannedPattern::Edge(EdgePattern {
375
+ source: NodePattern {
376
+ variable: Some(a_identifier.clone()),
377
+ labels: LabelExpression::None,
378
+ properties: None,
379
+ predicate: None,
380
+ },
381
+ variable: Some(e_identifier.clone()),
382
+ directivity: graphcore::EdgeDirectivity::Directed,
383
+ labels: LabelExpression::None,
384
+ properties: None,
385
+ predicate: Some(predicate),
386
+ destination: NodePattern {
387
+ variable: Some(b_identifier.clone()),
388
+ labels: LabelExpression::None,
389
+ properties: None,
390
+ predicate: None,
391
+ },
392
+ })],
393
+ optional: false,
394
+ }],
395
+ )
396
+ .unwrap();
397
+
398
+ let debug = format!("{:?}", program);
399
+ // Verify that the predicate instructions are present
400
+ assert!(
401
+ debug.contains("GetVariable"),
402
+ "Expected GetVariable instruction for variable access in predicate"
403
+ );
404
+ assert!(
405
+ debug.contains("AndBinaryOperator"),
406
+ "Expected AndBinaryOperator to combine predicate with filter stack"
407
+ );
408
+ assert!(
409
+ debug.contains("InferiorBinaryOperator"),
410
+ "Expected InferiorBinaryOperator for '<' comparison in predicate"
411
+ );
412
+ }
413
+
414
+ #[test]
415
+ fn test_compile_quantified_edge_legacy_syntax()
416
+ {
417
+ use gqlparser::oc::parse_oc_query as parse;
418
+
419
+ let function_manager = functions::Manager::new();
420
+
421
+ // Parse legacy [*1..3] syntax
422
+ let queries = parse("MATCH (a)-[:R*1..3]->(b) RETURN a").unwrap();
423
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
424
+ else
425
+ {
426
+ panic!("Expected Statements query");
427
+ };
428
+
429
+ let program = compile_statements(&function_manager, statements);
430
+ let debug = format!("{:?}", program);
431
+
432
+ // Verify that the program contains MatchEdge with the quantifier range
433
+ assert!(
434
+ debug.contains("recursive_range: Some(1..3)"),
435
+ "Expected recursive_range: Some(1..3) in compiled program"
436
+ );
437
+ }
438
+
439
+ #[test]
440
+ fn test_compile_quantified_edge_new_syntax()
441
+ {
442
+ use gqlparser::oc::parse_oc_query as parse;
443
+
444
+ let function_manager = functions::Manager::new();
445
+
446
+ // Parse new {1,3} syntax
447
+ let queries = parse("MATCH (a)-[:R]->{1,3}(b) RETURN a").unwrap();
448
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
449
+ else
450
+ {
451
+ panic!("Expected Statements query");
452
+ };
453
+
454
+ let program = compile_statements(&function_manager, statements);
455
+ let debug = format!("{:?}", program);
456
+
457
+ // Verify that the program contains MatchEdge with the quantifier range
458
+ assert!(
459
+ debug.contains("recursive_range: Some(1..3)"),
460
+ "Expected recursive_range: Some(1..3) in compiled program"
461
+ );
462
+ }
463
+
464
+ #[test]
465
+ fn test_compile_legacy_and_new_quantifier_syntax_identical()
466
+ {
467
+ use gqlparser::oc::parse_oc_query as parse;
468
+
469
+ let function_manager = functions::Manager::new();
470
+
471
+ // Parse legacy [*1..3] syntax
472
+ let queries_legacy = parse("MATCH (a)-[:R*1..3]->(b) RETURN a").unwrap();
473
+ let gqlparser::oc::ast::Query::Statements(statements_legacy) =
474
+ queries_legacy.into_iter().next().unwrap()
475
+ else
476
+ {
477
+ panic!("Expected Statements query");
478
+ };
479
+ let program_legacy = compile_statements(&function_manager, statements_legacy);
480
+
481
+ // Parse new {1,3} syntax
482
+ let queries_new = parse("MATCH (a)-[:R]->{1,3}(b) RETURN a").unwrap();
483
+ let gqlparser::oc::ast::Query::Statements(statements_new) =
484
+ queries_new.into_iter().next().unwrap()
485
+ else
486
+ {
487
+ panic!("Expected Statements query");
488
+ };
489
+ let program_new = compile_statements(&function_manager, statements_new);
490
+
491
+ // Both should produce identical compiled programs
492
+ compare_program(program_legacy, program_new);
493
+ }
494
+
495
+ #[test]
496
+ fn test_compile_quantified_edge_with_path_variable()
497
+ {
498
+ use gqlparser::oc::parse_oc_query as parse;
499
+
500
+ let function_manager = functions::Manager::new();
501
+
502
+ // Parse path variable with quantified edge
503
+ let queries = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p").unwrap();
504
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
505
+ else
506
+ {
507
+ panic!("Expected Statements query");
508
+ };
509
+
510
+ let program = compile_statements(&function_manager, statements);
511
+ let debug = format!("{:?}", program);
512
+
513
+ // Verify that the program contains MatchEdge with the quantifier range and path variable
514
+ assert!(
515
+ debug.contains("recursive_range: Some(1..3)"),
516
+ "Expected recursive_range: Some(1..3) in compiled program"
517
+ );
518
+ assert!(
519
+ debug.contains("path_variable: Some("),
520
+ "Expected path_variable to be set in compiled program"
521
+ );
522
+ }
523
+
524
+ #[test]
525
+ fn test_compile_create_quantified_edge_rejects()
526
+ {
527
+ use gqlparser::oc::parse_oc_query as parse;
528
+
529
+ let function_manager = functions::Manager::new();
530
+
531
+ // Parse CREATE with quantified edge
532
+ let queries = parse("CREATE (a)-[:R*1..3]->(b)").unwrap();
533
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
534
+ else
535
+ {
536
+ panic!("Expected Statements query");
537
+ };
538
+
539
+ // But compilation should fail with CreatingVarLength error
540
+ let compile_result = compile(
541
+ &function_manager,
542
+ statements
543
+ .into_iter()
544
+ .map(planner::plan)
545
+ .collect::<Result<Vec<_>, _>>()
546
+ .unwrap(),
547
+ );
548
+
549
+ assert!(
550
+ compile_result.is_err(),
551
+ "Expected compilation error for CREATE with quantified edge"
552
+ );
553
+ let error_msg = format!("{:?}", compile_result.err().unwrap());
554
+ assert!(
555
+ error_msg.contains("CreatingVarLength"),
556
+ "Expected CreatingVarLength error, got: {}",
557
+ error_msg
558
+ );
559
+ }
560
+
561
+ #[test]
562
+ fn test_compile_mixed_quantified_and_unquantified_edges()
563
+ {
564
+ use gqlparser::oc::parse_oc_query as parse;
565
+
566
+ let function_manager = functions::Manager::new();
567
+
568
+ // Parse MATCH with both quantified and unquantified edges as separate patterns
569
+ let queries = parse("MATCH (a)-[:R]->(b), (a)-[:S*1..2]->(c) RETURN a").unwrap();
570
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
571
+ else
572
+ {
573
+ panic!("Expected Statements query");
574
+ };
575
+
576
+ let program = compile_statements(&function_manager, statements);
577
+ let debug = format!("{:?}", program);
578
+
579
+ // Verify that the program contains instructions for both patterns
580
+ // The first edge pattern should have no quantifier (None)
581
+ // The second edge pattern should have the quantifier (Some(1..2))
582
+ assert!(
583
+ debug.contains("recursive_range: None"),
584
+ "Expected recursive_range: None for unquantified edge"
585
+ );
586
+ assert!(
587
+ debug.contains("recursive_range: Some(1..2)"),
588
+ "Expected recursive_range: Some(1..2) for quantified edge"
589
+ );
590
+ }
591
+
592
+ #[test]
593
+ fn test_compile_quantified_path_through_planner()
594
+ {
595
+ use gqlparser::oc::parse_oc_query as parse;
596
+
597
+ let function_manager = functions::Manager::new();
598
+
599
+ // Parse single-element quantified path: MATCH (a)-[:R*1..3]->(b) RETURN a
600
+ // This should compile to MatchEdge with recursive_range, not MatchQuantifiedPath
601
+ let queries = parse("MATCH (a)-[:R*1..3]->(b) RETURN a").unwrap();
602
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
603
+ else
604
+ {
605
+ panic!("Expected Statements query");
606
+ };
607
+
608
+ let program = compile_statements(&function_manager, statements);
609
+ let debug = format!("{:?}", program);
610
+
611
+ // Single-element quantified paths use MatchEdge with recursive_range for optimization
612
+ assert!(
613
+ debug.contains("recursive_range: Some(1..3)"),
614
+ "Expected MatchEdge with recursive_range: Some(1..3) for single-element quantified path, got: {}",
615
+ debug
616
+ );
617
+ }
618
+
619
+ #[test]
620
+ fn test_compile_bfs_strategy_quantified_path_succeeds()
621
+ {
622
+ use gqlparser::oc::ast::*;
623
+
624
+ let function_manager = functions::Manager::new();
625
+ let identifiers = VariableIdentifiers::default();
626
+ let a_identifier = identifiers.create_variable_from_name("a");
627
+ let b_identifier = identifiers.create_variable_from_name("b");
628
+
629
+ // Construct a LogicalPlan::NodeScan with PlannedPattern::QuantifiedPath using BFS strategy
630
+ let node_a = NodePattern {
631
+ variable: Some(a_identifier.clone()),
632
+ labels: LabelExpression::None,
633
+ properties: None,
634
+ predicate: None,
635
+ };
636
+
637
+ let node_b = NodePattern {
638
+ variable: Some(b_identifier),
639
+ labels: LabelExpression::None,
640
+ properties: None,
641
+ predicate: None,
642
+ };
643
+
644
+ let qp_source = node_a.clone();
645
+ let qp_destination = node_b.clone();
646
+ let edge_pattern = EdgePattern {
647
+ variable: None,
648
+ source: node_a,
649
+ destination: node_b,
650
+ directivity: graphcore::EdgeDirectivity::Directed,
651
+ labels: LabelExpression::String("R".into()),
652
+ properties: None,
653
+ predicate: None,
654
+ };
655
+
656
+ let quantified_pattern = QuantifiedPathPattern {
657
+ variable: None,
658
+ source: qp_source,
659
+ group: vec![
660
+ Pattern::Edge(edge_pattern.clone()),
661
+ Pattern::Edge(edge_pattern),
662
+ ],
663
+ group_where: None,
664
+ path_where: None,
665
+ quantifier: Some(1..3),
666
+ destination: qp_destination,
667
+ selector: PathSelector::Shortest(Value { value: 1.into() }.into()),
668
+ };
669
+
670
+ // This uses PathStrategy::Bfs (because Shortest selector maps to Bfs)
671
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
672
+ pattern: quantified_pattern.clone(),
673
+ strategy: planner::PathStrategy::Bfs {
674
+ stop_condition: planner::StopCondition::Shortest(Value { value: 1.into() }.into()),
675
+ },
676
+ };
677
+
678
+ let logical_plan = LogicalPlan::NodeScan {
679
+ patterns: vec![planned_pattern],
680
+ optional: false,
681
+ };
682
+
683
+ let compile_result = compile(&function_manager, vec![logical_plan]);
684
+ assert!(
685
+ compile_result.is_ok(),
686
+ "BFS strategy should compile successfully with multi-element group, got: {:?}",
687
+ compile_result.err()
688
+ );
689
+ }
690
+
691
+ #[test]
692
+ fn test_compile_multi_element_group_is_rejected_cleanly()
693
+ {
694
+ use gqlparser::oc::parse_oc_query as parse;
695
+
696
+ let function_manager = functions::Manager::new();
697
+
698
+ // Parse a real query containing a multi-element quantified group.
699
+ // This now compiles successfully since WI #634 implemented support.
700
+ let queries = parse("MATCH (a) ((n)-[:R]->(m)-[:S]->(k)){1,5} (b) RETURN a").unwrap();
701
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
702
+ else
703
+ {
704
+ panic!("Expected Statements query");
705
+ };
706
+
707
+ let compile_result = compile(
708
+ &function_manager,
709
+ statements
710
+ .into_iter()
711
+ .map(planner::plan)
712
+ .collect::<Result<Vec<_>, _>>()
713
+ .unwrap(),
714
+ );
715
+
716
+ // Multi-element quantified groups now compile successfully
717
+ assert!(
718
+ compile_result.is_ok(),
719
+ "Expected successful compilation for multi-element quantified group, got error: {:?}",
720
+ compile_result.err()
721
+ );
722
+ }
723
+
724
+ #[test]
725
+ fn test_compile_create_with_quantified_edge_error()
726
+ {
727
+ use gqlparser::oc::parse_oc_query as parse;
728
+
729
+ let function_manager = functions::Manager::new();
730
+
731
+ // Parse CREATE (a)-[:R*1..3]->(b)
732
+ let queries = parse("CREATE (a)-[:R*1..3]->(b)").unwrap();
733
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
734
+ else
735
+ {
736
+ panic!("Expected Statements query");
737
+ };
738
+
739
+ let compile_result = compile(
740
+ &function_manager,
741
+ statements
742
+ .into_iter()
743
+ .map(planner::plan)
744
+ .collect::<Result<Vec<_>, _>>()
745
+ .unwrap(),
746
+ );
747
+
748
+ assert!(
749
+ compile_result.is_err(),
750
+ "Expected compilation error for CREATE with quantified edge"
751
+ );
752
+ let error_msg = format!("{:?}", compile_result.err().unwrap());
753
+ assert!(
754
+ error_msg.contains("CreatingVarLength"),
755
+ "Expected CreatingVarLength error, got: {}",
756
+ error_msg
757
+ );
758
+ }
759
+
760
+ #[test]
761
+ fn test_variables_manager_quantified_path_variable_binding()
762
+ {
763
+ use gqlparser::oc::parse_oc_query as parse;
764
+
765
+ let function_manager = functions::Manager::new();
766
+
767
+ // Test 1: Basic binding - MATCH p = (a)-[:R*1..3]->(b) RETURN p
768
+ // Verify the path variable is properly declared
769
+ let queries = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p").unwrap();
770
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
771
+ else
772
+ {
773
+ panic!("Expected Statements query");
774
+ };
775
+
776
+ let program = compile_statements(&function_manager, statements);
777
+ let debug = format!("{:?}", program);
778
+
779
+ // Verify bytecode includes the quantified path setup
780
+ assert!(
781
+ debug.contains("recursive_range: Some(1..3)"),
782
+ "Expected quantified range in compiled output"
783
+ );
784
+
785
+ // Test 2: Variable usage - Verify binding by using p in a projection
786
+ // If p is not properly bound, this would fail during analysis
787
+ let queries2 = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN a, b, p").unwrap();
788
+ let gqlparser::oc::ast::Query::Statements(statements2) = queries2.into_iter().next().unwrap()
789
+ else
790
+ {
791
+ panic!("Expected Statements query");
792
+ };
793
+
794
+ let compile_result = compile_statements(&function_manager, statements2);
795
+ let debug2 = format!("{:?}", compile_result);
796
+
797
+ // Verify compilation succeeded, meaning p was properly declared and accessible
798
+ assert!(
799
+ !debug2.is_empty(),
800
+ "Expected successful compilation with path variable in return projection"
801
+ );
802
+ }
803
+
804
+ #[test]
805
+ fn test_variables_manager_merge_quantified_path_conflict_detection_scenario_a()
806
+ {
807
+ use gqlparser::oc::parse_oc_query as parse;
808
+
809
+ let function_manager = functions::Manager::new();
810
+
811
+ // Scenario A: Verify that MERGE allows proper variable handling with quantified paths
812
+ // MERGE (a:Person)-[:R*1..3]->(b) should compile successfully
813
+ // The conflict detection is in place as a safety check even if normal queries don't trigger it
814
+ let queries = parse("MERGE (a:Person)-[:R*1..3]->(b) RETURN a, b").unwrap();
815
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
816
+ else
817
+ {
818
+ panic!("Expected Statements query");
819
+ };
820
+
821
+ let planned_statements = statements
822
+ .into_iter()
823
+ .map(planner::plan)
824
+ .collect::<Result<Vec<_>, _>>()
825
+ .unwrap();
826
+
827
+ // This should fail with CreatingVarLength since MERGE's on_create falls back to CREATE
828
+ let compile_result = compile(&function_manager, planned_statements);
829
+
830
+ assert!(
831
+ compile_result.is_err(),
832
+ "Expected error for quantified path in MERGE on_create"
833
+ );
834
+ }
835
+
836
+ #[test]
837
+ fn test_variables_manager_merge_quantified_path_conflict_detection_scenario_b()
838
+ {
839
+ use gqlparser::oc::parse_oc_query as parse;
840
+
841
+ let function_manager = functions::Manager::new();
842
+
843
+ // Scenario B: Verify the QuantifiedPath arm's conflict check is properly in place
844
+ // Test that MERGE with a bound quantified path variable triggers the CreatingVarLength error
845
+ let queries = parse("MERGE p = (a:Person)-[:R*1..3]->(b) RETURN p").unwrap();
846
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
847
+ else
848
+ {
849
+ panic!("Expected Statements query");
850
+ };
851
+
852
+ let planned_statements = statements
853
+ .into_iter()
854
+ .map(planner::plan)
855
+ .collect::<Result<Vec<_>, _>>()
856
+ .unwrap();
857
+
858
+ // This should fail due to quantified paths not being supported in MERGE on_create
859
+ let compile_result = compile(&function_manager, planned_statements);
860
+
861
+ assert!(
862
+ compile_result.is_err(),
863
+ "Expected error for quantified path variable in MERGE (on_create fallback)"
864
+ );
865
+ let error_msg = format!("{:?}", compile_result.err().unwrap());
866
+ assert!(
867
+ error_msg.contains("CreatingVarLength"),
868
+ "Expected CreatingVarLength error, got: {}",
869
+ error_msg
870
+ );
871
+ }
872
+
873
+ #[test]
874
+ fn test_variables_manager_merge_with_quantified_path_creates_var_length_error()
875
+ {
876
+ use gqlparser::oc::parse_oc_query as parse;
877
+
878
+ let function_manager = functions::Manager::new();
879
+
880
+ // Verify that MERGE with quantified paths triggers CreatingVarLength error
881
+ // This tests the on_create fallback that uses compile_create_patterns
882
+ let queries = parse("MERGE (a)-[:R*1..2]->(b) RETURN a").unwrap();
883
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
884
+ else
885
+ {
886
+ panic!("Expected Statements query");
887
+ };
888
+
889
+ let planned_statements = statements
890
+ .into_iter()
891
+ .map(planner::plan)
892
+ .collect::<Result<Vec<_>, _>>()
893
+ .unwrap();
894
+
895
+ let compile_result = compile(&function_manager, planned_statements);
896
+
897
+ assert!(
898
+ compile_result.is_err(),
899
+ "Expected compilation error for quantified path in MERGE on_create fallback"
900
+ );
901
+ let error_msg = format!("{:?}", compile_result.err().unwrap());
902
+ assert!(
903
+ error_msg.contains("CreatingVarLength"),
904
+ "Expected CreatingVarLength error for quantified path in MERGE, got: {}",
905
+ error_msg
906
+ );
907
+ }
908
+
909
+ #[test]
910
+ fn test_compile_quantified_path_group_variables_and_quantifier()
911
+ {
912
+ use gqlparser::oc::ast::*;
913
+
914
+ let function_manager = functions::Manager::new();
915
+ let identifiers = VariableIdentifiers::default();
916
+ let a_identifier = identifiers.create_variable_from_name("a");
917
+ let b_identifier = identifiers.create_variable_from_name("b");
918
+ let n_identifier = identifiers.create_variable_from_name("n");
919
+ let r_identifier = identifiers.create_variable_from_name("r");
920
+ let m_identifier = identifiers.create_variable_from_name("m");
921
+
922
+ // Construct a QuantifiedPathPattern: (a:Station) ((n)-[r:KNOWS]->(m)){1,5} (b:Station)
923
+ let node_a = NodePattern {
924
+ variable: Some(a_identifier.clone()),
925
+ labels: LabelExpression::String("Station".into()),
926
+ properties: None,
927
+ predicate: None,
928
+ };
929
+
930
+ let node_b = NodePattern {
931
+ variable: Some(b_identifier.clone()),
932
+ labels: LabelExpression::String("Station".into()),
933
+ properties: None,
934
+ predicate: None,
935
+ };
936
+
937
+ let node_n = NodePattern {
938
+ variable: Some(n_identifier.clone()),
939
+ labels: LabelExpression::None,
940
+ properties: None,
941
+ predicate: None,
942
+ };
943
+
944
+ let node_m = NodePattern {
945
+ variable: Some(m_identifier.clone()),
946
+ labels: LabelExpression::None,
947
+ properties: None,
948
+ predicate: None,
949
+ };
950
+
951
+ let o_identifier = identifiers.create_variable_from_name("o");
952
+ let node_o = NodePattern {
953
+ variable: Some(o_identifier.clone()),
954
+ labels: LabelExpression::String("Person".into()),
955
+ properties: None,
956
+ predicate: None,
957
+ };
958
+
959
+ let edge_pattern = EdgePattern {
960
+ variable: Some(r_identifier.clone()),
961
+ source: node_n,
962
+ destination: node_m,
963
+ directivity: graphcore::EdgeDirectivity::Directed,
964
+ labels: LabelExpression::String("KNOWS".into()),
965
+ properties: None,
966
+ predicate: None,
967
+ };
968
+
969
+ let quantified_pattern = QuantifiedPathPattern {
970
+ variable: None,
971
+ source: node_a,
972
+ group: vec![Pattern::Edge(edge_pattern), Pattern::Node(node_o)],
973
+ group_where: None,
974
+ path_where: None,
975
+ quantifier: Some(1..5),
976
+ destination: node_b,
977
+ selector: PathSelector::All,
978
+ };
979
+
980
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
981
+ pattern: quantified_pattern,
982
+ strategy: planner::PathStrategy::Dfs,
983
+ };
984
+
985
+ let logical_plan = LogicalPlan::NodeScan {
986
+ patterns: vec![planned_pattern],
987
+ optional: false,
988
+ };
989
+
990
+ let compile_result = compile(&function_manager, vec![logical_plan]);
991
+ assert!(
992
+ compile_result.is_ok(),
993
+ "Expected successful compilation for quantified path, got error: {:?}",
994
+ compile_result.err()
995
+ );
996
+
997
+ let program = compile_result.unwrap();
998
+ assert!(!program.is_empty(), "Expected non-empty program blocks");
999
+
1000
+ // Find the Match block
1001
+ let match_block = program.iter().find_map(|block| {
1002
+ if let Block::Match { blocks, .. } = block
1003
+ {
1004
+ Some(blocks)
1005
+ }
1006
+ else
1007
+ {
1008
+ None
1009
+ }
1010
+ });
1011
+
1012
+ assert!(
1013
+ match_block.is_some(),
1014
+ "Expected to find a Match block in the program"
1015
+ );
1016
+
1017
+ let match_blocks = match_block.unwrap();
1018
+
1019
+ // Find the MatchQuantifiedPath
1020
+ let quantified_match = match_blocks
1021
+ .iter()
1022
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }));
1023
+
1024
+ assert!(
1025
+ quantified_match.is_some(),
1026
+ "Expected to find a MatchQuantifiedPath block"
1027
+ );
1028
+
1029
+ let BlockMatch::MatchQuantifiedPath {
1030
+ group_variables,
1031
+ quantifier,
1032
+ source_instructions,
1033
+ destination_filter,
1034
+ group_where_filter,
1035
+ ..
1036
+ } = quantified_match.unwrap()
1037
+ else
1038
+ {
1039
+ unreachable!()
1040
+ };
1041
+
1042
+ // Verify group_variables has exactly 3 entries (n, r, m, o) - updated from 3 to 4
1043
+ assert_eq!(
1044
+ group_variables.len(),
1045
+ 4,
1046
+ "Expected 4 group variables (n, r, m, o), got {}",
1047
+ group_variables.len()
1048
+ );
1049
+
1050
+ // Verify quantifier is Some(1..5)
1051
+ assert_eq!(
1052
+ quantifier,
1053
+ &(1..5),
1054
+ "Expected quantifier to be 1..5, got {:?}",
1055
+ quantifier
1056
+ );
1057
+
1058
+ // Verify group_where_filter is None (no WHERE clause specified)
1059
+ assert!(
1060
+ group_where_filter.is_none(),
1061
+ "Expected group_where_filter to be None"
1062
+ );
1063
+
1064
+ // Verify source_instructions contain Station label filter
1065
+ assert!(
1066
+ !source_instructions.is_empty(),
1067
+ "Expected non-empty source_instructions"
1068
+ );
1069
+
1070
+ // Verify destination_filter contain Station label check (not destination_instructions,
1071
+ // since destination is only validated, not queried from store)
1072
+ assert!(
1073
+ !destination_filter.is_empty(),
1074
+ "Expected non-empty destination_filter"
1075
+ );
1076
+ }
1077
+
1078
+ #[test]
1079
+ fn test_compile_quantified_path_with_group_where_clause()
1080
+ {
1081
+ use gqlparser::oc::ast::*;
1082
+
1083
+ let function_manager = functions::Manager::new();
1084
+ let identifiers = VariableIdentifiers::default();
1085
+ let a_identifier = identifiers.create_variable_from_name("a");
1086
+ let b_identifier = identifiers.create_variable_from_name("b");
1087
+ let n_identifier = identifiers.create_variable_from_name("n");
1088
+ let r_identifier = identifiers.create_variable_from_name("r");
1089
+ let m_identifier = identifiers.create_variable_from_name("m");
1090
+
1091
+ // Construct a QuantifiedPathPattern with a WHERE clause on the group
1092
+ let node_a = NodePattern {
1093
+ variable: Some(a_identifier.clone()),
1094
+ labels: LabelExpression::String("Station".into()),
1095
+ properties: None,
1096
+ predicate: None,
1097
+ };
1098
+
1099
+ let node_b = NodePattern {
1100
+ variable: Some(b_identifier.clone()),
1101
+ labels: LabelExpression::String("Station".into()),
1102
+ properties: None,
1103
+ predicate: None,
1104
+ };
1105
+
1106
+ let node_n = NodePattern {
1107
+ variable: Some(n_identifier.clone()),
1108
+ labels: LabelExpression::None,
1109
+ properties: None,
1110
+ predicate: None,
1111
+ };
1112
+
1113
+ let node_m = NodePattern {
1114
+ variable: Some(m_identifier.clone()),
1115
+ labels: LabelExpression::None,
1116
+ properties: None,
1117
+ predicate: None,
1118
+ };
1119
+
1120
+ let o_identifier = identifiers.create_variable_from_name("o");
1121
+ let node_o = NodePattern {
1122
+ variable: Some(o_identifier.clone()),
1123
+ labels: LabelExpression::String("Person".into()),
1124
+ properties: None,
1125
+ predicate: None,
1126
+ };
1127
+
1128
+ let edge_pattern = EdgePattern {
1129
+ variable: Some(r_identifier.clone()),
1130
+ source: node_n,
1131
+ destination: node_m,
1132
+ directivity: graphcore::EdgeDirectivity::Directed,
1133
+ labels: LabelExpression::String("KNOWS".into()),
1134
+ properties: None,
1135
+ predicate: None,
1136
+ };
1137
+
1138
+ // Create a WHERE clause on the group: WHERE r.weight > 10
1139
+ let group_where = Some(Expression::RelationalSuperior(Box::new(
1140
+ RelationalSuperior {
1141
+ left: Expression::MemberAccess(Box::new(MemberAccess {
1142
+ left: Expression::Variable(Variable {
1143
+ identifier: r_identifier.clone(),
1144
+ }),
1145
+ path: vec!["weight".to_string()],
1146
+ })),
1147
+ right: Expression::Value(Value {
1148
+ value: 10i64.into(),
1149
+ }),
1150
+ },
1151
+ )));
1152
+
1153
+ // Create a multi-element group: (n)-[r:KNOWS]->(m) (o)
1154
+ let quantified_pattern = QuantifiedPathPattern {
1155
+ variable: None,
1156
+ source: node_a,
1157
+ group: vec![Pattern::Edge(edge_pattern), Pattern::Node(node_o)],
1158
+ group_where,
1159
+ path_where: None,
1160
+ quantifier: Some(1..5),
1161
+ destination: node_b,
1162
+ selector: PathSelector::All,
1163
+ };
1164
+
1165
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
1166
+ pattern: quantified_pattern,
1167
+ strategy: planner::PathStrategy::Dfs,
1168
+ };
1169
+
1170
+ let logical_plan = LogicalPlan::NodeScan {
1171
+ patterns: vec![planned_pattern],
1172
+ optional: false,
1173
+ };
1174
+
1175
+ let compile_result = compile(&function_manager, vec![logical_plan]);
1176
+ assert!(
1177
+ compile_result.is_ok(),
1178
+ "Expected successful compilation for quantified path with GROUP WHERE, got error: {:?}",
1179
+ compile_result.err()
1180
+ );
1181
+
1182
+ let program = compile_result.unwrap();
1183
+
1184
+ // Find the Match block
1185
+ let match_block = program.iter().find_map(|block| {
1186
+ if let Block::Match { blocks, .. } = block
1187
+ {
1188
+ Some(blocks)
1189
+ }
1190
+ else
1191
+ {
1192
+ None
1193
+ }
1194
+ });
1195
+
1196
+ let match_blocks = match_block.unwrap();
1197
+
1198
+ // Find the MatchQuantifiedPath
1199
+ let BlockMatch::MatchQuantifiedPath {
1200
+ group_where_filter, ..
1201
+ } = match_blocks
1202
+ .iter()
1203
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1204
+ .unwrap()
1205
+ else
1206
+ {
1207
+ unreachable!()
1208
+ };
1209
+
1210
+ // Verify group_where_filter is Some (WHERE clause is present)
1211
+ assert!(
1212
+ group_where_filter.is_some(),
1213
+ "Expected group_where_filter to be Some"
1214
+ );
1215
+ }
1216
+
1217
+ #[test]
1218
+ fn test_compile_quantified_path_without_group_where_clause()
1219
+ {
1220
+ use gqlparser::oc::ast::*;
1221
+
1222
+ let function_manager = functions::Manager::new();
1223
+ let identifiers = VariableIdentifiers::default();
1224
+ let a_identifier = identifiers.create_variable_from_name("a");
1225
+ let b_identifier = identifiers.create_variable_from_name("b");
1226
+ let n_identifier = identifiers.create_variable_from_name("n");
1227
+ let r_identifier = identifiers.create_variable_from_name("r");
1228
+ let m_identifier = identifiers.create_variable_from_name("m");
1229
+
1230
+ // Construct a QuantifiedPathPattern without a WHERE clause
1231
+ let node_a = NodePattern {
1232
+ variable: Some(a_identifier.clone()),
1233
+ labels: LabelExpression::String("Station".into()),
1234
+ properties: None,
1235
+ predicate: None,
1236
+ };
1237
+
1238
+ let node_b = NodePattern {
1239
+ variable: Some(b_identifier.clone()),
1240
+ labels: LabelExpression::String("Station".into()),
1241
+ properties: None,
1242
+ predicate: None,
1243
+ };
1244
+
1245
+ let node_n = NodePattern {
1246
+ variable: Some(n_identifier.clone()),
1247
+ labels: LabelExpression::None,
1248
+ properties: None,
1249
+ predicate: None,
1250
+ };
1251
+
1252
+ let node_m = NodePattern {
1253
+ variable: Some(m_identifier.clone()),
1254
+ labels: LabelExpression::None,
1255
+ properties: None,
1256
+ predicate: None,
1257
+ };
1258
+
1259
+ let o_identifier = identifiers.create_variable_from_name("o");
1260
+ let node_o = NodePattern {
1261
+ variable: Some(o_identifier.clone()),
1262
+ labels: LabelExpression::String("Person".into()),
1263
+ properties: None,
1264
+ predicate: None,
1265
+ };
1266
+
1267
+ let edge_pattern = EdgePattern {
1268
+ variable: Some(r_identifier.clone()),
1269
+ source: node_n,
1270
+ destination: node_m,
1271
+ directivity: graphcore::EdgeDirectivity::Directed,
1272
+ labels: LabelExpression::String("KNOWS".into()),
1273
+ properties: None,
1274
+ predicate: None,
1275
+ };
1276
+
1277
+ // Create a multi-element group: (n)-[r:KNOWS]->(m) (o)
1278
+ let quantified_pattern = QuantifiedPathPattern {
1279
+ variable: None,
1280
+ source: node_a,
1281
+ group: vec![Pattern::Edge(edge_pattern), Pattern::Node(node_o)],
1282
+ group_where: None,
1283
+ path_where: None,
1284
+ quantifier: Some(1..5),
1285
+ destination: node_b,
1286
+ selector: PathSelector::All,
1287
+ };
1288
+
1289
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
1290
+ pattern: quantified_pattern,
1291
+ strategy: planner::PathStrategy::Dfs,
1292
+ };
1293
+
1294
+ let logical_plan = LogicalPlan::NodeScan {
1295
+ patterns: vec![planned_pattern],
1296
+ optional: false,
1297
+ };
1298
+
1299
+ let compile_result = compile(&function_manager, vec![logical_plan]);
1300
+ assert!(
1301
+ compile_result.is_ok(),
1302
+ "Expected successful compilation for quantified path without GROUP WHERE, got error: {:?}",
1303
+ compile_result.err()
1304
+ );
1305
+
1306
+ let program = compile_result.unwrap();
1307
+
1308
+ // Find the Match block
1309
+ let match_block = program.iter().find_map(|block| {
1310
+ if let Block::Match { blocks, .. } = block
1311
+ {
1312
+ Some(blocks)
1313
+ }
1314
+ else
1315
+ {
1316
+ None
1317
+ }
1318
+ });
1319
+
1320
+ let match_blocks = match_block.unwrap();
1321
+
1322
+ // Find the MatchQuantifiedPath
1323
+ let BlockMatch::MatchQuantifiedPath {
1324
+ group_where_filter, ..
1325
+ } = match_blocks
1326
+ .iter()
1327
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1328
+ .unwrap()
1329
+ else
1330
+ {
1331
+ unreachable!()
1332
+ };
1333
+
1334
+ // Verify group_where_filter is None (no WHERE clause specified)
1335
+ assert!(
1336
+ group_where_filter.is_none(),
1337
+ "Expected group_where_filter to be None"
1338
+ );
1339
+ }
1340
+
1341
+ #[test]
1342
+ fn test_quantified_path_strategy_is_compiled()
1343
+ {
1344
+ use gqlparser::oc::ast::*;
1345
+
1346
+ let function_manager = functions::Manager::new();
1347
+ let identifiers = VariableIdentifiers::default();
1348
+ let a_identifier = identifiers.create_variable_from_name("a");
1349
+ let b_identifier = identifiers.create_variable_from_name("b");
1350
+
1351
+ let node_a = NodePattern {
1352
+ variable: Some(a_identifier.clone()),
1353
+ labels: LabelExpression::None,
1354
+ properties: None,
1355
+ predicate: None,
1356
+ };
1357
+
1358
+ let node_b = NodePattern {
1359
+ variable: Some(b_identifier),
1360
+ labels: LabelExpression::None,
1361
+ properties: None,
1362
+ predicate: None,
1363
+ };
1364
+
1365
+ let qp_source = node_a.clone();
1366
+ let qp_destination = node_b.clone();
1367
+ let edge_pattern = EdgePattern {
1368
+ variable: None,
1369
+ source: node_a,
1370
+ destination: node_b,
1371
+ directivity: graphcore::EdgeDirectivity::Directed,
1372
+ labels: LabelExpression::String("R".into()),
1373
+ properties: None,
1374
+ predicate: None,
1375
+ };
1376
+
1377
+ // Create a quantified pattern with group.len() > 1 to trigger compile_quantified_path
1378
+ let quantified_pattern = QuantifiedPathPattern {
1379
+ variable: None,
1380
+ source: qp_source,
1381
+ group: vec![
1382
+ Pattern::Edge(edge_pattern.clone()),
1383
+ Pattern::Edge(edge_pattern),
1384
+ ],
1385
+ group_where: None,
1386
+ path_where: None,
1387
+ quantifier: Some(1..3),
1388
+ destination: qp_destination,
1389
+ selector: PathSelector::Shortest(Value { value: 3.into() }.into()),
1390
+ };
1391
+
1392
+ // Create a PlannedPattern with Bfs strategy and Shortest(3)
1393
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
1394
+ pattern: quantified_pattern,
1395
+ strategy: planner::PathStrategy::Bfs {
1396
+ stop_condition: planner::StopCondition::Shortest(Value { value: 3.into() }.into()),
1397
+ },
1398
+ };
1399
+
1400
+ let logical_plan = LogicalPlan::NodeScan {
1401
+ patterns: vec![planned_pattern],
1402
+ optional: false,
1403
+ };
1404
+
1405
+ let compile_result = compile(&function_manager, vec![logical_plan]);
1406
+ assert!(compile_result.is_ok(), "compilation should succeed");
1407
+
1408
+ let compiled = compile_result.unwrap();
1409
+
1410
+ // Find the MatchQuantifiedPath in the compiled output
1411
+ let quantified_match = compiled
1412
+ .iter()
1413
+ .find_map(|block| {
1414
+ if let Block::Match { blocks, .. } = block
1415
+ {
1416
+ blocks
1417
+ .iter()
1418
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1419
+ }
1420
+ else
1421
+ {
1422
+ None
1423
+ }
1424
+ })
1425
+ .expect("should find MatchQuantifiedPath");
1426
+
1427
+ // Verify the strategy field is present and has the correct value
1428
+ match quantified_match
1429
+ {
1430
+ BlockMatch::MatchQuantifiedPath { strategy, .. } =>
1431
+ {
1432
+ match strategy
1433
+ {
1434
+ interpreter::instructions::PathStrategy::Bfs { stop_condition } =>
1435
+ {
1436
+ match stop_condition
1437
+ {
1438
+ interpreter::instructions::StopCondition::Shortest(_) =>
1439
+ {
1440
+ // Success - the strategy was properly compiled
1441
+ }
1442
+ _ => panic!("Expected Shortest stop condition"),
1443
+ }
1444
+ }
1445
+ _ => panic!("Expected Bfs strategy"),
1446
+ }
1447
+ }
1448
+ _ => panic!("Expected MatchQuantifiedPath"),
1449
+ }
1450
+ }
1451
+
1452
+ #[test]
1453
+ fn test_single_element_quantified_path_bfs_strategy_routes_to_quantified_path()
1454
+ {
1455
+ use gqlparser::oc::ast::*;
1456
+
1457
+ let function_manager = functions::Manager::new();
1458
+ let identifiers = VariableIdentifiers::default();
1459
+ let a_identifier = identifiers.create_variable_from_name("a");
1460
+ let b_identifier = identifiers.create_variable_from_name("b");
1461
+
1462
+ let node_a = NodePattern {
1463
+ variable: Some(a_identifier.clone()),
1464
+ labels: LabelExpression::None,
1465
+ properties: None,
1466
+ predicate: None,
1467
+ };
1468
+
1469
+ let node_b = NodePattern {
1470
+ variable: Some(b_identifier),
1471
+ labels: LabelExpression::None,
1472
+ properties: None,
1473
+ predicate: None,
1474
+ };
1475
+
1476
+ let edge_pattern = EdgePattern {
1477
+ variable: None,
1478
+ source: node_a.clone(),
1479
+ destination: node_b.clone(),
1480
+ directivity: graphcore::EdgeDirectivity::Directed,
1481
+ labels: LabelExpression::String("R".into()),
1482
+ properties: None,
1483
+ predicate: None,
1484
+ };
1485
+
1486
+ // Create a quantified pattern with group.len() == 1 but Bfs strategy
1487
+ let quantified_pattern = QuantifiedPathPattern {
1488
+ variable: None,
1489
+ source: node_a,
1490
+ group: vec![Pattern::Edge(edge_pattern)],
1491
+ group_where: None,
1492
+ path_where: None,
1493
+ quantifier: Some(1..3),
1494
+ destination: node_b,
1495
+ selector: PathSelector::Shortest(Value { value: 1.into() }.into()),
1496
+ };
1497
+
1498
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
1499
+ pattern: quantified_pattern,
1500
+ strategy: planner::PathStrategy::Bfs {
1501
+ stop_condition: planner::StopCondition::Shortest(Value { value: 1.into() }.into()),
1502
+ },
1503
+ };
1504
+
1505
+ let logical_plan = LogicalPlan::NodeScan {
1506
+ patterns: vec![planned_pattern],
1507
+ optional: false,
1508
+ };
1509
+
1510
+ let compile_result = compile(&function_manager, vec![logical_plan]);
1511
+ assert!(compile_result.is_ok(), "compilation should succeed");
1512
+
1513
+ let compiled = compile_result.unwrap();
1514
+
1515
+ // Verify we get MatchQuantifiedPath, not MatchEdge
1516
+ let found_quantified_path = compiled.iter().any(|block| {
1517
+ if let Block::Match { blocks, .. } = block
1518
+ {
1519
+ blocks
1520
+ .iter()
1521
+ .any(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1522
+ }
1523
+ else
1524
+ {
1525
+ false
1526
+ }
1527
+ });
1528
+
1529
+ assert!(
1530
+ found_quantified_path,
1531
+ "single-element quantified path with Bfs strategy should route to MatchQuantifiedPath"
1532
+ );
1533
+ }
1534
+
1535
+ #[test]
1536
+ fn test_single_element_quantified_path_dfs_strategy_routes_to_edge()
1537
+ {
1538
+ use gqlparser::oc::ast::*;
1539
+
1540
+ let function_manager = functions::Manager::new();
1541
+ let identifiers = VariableIdentifiers::default();
1542
+ let a_identifier = identifiers.create_variable_from_name("a");
1543
+ let b_identifier = identifiers.create_variable_from_name("b");
1544
+
1545
+ let node_a = NodePattern {
1546
+ variable: Some(a_identifier.clone()),
1547
+ labels: LabelExpression::None,
1548
+ properties: None,
1549
+ predicate: None,
1550
+ };
1551
+
1552
+ let node_b = NodePattern {
1553
+ variable: Some(b_identifier),
1554
+ labels: LabelExpression::None,
1555
+ properties: None,
1556
+ predicate: None,
1557
+ };
1558
+
1559
+ let edge_pattern = EdgePattern {
1560
+ variable: None,
1561
+ source: node_a.clone(),
1562
+ destination: node_b.clone(),
1563
+ directivity: graphcore::EdgeDirectivity::Directed,
1564
+ labels: LabelExpression::String("R".into()),
1565
+ properties: None,
1566
+ predicate: None,
1567
+ };
1568
+
1569
+ // Create a quantified pattern with group.len() == 1 and Dfs strategy
1570
+ let quantified_pattern = QuantifiedPathPattern {
1571
+ variable: None,
1572
+ source: node_a,
1573
+ group: vec![Pattern::Edge(edge_pattern)],
1574
+ group_where: None,
1575
+ path_where: None,
1576
+ quantifier: Some(1..3),
1577
+ destination: node_b,
1578
+ selector: PathSelector::All,
1579
+ };
1580
+
1581
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
1582
+ pattern: quantified_pattern,
1583
+ strategy: planner::PathStrategy::Dfs,
1584
+ };
1585
+
1586
+ let logical_plan = LogicalPlan::NodeScan {
1587
+ patterns: vec![planned_pattern],
1588
+ optional: false,
1589
+ };
1590
+
1591
+ let compile_result = compile(&function_manager, vec![logical_plan]);
1592
+ assert!(compile_result.is_ok(), "compilation should succeed");
1593
+
1594
+ let compiled = compile_result.unwrap();
1595
+
1596
+ // Verify we get MatchEdge, not MatchQuantifiedPath
1597
+ let found_match_edge = compiled.iter().any(|block| {
1598
+ if let Block::Match { blocks, .. } = block
1599
+ {
1600
+ blocks
1601
+ .iter()
1602
+ .any(|block_match| matches!(block_match, BlockMatch::MatchEdge { .. }))
1603
+ }
1604
+ else
1605
+ {
1606
+ false
1607
+ }
1608
+ });
1609
+
1610
+ assert!(
1611
+ found_match_edge,
1612
+ "single-element quantified path with Dfs strategy should route to MatchEdge"
1613
+ );
1614
+ }
1615
+
1616
+ #[test]
1617
+ fn test_compile_shortest_with_literal_k()
1618
+ {
1619
+ use gqlparser::oc::parse_oc_query as parse;
1620
+
1621
+ let function_manager = functions::Manager::new();
1622
+
1623
+ let queries = parse("MATCH p = SHORTEST 3 (a)-[:R*1..3]->(b) RETURN p").unwrap();
1624
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1625
+ else
1626
+ {
1627
+ panic!("Expected Statements query");
1628
+ };
1629
+
1630
+ let compile_result = compile(&function_manager, {
1631
+ statements
1632
+ .into_iter()
1633
+ .map(planner::plan)
1634
+ .collect::<Result<Vec<_>, _>>()
1635
+ .unwrap()
1636
+ });
1637
+
1638
+ assert!(compile_result.is_ok(), "compilation should succeed");
1639
+
1640
+ let compiled = compile_result.unwrap();
1641
+
1642
+ // Find the MatchQuantifiedPath in the compiled output
1643
+ let quantified_match = compiled
1644
+ .iter()
1645
+ .find_map(|block| {
1646
+ if let Block::Match { blocks, .. } = block
1647
+ {
1648
+ blocks
1649
+ .iter()
1650
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1651
+ }
1652
+ else
1653
+ {
1654
+ None
1655
+ }
1656
+ })
1657
+ .expect("should find MatchQuantifiedPath");
1658
+
1659
+ // Verify strategy contains Shortest with compiled instructions
1660
+ match quantified_match
1661
+ {
1662
+ BlockMatch::MatchQuantifiedPath { strategy, .. } =>
1663
+ {
1664
+ match strategy
1665
+ {
1666
+ interpreter::instructions::PathStrategy::Bfs { stop_condition } =>
1667
+ {
1668
+ match stop_condition
1669
+ {
1670
+ interpreter::instructions::StopCondition::Shortest(instructions) =>
1671
+ {
1672
+ // Verify that we have instructions (not just an empty buffer)
1673
+ assert!(
1674
+ !instructions.is_empty(),
1675
+ "Shortest(3) should compile to non-empty instructions"
1676
+ );
1677
+ }
1678
+ _ => panic!("Expected Shortest stop condition, got ShortestGroups"),
1679
+ }
1680
+ }
1681
+ _ => panic!("Expected Bfs strategy"),
1682
+ }
1683
+ }
1684
+ _ => panic!("Expected MatchQuantifiedPath"),
1685
+ }
1686
+ }
1687
+
1688
+ #[test]
1689
+ fn test_compile_shortest_with_parameter_k()
1690
+ {
1691
+ use gqlparser::oc::parse_oc_query as parse;
1692
+
1693
+ let function_manager = functions::Manager::new();
1694
+
1695
+ let queries = parse("MATCH p = SHORTEST $k (a)-[:R*1..3]->(b) RETURN p").unwrap();
1696
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1697
+ else
1698
+ {
1699
+ panic!("Expected Statements query");
1700
+ };
1701
+
1702
+ let compile_result = compile(&function_manager, {
1703
+ statements
1704
+ .into_iter()
1705
+ .map(planner::plan)
1706
+ .collect::<Result<Vec<_>, _>>()
1707
+ .unwrap()
1708
+ });
1709
+
1710
+ assert!(compile_result.is_ok(), "compilation should succeed");
1711
+
1712
+ let compiled = compile_result.unwrap();
1713
+
1714
+ // Find the MatchQuantifiedPath in the compiled output
1715
+ let quantified_match = compiled
1716
+ .iter()
1717
+ .find_map(|block| {
1718
+ if let Block::Match { blocks, .. } = block
1719
+ {
1720
+ blocks
1721
+ .iter()
1722
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1723
+ }
1724
+ else
1725
+ {
1726
+ None
1727
+ }
1728
+ })
1729
+ .expect("should find MatchQuantifiedPath");
1730
+
1731
+ // Verify strategy contains Shortest with compiled instructions that reference the parameter
1732
+ match quantified_match
1733
+ {
1734
+ BlockMatch::MatchQuantifiedPath { strategy, .. } =>
1735
+ {
1736
+ match strategy
1737
+ {
1738
+ interpreter::instructions::PathStrategy::Bfs { stop_condition } =>
1739
+ {
1740
+ match stop_condition
1741
+ {
1742
+ interpreter::instructions::StopCondition::Shortest(instructions) =>
1743
+ {
1744
+ // Verify that we have instructions (not just an empty buffer)
1745
+ assert!(
1746
+ !instructions.is_empty(),
1747
+ "Shortest($k) should compile to non-empty instructions"
1748
+ );
1749
+ // Verify the instructions contain a parameter reference
1750
+ let has_parameter = instructions
1751
+ .iter()
1752
+ .any(|instr| matches!(instr, Instruction::GetParameter { .. }));
1753
+ assert!(
1754
+ has_parameter,
1755
+ "Shortest($k) instructions should contain GetParameter instruction"
1756
+ );
1757
+ }
1758
+ _ => panic!("Expected Shortest stop condition, got ShortestGroups"),
1759
+ }
1760
+ }
1761
+ _ => panic!("Expected Bfs strategy"),
1762
+ }
1763
+ }
1764
+ _ => panic!("Expected MatchQuantifiedPath"),
1765
+ }
1766
+ }
1767
+
1768
+ #[test]
1769
+ fn test_compile_shortest_groups_with_literal_k()
1770
+ {
1771
+ use gqlparser::oc::parse_oc_query as parse;
1772
+
1773
+ let function_manager = functions::Manager::new();
1774
+
1775
+ let queries = parse("MATCH p = SHORTEST 2 GROUPS (a)-[:R*1..3]->(b) RETURN p").unwrap();
1776
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1777
+ else
1778
+ {
1779
+ panic!("Expected Statements query");
1780
+ };
1781
+
1782
+ let compile_result = compile(&function_manager, {
1783
+ statements
1784
+ .into_iter()
1785
+ .map(planner::plan)
1786
+ .collect::<Result<Vec<_>, _>>()
1787
+ .unwrap()
1788
+ });
1789
+
1790
+ assert!(compile_result.is_ok(), "compilation should succeed");
1791
+
1792
+ let compiled = compile_result.unwrap();
1793
+
1794
+ // Find the MatchQuantifiedPath in the compiled output
1795
+ let quantified_match = compiled
1796
+ .iter()
1797
+ .find_map(|block| {
1798
+ if let Block::Match { blocks, .. } = block
1799
+ {
1800
+ blocks
1801
+ .iter()
1802
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
1803
+ }
1804
+ else
1805
+ {
1806
+ None
1807
+ }
1808
+ })
1809
+ .expect("should find MatchQuantifiedPath");
1810
+
1811
+ // Verify strategy contains ShortestGroups (not Shortest)
1812
+ match quantified_match
1813
+ {
1814
+ BlockMatch::MatchQuantifiedPath { strategy, .. } =>
1815
+ {
1816
+ match strategy
1817
+ {
1818
+ interpreter::instructions::PathStrategy::Bfs { stop_condition } =>
1819
+ {
1820
+ match stop_condition
1821
+ {
1822
+ interpreter::instructions::StopCondition::ShortestGroups(instructions) =>
1823
+ {
1824
+ // Verify that we have instructions (not just an empty buffer)
1825
+ assert!(
1826
+ !instructions.is_empty(),
1827
+ "ShortestGroups(2) should compile to non-empty instructions"
1828
+ );
1829
+ }
1830
+ interpreter::instructions::StopCondition::Shortest(_) =>
1831
+ {
1832
+ panic!("Expected ShortestGroups, got Shortest — the distinction is lost!");
1833
+ }
1834
+ }
1835
+ }
1836
+ _ => panic!("Expected Bfs strategy"),
1837
+ }
1838
+ }
1839
+ _ => panic!("Expected MatchQuantifiedPath"),
1840
+ }
1841
+ }
1842
+
1843
+ #[test]
1844
+ fn test_inline_predicate_compiles_to_pattern_filter()
1845
+ {
1846
+ use gqlparser::oc::parse_oc_query as parse;
1847
+
1848
+ let function_manager = functions::Manager::new();
1849
+
1850
+ // Baseline test: inline predicate on pattern
1851
+ let queries = parse("MATCH (a:Node {name: 'test'}) RETURN a").unwrap();
1852
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1853
+ else
1854
+ {
1855
+ panic!("Expected Statements query");
1856
+ };
1857
+
1858
+ let program = compile(&function_manager, {
1859
+ statements
1860
+ .into_iter()
1861
+ .map(planner::plan)
1862
+ .collect::<Result<Vec<_>, _>>()
1863
+ .unwrap()
1864
+ })
1865
+ .unwrap();
1866
+
1867
+ // Find the Match block
1868
+ let match_block = program
1869
+ .iter()
1870
+ .find(|block| matches!(block, Block::Match { .. }))
1871
+ .expect("should find Match block");
1872
+
1873
+ match match_block
1874
+ {
1875
+ Block::Match {
1876
+ blocks: _, filter, ..
1877
+ } =>
1878
+ {
1879
+ // The block-level filter should be empty (no top-level WHERE)
1880
+ assert!(
1881
+ filter.is_empty(),
1882
+ "Block-level filter should be empty when only inline predicate present"
1883
+ );
1884
+ }
1885
+ _ => panic!("Expected Match block"),
1886
+ }
1887
+ }
1888
+
1889
+ #[test]
1890
+ fn test_top_level_where_compiles_to_block_filter()
1891
+ {
1892
+ use gqlparser::oc::parse_oc_query as parse;
1893
+
1894
+ let function_manager = functions::Manager::new();
1895
+
1896
+ // Baseline test: top-level WHERE clause should compile to block-level filter
1897
+ let queries = parse("MATCH (a:Node) WHERE a.value > 10 RETURN a").unwrap();
1898
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1899
+ else
1900
+ {
1901
+ panic!("Expected Statements query");
1902
+ };
1903
+
1904
+ let program = compile(&function_manager, {
1905
+ statements
1906
+ .into_iter()
1907
+ .map(planner::plan)
1908
+ .collect::<Result<Vec<_>, _>>()
1909
+ .unwrap()
1910
+ })
1911
+ .unwrap();
1912
+
1913
+ // Find the Match block
1914
+ let match_block = program
1915
+ .iter()
1916
+ .find(|block| matches!(block, Block::Match { .. }))
1917
+ .expect("should find Match block");
1918
+
1919
+ match match_block
1920
+ {
1921
+ Block::Match { blocks, filter, .. } =>
1922
+ {
1923
+ // The block-level filter should have instructions (the WHERE clause)
1924
+ assert!(
1925
+ !filter.is_empty(),
1926
+ "Block-level filter should contain instructions for WHERE clause"
1927
+ );
1928
+
1929
+ // Pattern filters should not include the WHERE clause
1930
+ let all_patterns_empty = blocks.iter().all(|bm| match bm
1931
+ {
1932
+ BlockMatch::MatchNode { filter, .. } => filter.is_empty(),
1933
+ _ => true,
1934
+ });
1935
+
1936
+ assert!(
1937
+ all_patterns_empty,
1938
+ "Pattern filters should be empty when WHERE is top-level"
1939
+ );
1940
+ }
1941
+ _ => panic!("Expected Match block"),
1942
+ }
1943
+ }
1944
+
1945
+ #[test]
1946
+ fn test_wrapper_group_where_compiles_to_group_where_filter()
1947
+ {
1948
+ use gqlparser::oc::parse_oc_query as parse;
1949
+
1950
+ let function_manager = functions::Manager::new();
1951
+
1952
+ // Test that quantified paths can compile without errors
1953
+ let queries = parse("MATCH (a:A)-[*1..3]->(b:B) RETURN a, b").unwrap();
1954
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1955
+ else
1956
+ {
1957
+ panic!("Expected Statements query");
1958
+ };
1959
+
1960
+ let program = compile(&function_manager, {
1961
+ statements
1962
+ .into_iter()
1963
+ .map(planner::plan)
1964
+ .collect::<Result<Vec<_>, _>>()
1965
+ .unwrap()
1966
+ })
1967
+ .unwrap();
1968
+
1969
+ // Ensure at least one Match block exists
1970
+ let has_match = program
1971
+ .iter()
1972
+ .any(|block| matches!(block, Block::Match { .. }));
1973
+
1974
+ assert!(has_match, "Should have a Match block");
1975
+ }
1976
+
1977
+ #[test]
1978
+ fn test_post_filter_form_compiles_to_block_filter()
1979
+ {
1980
+ use gqlparser::oc::parse_oc_query as parse;
1981
+
1982
+ let function_manager = functions::Manager::new();
1983
+
1984
+ // Equivalent form with WHERE outside the wrapper scope
1985
+ // MATCH p = (:A)-[:R]->(:B) WHERE p RETURN p
1986
+ let queries = parse("MATCH p = (:A)-[:R]->(:B) WHERE p IS NOT NULL RETURN p").unwrap();
1987
+ let gqlparser::oc::ast::Query::Statements(statements) = queries.into_iter().next().unwrap()
1988
+ else
1989
+ {
1990
+ panic!("Expected Statements query");
1991
+ };
1992
+
1993
+ let program = compile(&function_manager, {
1994
+ statements
1995
+ .into_iter()
1996
+ .map(planner::plan)
1997
+ .collect::<Result<Vec<_>, _>>()
1998
+ .unwrap()
1999
+ })
2000
+ .unwrap();
2001
+
2002
+ // Find the Match block
2003
+ let match_block = program
2004
+ .iter()
2005
+ .find(|block| matches!(block, Block::Match { .. }))
2006
+ .expect("should find Match block");
2007
+
2008
+ match match_block
2009
+ {
2010
+ Block::Match {
2011
+ blocks: _, filter, ..
2012
+ } =>
2013
+ {
2014
+ // The block-level filter should have instructions (the WHERE clause)
2015
+ assert!(
2016
+ !filter.is_empty(),
2017
+ "Block-level filter should contain instructions for top-level WHERE clause"
2018
+ );
2019
+ }
2020
+ _ => panic!("Expected Match block"),
2021
+ }
2022
+ }
2023
+
2024
+ // WI#669: Tests for path_filter field on MatchQuantifiedPath
2025
+ #[test]
2026
+ fn test_genuine_group_predicate_compiles_to_group_where_filter()
2027
+ {
2028
+ use crate::interpreter::instructions::BlockMatch;
2029
+ use gqlparser::oc::ast::*;
2030
+
2031
+ let function_manager = functions::Manager::new();
2032
+ let identifiers = VariableIdentifiers::default();
2033
+ let a_identifier = identifiers.create_variable_from_name("a");
2034
+ let b_identifier = identifiers.create_variable_from_name("b");
2035
+ let n_identifier = identifiers.create_variable_from_name("n");
2036
+ let r_identifier = identifiers.create_variable_from_name("r");
2037
+ let m_identifier = identifiers.create_variable_from_name("m");
2038
+
2039
+ // Construct a multi-element group with a group_where predicate
2040
+ let node_a = NodePattern {
2041
+ variable: Some(a_identifier.clone()),
2042
+ labels: LabelExpression::None,
2043
+ properties: None,
2044
+ predicate: None,
2045
+ };
2046
+
2047
+ let node_b = NodePattern {
2048
+ variable: Some(b_identifier),
2049
+ labels: LabelExpression::None,
2050
+ properties: None,
2051
+ predicate: None,
2052
+ };
2053
+
2054
+ let node_n = NodePattern {
2055
+ variable: Some(n_identifier.clone()),
2056
+ labels: LabelExpression::None,
2057
+ properties: None,
2058
+ predicate: None,
2059
+ };
2060
+
2061
+ let node_m = NodePattern {
2062
+ variable: Some(m_identifier),
2063
+ labels: LabelExpression::None,
2064
+ properties: None,
2065
+ predicate: None,
2066
+ };
2067
+
2068
+ // Create edge pattern: (n)-[r]->(m)
2069
+ let edge_pattern = EdgePattern {
2070
+ variable: Some(r_identifier),
2071
+ source: node_n.clone(),
2072
+ destination: node_m.clone(),
2073
+ directivity: graphcore::EdgeDirectivity::Directed,
2074
+ labels: LabelExpression::None,
2075
+ properties: None,
2076
+ predicate: None,
2077
+ };
2078
+
2079
+ // Create edge pattern 2: (m)-[s]->(p) to make multi-element group
2080
+ let node_p = NodePattern {
2081
+ variable: Some(identifiers.create_variable_from_name("p")),
2082
+ labels: LabelExpression::None,
2083
+ properties: None,
2084
+ predicate: None,
2085
+ };
2086
+
2087
+ let edge_pattern_2 = EdgePattern {
2088
+ variable: Some(identifiers.create_variable_from_name("s")),
2089
+ source: node_m,
2090
+ destination: node_p,
2091
+ directivity: graphcore::EdgeDirectivity::Directed,
2092
+ labels: LabelExpression::None,
2093
+ properties: None,
2094
+ predicate: None,
2095
+ };
2096
+
2097
+ // Create the group_where expression: n.x > 1
2098
+ let group_where = Some(Expression::RelationalSuperior(Box::new(
2099
+ RelationalSuperior {
2100
+ left: Expression::MemberAccess(Box::new(MemberAccess {
2101
+ left: Expression::Variable(Variable {
2102
+ identifier: n_identifier.clone(),
2103
+ }),
2104
+ path: vec!["x".to_string()],
2105
+ })),
2106
+ right: Expression::Value(Value { value: 1i64.into() }),
2107
+ },
2108
+ )));
2109
+
2110
+ // Create quantified pattern with group_where predicate
2111
+ let quantified_pattern = QuantifiedPathPattern {
2112
+ variable: None,
2113
+ source: node_a,
2114
+ group: vec![Pattern::Edge(edge_pattern), Pattern::Edge(edge_pattern_2)],
2115
+ group_where,
2116
+ path_where: None, // No wrapper predicate
2117
+ quantifier: Some(1..5),
2118
+ destination: node_b,
2119
+ selector: PathSelector::All,
2120
+ };
2121
+
2122
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
2123
+ pattern: quantified_pattern,
2124
+ strategy: planner::PathStrategy::Dfs,
2125
+ };
2126
+
2127
+ let logical_plan = LogicalPlan::NodeScan {
2128
+ patterns: vec![planned_pattern],
2129
+ optional: false,
2130
+ };
2131
+
2132
+ let program = compile(&function_manager, vec![logical_plan]).unwrap();
2133
+
2134
+ // Find the MatchQuantifiedPath instruction and verify field placement
2135
+ let mut found_group_where = false;
2136
+ for block in &program
2137
+ {
2138
+ if let crate::interpreter::instructions::Block::Match { blocks, .. } = block
2139
+ {
2140
+ for match_block in blocks
2141
+ {
2142
+ if let BlockMatch::MatchQuantifiedPath {
2143
+ path_filter,
2144
+ group_where_filter,
2145
+ ..
2146
+ } = match_block
2147
+ {
2148
+ // For genuine groups, group_where_filter should have instructions
2149
+ // and path_filter should be None
2150
+ if group_where_filter.is_some()
2151
+ {
2152
+ found_group_where = true;
2153
+ assert!(
2154
+ path_filter.is_none(),
2155
+ "Genuine group predicate should not land in path_filter, found both path_filter and group_where_filter as Some"
2156
+ );
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+ }
2162
+
2163
+ assert!(
2164
+ found_group_where,
2165
+ "Should have found group_where_filter for genuine group with WHERE clause"
2166
+ );
2167
+ }
2168
+
2169
+ #[test]
2170
+ fn test_path_filter_and_group_where_filter_never_both_some()
2171
+ {
2172
+ use crate::interpreter::instructions::BlockMatch;
2173
+ use gqlparser::oc::ast::*;
2174
+
2175
+ let function_manager = functions::Manager::new();
2176
+ let identifiers = VariableIdentifiers::default();
2177
+ let a_identifier = identifiers.create_variable_from_name("a");
2178
+ let b_identifier = identifiers.create_variable_from_name("b");
2179
+ let n_identifier = identifiers.create_variable_from_name("n");
2180
+ let r_identifier = identifiers.create_variable_from_name("r");
2181
+ let m_identifier = identifiers.create_variable_from_name("m");
2182
+
2183
+ // Construct a multi-element group with a group_where predicate
2184
+ let node_a = NodePattern {
2185
+ variable: Some(a_identifier.clone()),
2186
+ labels: LabelExpression::None,
2187
+ properties: None,
2188
+ predicate: None,
2189
+ };
2190
+
2191
+ let node_b = NodePattern {
2192
+ variable: Some(b_identifier),
2193
+ labels: LabelExpression::None,
2194
+ properties: None,
2195
+ predicate: None,
2196
+ };
2197
+
2198
+ let node_n = NodePattern {
2199
+ variable: Some(n_identifier.clone()),
2200
+ labels: LabelExpression::None,
2201
+ properties: None,
2202
+ predicate: None,
2203
+ };
2204
+
2205
+ let node_m = NodePattern {
2206
+ variable: Some(m_identifier),
2207
+ labels: LabelExpression::None,
2208
+ properties: None,
2209
+ predicate: None,
2210
+ };
2211
+
2212
+ let edge_pattern = EdgePattern {
2213
+ variable: Some(r_identifier),
2214
+ source: node_n.clone(),
2215
+ destination: node_m.clone(),
2216
+ directivity: graphcore::EdgeDirectivity::Directed,
2217
+ labels: LabelExpression::None,
2218
+ properties: None,
2219
+ predicate: None,
2220
+ };
2221
+
2222
+ let node_p = NodePattern {
2223
+ variable: Some(identifiers.create_variable_from_name("p")),
2224
+ labels: LabelExpression::None,
2225
+ properties: None,
2226
+ predicate: None,
2227
+ };
2228
+
2229
+ let edge_pattern_2 = EdgePattern {
2230
+ variable: Some(identifiers.create_variable_from_name("s")),
2231
+ source: node_m,
2232
+ destination: node_p,
2233
+ directivity: graphcore::EdgeDirectivity::Directed,
2234
+ labels: LabelExpression::None,
2235
+ properties: None,
2236
+ predicate: None,
2237
+ };
2238
+
2239
+ // Create quantified pattern with only group_where (no path_where)
2240
+ let group_where = Some(Expression::RelationalSuperior(Box::new(
2241
+ RelationalSuperior {
2242
+ left: Expression::MemberAccess(Box::new(MemberAccess {
2243
+ left: Expression::Variable(Variable {
2244
+ identifier: n_identifier.clone(),
2245
+ }),
2246
+ path: vec!["x".to_string()],
2247
+ })),
2248
+ right: Expression::Value(Value { value: 1i64.into() }),
2249
+ },
2250
+ )));
2251
+
2252
+ let quantified_pattern = QuantifiedPathPattern {
2253
+ variable: None,
2254
+ source: node_a,
2255
+ group: vec![Pattern::Edge(edge_pattern), Pattern::Edge(edge_pattern_2)],
2256
+ group_where,
2257
+ path_where: None,
2258
+ quantifier: Some(1..5),
2259
+ destination: node_b,
2260
+ selector: PathSelector::All,
2261
+ };
2262
+
2263
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
2264
+ pattern: quantified_pattern,
2265
+ strategy: planner::PathStrategy::Dfs,
2266
+ };
2267
+
2268
+ let logical_plan = LogicalPlan::NodeScan {
2269
+ patterns: vec![planned_pattern],
2270
+ optional: false,
2271
+ };
2272
+
2273
+ let program = compile(&function_manager, vec![logical_plan]).unwrap();
2274
+
2275
+ // Verify: both path_filter and group_where_filter should not be Some simultaneously
2276
+ for block in &program
2277
+ {
2278
+ if let crate::interpreter::instructions::Block::Match { blocks, .. } = block
2279
+ {
2280
+ for match_block in blocks
2281
+ {
2282
+ if let BlockMatch::MatchQuantifiedPath {
2283
+ path_filter,
2284
+ group_where_filter,
2285
+ ..
2286
+ } = match_block
2287
+ {
2288
+ assert!(
2289
+ !(group_where_filter.is_some() && path_filter.is_some()),
2290
+ "Both path_filter and group_where_filter should never be Some on the same MatchQuantifiedPath — \
2291
+ path_filter is for wrapper predicates, group_where_filter is for genuine group predicates"
2292
+ );
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ }
2298
+
2299
+ // WI#670: Tests for PathUniqueness seam
2300
+ #[test]
2301
+ fn test_path_uniqueness_trail_is_reusable_empty_set()
2302
+ {
2303
+ use crate::graph::Key;
2304
+ use crate::interpreter::path::PathUniqueness;
2305
+ use rustc_hash::FxHashSet;
2306
+
2307
+ let uniqueness = PathUniqueness::Trail;
2308
+ let mut used_edges: FxHashSet<Key> = FxHashSet::default();
2309
+
2310
+ // Create mock edge keys
2311
+ let edge_key = Key::new(1);
2312
+
2313
+ // Initially, edge should be reusable (not in used set)
2314
+ assert!(
2315
+ uniqueness.is_reusable(&used_edges, &edge_key),
2316
+ "Edge should be reusable when not in used_edges set"
2317
+ );
2318
+
2319
+ // After marking used, edge should not be reusable
2320
+ uniqueness.mark_used(&mut used_edges, edge_key);
2321
+ assert!(
2322
+ !uniqueness.is_reusable(&used_edges, &edge_key),
2323
+ "Edge should not be reusable after mark_used"
2324
+ );
2325
+
2326
+ // After marking unused, edge should be reusable again
2327
+ uniqueness.mark_unused(&mut used_edges, &edge_key);
2328
+ assert!(
2329
+ uniqueness.is_reusable(&used_edges, &edge_key),
2330
+ "Edge should be reusable after mark_unused"
2331
+ );
2332
+ }
2333
+
2334
+ #[test]
2335
+ fn test_path_uniqueness_trail_round_trip()
2336
+ {
2337
+ use crate::graph::Key;
2338
+ use crate::interpreter::path::PathUniqueness;
2339
+ use rustc_hash::FxHashSet;
2340
+
2341
+ let uniqueness = PathUniqueness::Trail;
2342
+ let mut used_edges: FxHashSet<Key> = FxHashSet::default();
2343
+
2344
+ let edge_key_1 = Key::new(1);
2345
+ let edge_key_2 = Key::new(2);
2346
+
2347
+ // Mark multiple edges as used
2348
+ uniqueness.mark_used(&mut used_edges, edge_key_1);
2349
+ uniqueness.mark_used(&mut used_edges, edge_key_2);
2350
+
2351
+ // Both should be non-reusable
2352
+ assert!(!uniqueness.is_reusable(&used_edges, &edge_key_1));
2353
+ assert!(!uniqueness.is_reusable(&used_edges, &edge_key_2));
2354
+
2355
+ // Mark first edge as unused
2356
+ uniqueness.mark_unused(&mut used_edges, &edge_key_1);
2357
+
2358
+ // First should now be reusable, second should still be non-reusable
2359
+ assert!(uniqueness.is_reusable(&used_edges, &edge_key_1));
2360
+ assert!(!uniqueness.is_reusable(&used_edges, &edge_key_2));
2361
+
2362
+ // Mark second edge as unused
2363
+ uniqueness.mark_unused(&mut used_edges, &edge_key_2);
2364
+
2365
+ // Both should now be reusable again
2366
+ assert!(uniqueness.is_reusable(&used_edges, &edge_key_1));
2367
+ assert!(uniqueness.is_reusable(&used_edges, &edge_key_2));
2368
+
2369
+ // Set should be empty
2370
+ assert!(
2371
+ used_edges.is_empty(),
2372
+ "used_edges should be empty after all edges unmarked"
2373
+ );
2374
+ }
2375
+
2376
+ #[test]
2377
+ fn test_path_uniqueness_trail_preserves_state()
2378
+ {
2379
+ use crate::graph::Key;
2380
+ use crate::interpreter::path::PathUniqueness;
2381
+ use rustc_hash::FxHashSet;
2382
+
2383
+ let uniqueness = PathUniqueness::Trail;
2384
+ let mut used_edges: FxHashSet<Key> = FxHashSet::default();
2385
+
2386
+ let edge_key_1 = Key::new(1);
2387
+ let edge_key_2 = Key::new(2);
2388
+ let edge_key_3 = Key::new(3);
2389
+
2390
+ // Simulate a DFS path traversal: mark edges as we go down, unmark as we backtrack
2391
+ uniqueness.mark_used(&mut used_edges, edge_key_1);
2392
+ assert_eq!(used_edges.len(), 1);
2393
+
2394
+ uniqueness.mark_used(&mut used_edges, edge_key_2);
2395
+ assert_eq!(used_edges.len(), 2);
2396
+
2397
+ uniqueness.mark_used(&mut used_edges, edge_key_3);
2398
+ assert_eq!(used_edges.len(), 3);
2399
+
2400
+ // Backtrack from edge_3
2401
+ uniqueness.mark_unused(&mut used_edges, &edge_key_3);
2402
+ assert_eq!(used_edges.len(), 2);
2403
+ assert!(!used_edges.contains(&edge_key_3));
2404
+ assert!(used_edges.contains(&edge_key_1));
2405
+ assert!(used_edges.contains(&edge_key_2));
2406
+
2407
+ // Backtrack from edge_2
2408
+ uniqueness.mark_unused(&mut used_edges, &edge_key_2);
2409
+ assert_eq!(used_edges.len(), 1);
2410
+ assert!(!used_edges.contains(&edge_key_2));
2411
+ assert!(used_edges.contains(&edge_key_1));
2412
+
2413
+ // Backtrack from edge_1
2414
+ uniqueness.mark_unused(&mut used_edges, &edge_key_1);
2415
+ assert_eq!(used_edges.len(), 0);
2416
+ assert!(used_edges.is_empty());
2417
+ }
2418
+
2419
+ // WI689 Tests: Group terminal variable tracking
2420
+
2421
+ #[test]
2422
+ fn test_group_terminal_variable_compiled_for_unnamed_node()
2423
+ {
2424
+ use gqlparser::oc::ast::*;
2425
+
2426
+ let function_manager = functions::Manager::new();
2427
+ let identifiers = VariableIdentifiers::default();
2428
+ let a_identifier = identifiers.create_variable_from_name("a");
2429
+ let b_identifier = identifiers.create_variable_from_name("b");
2430
+
2431
+ let node_a = NodePattern {
2432
+ variable: Some(a_identifier.clone()),
2433
+ labels: LabelExpression::None,
2434
+ properties: None,
2435
+ predicate: None,
2436
+ };
2437
+
2438
+ let node_b = NodePattern {
2439
+ variable: Some(b_identifier),
2440
+ labels: LabelExpression::None,
2441
+ properties: None,
2442
+ predicate: None,
2443
+ };
2444
+
2445
+ let qp_source = node_a.clone();
2446
+ let qp_destination = node_b.clone();
2447
+
2448
+ // Inner group: unnamed nodes with one edge
2449
+ let node_n = NodePattern {
2450
+ variable: Some(identifiers.create_variable_from_name("n")),
2451
+ labels: LabelExpression::None,
2452
+ properties: None,
2453
+ predicate: None,
2454
+ };
2455
+
2456
+ let node_m = NodePattern {
2457
+ variable: Some(identifiers.create_variable_from_name("m")),
2458
+ labels: LabelExpression::None,
2459
+ properties: None,
2460
+ predicate: None,
2461
+ };
2462
+
2463
+ let edge_pattern = EdgePattern {
2464
+ variable: Some(identifiers.create_variable_from_name("r")),
2465
+ source: node_n.clone(),
2466
+ destination: node_m,
2467
+ directivity: graphcore::EdgeDirectivity::Directed,
2468
+ labels: LabelExpression::String("R".into()),
2469
+ properties: None,
2470
+ predicate: None,
2471
+ };
2472
+
2473
+ let quantified_pattern = QuantifiedPathPattern {
2474
+ variable: None,
2475
+ source: qp_source,
2476
+ group: vec![Pattern::Node(node_n), Pattern::Edge(edge_pattern)],
2477
+ group_where: None,
2478
+ path_where: None,
2479
+ quantifier: Some(1..5),
2480
+ destination: qp_destination,
2481
+ selector: PathSelector::All,
2482
+ };
2483
+
2484
+ // Create a PlannedPattern with Dfs strategy (default)
2485
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
2486
+ pattern: quantified_pattern,
2487
+ strategy: planner::PathStrategy::Dfs,
2488
+ };
2489
+
2490
+ let logical_plan = LogicalPlan::NodeScan {
2491
+ patterns: vec![planned_pattern],
2492
+ optional: false,
2493
+ };
2494
+
2495
+ let compile_result = compile(&function_manager, vec![logical_plan]);
2496
+ assert!(compile_result.is_ok(), "compilation should succeed");
2497
+
2498
+ let compiled = compile_result.unwrap();
2499
+
2500
+ // Find the MatchQuantifiedPath in the compiled output
2501
+ let quantified_match = compiled
2502
+ .iter()
2503
+ .find_map(|block| {
2504
+ if let Block::Match { blocks, .. } = block
2505
+ {
2506
+ blocks
2507
+ .iter()
2508
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
2509
+ }
2510
+ else
2511
+ {
2512
+ None
2513
+ }
2514
+ })
2515
+ .expect("should find MatchQuantifiedPath");
2516
+
2517
+ // Verify the group_terminal_variable field exists and is set
2518
+ match quantified_match
2519
+ {
2520
+ BlockMatch::MatchQuantifiedPath {
2521
+ group_terminal_variable: _,
2522
+ group_variables,
2523
+ ..
2524
+ } =>
2525
+ {
2526
+ // The group_terminal_variable should be a valid column ID
2527
+ // (we can't easily check the exact value, but we can verify it exists)
2528
+ assert!(
2529
+ !group_variables.is_empty(),
2530
+ "group_variables should not be empty"
2531
+ );
2532
+ // group_terminal_variable should point to one of the group variables
2533
+ // (this will be verified by not panicking during compilation)
2534
+ }
2535
+ _ => panic!("Expected MatchQuantifiedPath"),
2536
+ }
2537
+ }
2538
+
2539
+ #[test]
2540
+ fn test_group_terminal_variable_compiled_for_named_node()
2541
+ {
2542
+ use gqlparser::oc::ast::*;
2543
+
2544
+ let function_manager = functions::Manager::new();
2545
+ let identifiers = VariableIdentifiers::default();
2546
+ let a_identifier = identifiers.create_variable_from_name("a");
2547
+ let b_identifier = identifiers.create_variable_from_name("b");
2548
+
2549
+ let node_a = NodePattern {
2550
+ variable: Some(a_identifier.clone()),
2551
+ labels: LabelExpression::None,
2552
+ properties: None,
2553
+ predicate: None,
2554
+ };
2555
+
2556
+ let node_b = NodePattern {
2557
+ variable: Some(b_identifier),
2558
+ labels: LabelExpression::None,
2559
+ properties: None,
2560
+ predicate: None,
2561
+ };
2562
+
2563
+ let qp_source = node_a.clone();
2564
+ let qp_destination = node_b.clone();
2565
+
2566
+ // Inner group: named nodes with one edge
2567
+ let node_n = NodePattern {
2568
+ variable: Some(identifiers.create_variable_from_name("n")),
2569
+ labels: LabelExpression::None,
2570
+ properties: None,
2571
+ predicate: None,
2572
+ };
2573
+
2574
+ let node_m = NodePattern {
2575
+ variable: Some(identifiers.create_variable_from_name("m")),
2576
+ labels: LabelExpression::None,
2577
+ properties: None,
2578
+ predicate: None,
2579
+ };
2580
+
2581
+ let edge_pattern = EdgePattern {
2582
+ variable: Some(identifiers.create_variable_from_name("r")),
2583
+ source: node_n.clone(),
2584
+ destination: node_m,
2585
+ directivity: graphcore::EdgeDirectivity::Directed,
2586
+ labels: LabelExpression::String("R".into()),
2587
+ properties: None,
2588
+ predicate: None,
2589
+ };
2590
+
2591
+ let quantified_pattern = QuantifiedPathPattern {
2592
+ variable: None,
2593
+ source: qp_source,
2594
+ group: vec![Pattern::Node(node_n), Pattern::Edge(edge_pattern)],
2595
+ group_where: None,
2596
+ path_where: None,
2597
+ quantifier: Some(1..5),
2598
+ destination: qp_destination,
2599
+ selector: PathSelector::All,
2600
+ };
2601
+
2602
+ // Create a PlannedPattern with Dfs strategy (default)
2603
+ let planned_pattern = planner::PlannedPattern::QuantifiedPath {
2604
+ pattern: quantified_pattern,
2605
+ strategy: planner::PathStrategy::Dfs,
2606
+ };
2607
+
2608
+ let logical_plan = LogicalPlan::NodeScan {
2609
+ patterns: vec![planned_pattern],
2610
+ optional: false,
2611
+ };
2612
+
2613
+ let compile_result = compile(&function_manager, vec![logical_plan]);
2614
+ assert!(compile_result.is_ok(), "compilation should succeed");
2615
+
2616
+ let compiled = compile_result.unwrap();
2617
+
2618
+ // Find the MatchQuantifiedPath in the compiled output
2619
+ let quantified_match = compiled
2620
+ .iter()
2621
+ .find_map(|block| {
2622
+ if let Block::Match { blocks, .. } = block
2623
+ {
2624
+ blocks
2625
+ .iter()
2626
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
2627
+ }
2628
+ else
2629
+ {
2630
+ None
2631
+ }
2632
+ })
2633
+ .expect("should find MatchQuantifiedPath");
2634
+
2635
+ // Verify the group_terminal_variable field exists and is set
2636
+ match quantified_match
2637
+ {
2638
+ BlockMatch::MatchQuantifiedPath {
2639
+ group_terminal_variable: _,
2640
+ group_variables,
2641
+ ..
2642
+ } =>
2643
+ {
2644
+ // The group_terminal_variable should be a valid column ID
2645
+ assert!(
2646
+ !group_variables.is_empty(),
2647
+ "group_variables should not be empty"
2648
+ );
2649
+ // group_terminal_variable should point to one of the group variables
2650
+ }
2651
+ _ => panic!("Expected MatchQuantifiedPath"),
2652
+ }
2653
+ }
2654
+
2655
+ #[test]
2656
+ fn test_path_filter_shortest_query_compiles()
2657
+ {
2658
+ // WI#711: Verify that SHORTEST queries with WHERE clauses compile correctly
2659
+ // and produce a MatchQuantifiedPath block
2660
+ //
2661
+ // Query: MATCH p = SHORTEST 1 (a:Station)-[:LINK]-+(d:Station) WHERE length(p) % 2 = 0 RETURN p
2662
+ //
2663
+ // This test verifies the compilation pipeline handles path filtering queries correctly.
2664
+
2665
+ use gqlparser::oc::parse_oc_query as parse;
2666
+
2667
+ let function_manager = functions::Manager::new();
2668
+
2669
+ let query_str = "MATCH p = SHORTEST 1 (a:Station {name:'A'})-[:LINK]-+(d:Station {name:'D'}) WHERE length(p) % 2 = 0 RETURN p";
2670
+ let queries = parse(query_str).expect("Should parse successfully");
2671
+
2672
+ let gqlparser::oc::ast::Query::Statements(statements) =
2673
+ queries.into_iter().next().expect("Should have a query")
2674
+ else
2675
+ {
2676
+ panic!("Expected Statements query");
2677
+ };
2678
+
2679
+ let program = compile_statements(&function_manager, statements);
2680
+
2681
+ // Find the Match block
2682
+ let match_block = program.iter().find_map(|block| {
2683
+ if let Block::Match { blocks, .. } = block
2684
+ {
2685
+ Some(blocks)
2686
+ }
2687
+ else
2688
+ {
2689
+ None
2690
+ }
2691
+ });
2692
+
2693
+ assert!(
2694
+ match_block.is_some(),
2695
+ "Expected to find a Match block in the program"
2696
+ );
2697
+
2698
+ let match_blocks = match_block.unwrap();
2699
+
2700
+ // Find the MatchQuantifiedPath
2701
+ let quantified_match = match_blocks
2702
+ .iter()
2703
+ .find(|block_match| matches!(block_match, BlockMatch::MatchQuantifiedPath { .. }))
2704
+ .expect("Expected to find a MatchQuantifiedPath block for SHORTEST query");
2705
+
2706
+ // Just verify it's a MatchQuantifiedPath - the WHERE clause handling
2707
+ // is verified through end-to-end evaluator tests, not compiler structure
2708
+ assert!(
2709
+ matches!(quantified_match, BlockMatch::MatchQuantifiedPath { .. }),
2710
+ "MatchQuantifiedPath should be present in compiled SHORTEST query"
2711
+ );
2712
+ }
2713
+
2714
+ #[test]
2715
+ fn test_compile_group_single_edge_range()
2716
+ {
2717
+ let function_manager = functions::Manager::new();
2718
+
2719
+ let program = compile_statements(&function_manager, ast::group_single_edge_range());
2720
+ compare_program(program, programs::group_single_edge_range())
2721
+ }
2722
+
2723
+ #[test]
2724
+ fn test_compile_group_two_edges_range()
2725
+ {
2726
+ let function_manager = functions::Manager::new();
2727
+
2728
+ let program = compile_statements(&function_manager, ast::group_two_edges_range());
2729
+ compare_program(program, programs::group_two_edges_range())
2730
+ }
2731
+
2732
+ #[test]
2733
+ fn test_compile_shortest_single_edge_full_return()
2734
+ {
2735
+ let function_manager = functions::Manager::new();
2736
+
2737
+ let program = compile_statements(&function_manager, ast::shortest_single_edge_full_return());
2738
+ compare_program(
2739
+ program,
2740
+ programs::shortest_single_edge_full_return(&function_manager),
2741
+ )
2742
+ }