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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +8 -8
- data/ext/db-index/Cargo.toml +2 -2
- data/ext/gqlitedb/Cargo.toml +0 -2
- data/ext/gqlitedb/src/aggregators.rs +28 -0
- data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
- data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
- data/ext/gqlitedb/src/compiler.rs +506 -48
- data/ext/gqlitedb/src/connection.rs +14 -2
- data/ext/gqlitedb/src/consts.rs +1 -1
- data/ext/gqlitedb/src/error.rs +31 -0
- data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
- data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
- data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
- data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
- data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
- data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
- data/ext/gqlitedb/src/interpreter/path.rs +542 -0
- data/ext/gqlitedb/src/planner.rs +231 -6
- data/ext/gqlitedb/src/query_result.rs +19 -4
- data/ext/gqlitedb/src/store/redb.rs +1 -1
- data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
- data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
- data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
- data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
- data/ext/gqlitedb/src/tests/connection.rs +101 -0
- data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
- data/ext/gqlitedb/src/tests/parser.rs +1225 -0
- data/ext/gqlitedb/src/tests/planner.rs +433 -2
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
- data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
- data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
- data/ext/gqliterb/Cargo.toml +0 -1
- data/ext/gqlparser/src/oc/ast.rs +72 -3
- data/ext/gqlparser/src/oc/error.rs +18 -0
- data/ext/gqlparser/src/oc/lexer.rs +157 -2
- data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
- data/ext/gqlparser/src/oc/parser.rs +1312 -182
- data/ext/gqlparser/src/oc/string.rs +172 -0
- data/ext/gqlparser/src/oc.rs +1 -0
- data/ext/graphcore/src/value/value_map.rs +2 -2
- data/ext/graphcore/src/value.rs +13 -1
- metadata +6 -2
- 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![
|
|
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,
|
|
@@ -248,6 +250,7 @@ fn test_compile_logicalplan_projection_modifiers()
|
|
|
248
250
|
asc: false,
|
|
249
251
|
instructions: vec![Instruction::GetVariable { col_id: 0 }],
|
|
250
252
|
}],
|
|
253
|
+
distinct: false,
|
|
251
254
|
},
|
|
252
255
|
variables_sizes: VariablesSizes {
|
|
253
256
|
persistent_variables: 1,
|
|
@@ -287,3 +290,2453 @@ fn test_compile_multiple_vector_indexes_produces_distinct_instructions()
|
|
|
287
290
|
assert!(debug.contains("\"idx1\""));
|
|
288
291
|
assert!(debug.contains("\"idx2\""));
|
|
289
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
|
+
}
|