#![cfg(feature = "cypher")]
use grafeo_common::types::Value;
use grafeo_engine::{Config, GrafeoDB};
fn create_star_graph(db: &GrafeoDB) {
let session = db.session();
for i in 0..3 {
session
.create_node_with_props(&["Person"], [("id", Value::Int64(i))])
.unwrap();
}
for i in 10..14 {
session
.create_node_with_props(&["Person"], [("id", Value::Int64(i))])
.unwrap();
}
for i in 20..22 {
session
.create_node_with_props(&["Person"], [("id", Value::Int64(i))])
.unwrap();
}
for i in 30..33 {
session
.create_node_with_props(&["Person"], [("id", Value::Int64(i))])
.unwrap();
}
session.execute_cypher("MATCH (a:Person {id: 0}), (b:Person) WHERE b.id >= 10 AND b.id < 14 CREATE (a)-[:KNOWS]->(b)").unwrap();
session.execute_cypher("MATCH (a:Person {id: 1}), (b:Person) WHERE b.id >= 20 AND b.id < 22 CREATE (a)-[:KNOWS]->(b)").unwrap();
session.execute_cypher("MATCH (a:Person {id: 2}), (b:Person) WHERE b.id >= 30 AND b.id < 33 CREATE (a)-[:KNOWS]->(b)").unwrap();
session
.execute_cypher("MATCH (a:Person) WHERE a.id = 10 CREATE (a)-[:KNOWS]->(:Person {id: 100})")
.unwrap();
session
.execute_cypher("MATCH (a:Person) WHERE a.id = 10 CREATE (a)-[:KNOWS]->(:Person {id: 101})")
.unwrap();
session
.execute_cypher("MATCH (a:Person) WHERE a.id = 11 CREATE (a)-[:KNOWS]->(:Person {id: 110})")
.unwrap();
session
.execute_cypher("MATCH (a:Person) WHERE a.id = 20 CREATE (a)-[:KNOWS]->(:Person {id: 200})")
.unwrap();
}
#[test]
fn test_count_with_factorized_execution() {
let db = GrafeoDB::new_in_memory();
create_star_graph(&db);
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b) RETURN COUNT(b)")
.unwrap();
println!("1-hop COUNT(b): {:?}", result.iter().collect::<Vec<_>>());
assert_eq!(result.row_count(), 1);
let count = result.iter().next().unwrap()[0].as_int64().unwrap();
println!("Total 1-hop paths: {}", count);
assert!(count >= 9, "Should have at least 9 one-hop paths");
}
#[test]
fn test_count_two_hop_factorized() {
let db = GrafeoDB::new_in_memory();
create_star_graph(&db);
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c) RETURN COUNT(c)")
.unwrap();
println!("2-hop COUNT(c): {:?}", result.iter().collect::<Vec<_>>());
assert_eq!(result.row_count(), 1);
let count = result.iter().next().unwrap()[0].as_int64().unwrap();
println!("Total 2-hop paths: {}", count);
assert!(count > 0, "Should have some 2-hop paths");
}
#[test]
fn test_factorized_vs_flat_count_correctness() {
let db_factorized = GrafeoDB::new_in_memory();
let db_flat = GrafeoDB::with_config(Config::default().without_factorized_execution()).unwrap();
for db in [&db_factorized, &db_flat] {
let session = db.session();
session
.create_node_with_props(&["Node"], [("name", Value::String("A".into()))])
.unwrap();
session
.create_node_with_props(&["Node"], [("name", Value::String("B1".into()))])
.unwrap();
session
.create_node_with_props(&["Node"], [("name", Value::String("B2".into()))])
.unwrap();
session
.create_node_with_props(&["Node"], [("name", Value::String("C1".into()))])
.unwrap();
session
.create_node_with_props(&["Node"], [("name", Value::String("C2".into()))])
.unwrap();
session
.execute_cypher(
"MATCH (a:Node {name: 'A'}), (b:Node {name: 'B1'}) CREATE (a)-[:LINK]->(b)",
)
.unwrap();
session
.execute_cypher(
"MATCH (a:Node {name: 'A'}), (b:Node {name: 'B2'}) CREATE (a)-[:LINK]->(b)",
)
.unwrap();
session
.execute_cypher(
"MATCH (a:Node {name: 'B1'}), (c:Node {name: 'C1'}) CREATE (a)-[:LINK]->(c)",
)
.unwrap();
session
.execute_cypher(
"MATCH (a:Node {name: 'B1'}), (c:Node {name: 'C2'}) CREATE (a)-[:LINK]->(c)",
)
.unwrap();
session
.execute_cypher(
"MATCH (a:Node {name: 'B2'}), (c:Node {name: 'C1'}) CREATE (a)-[:LINK]->(c)",
)
.unwrap();
}
let factorized_result = db_factorized
.session()
.execute("MATCH (a:Node {name: 'A'})-[:LINK]->(b)-[:LINK]->(c) RETURN COUNT(c)")
.unwrap();
let flat_result = db_flat
.session()
.execute("MATCH (a:Node {name: 'A'})-[:LINK]->(b)-[:LINK]->(c) RETURN COUNT(c)")
.unwrap();
let factorized_count = factorized_result.iter().next().unwrap()[0]
.as_int64()
.unwrap();
let flat_count = flat_result.iter().next().unwrap()[0].as_int64().unwrap();
println!("Factorized COUNT: {}", factorized_count);
println!("Flat COUNT: {}", flat_count);
println!("Expected: 3");
assert_eq!(
factorized_count, 3,
"Factorized execution should count 3 paths"
);
assert_eq!(flat_count, 3, "Flat execution should count 3 paths");
assert_eq!(
factorized_count, flat_count,
"Both should return the same count"
);
}
#[test]
fn test_count_star_simple() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Center"], [("name", Value::String("center".into()))])
.unwrap();
session
.create_node_with_props(&["Target"], [("name", Value::String("A".into()))])
.unwrap();
session
.create_node_with_props(&["Target"], [("name", Value::String("B".into()))])
.unwrap();
session
.create_node_with_props(&["Target"], [("name", Value::String("C".into()))])
.unwrap();
session
.execute_cypher("MATCH (c:Center), (t:Target) CREATE (c)-[:POINTS_TO]->(t)")
.unwrap();
session
.create_node_with_props(&["Leaf"], [("name", Value::String("L1".into()))])
.unwrap();
session
.create_node_with_props(&["Leaf"], [("name", Value::String("L2".into()))])
.unwrap();
session
.execute_cypher("MATCH (t:Target {name: 'A'}), (l:Leaf) CREATE (t)-[:POINTS_TO]->(l)")
.unwrap();
let result = session
.execute("MATCH (c:Center)-[:POINTS_TO]->(t)-[:POINTS_TO]->(l) RETURN COUNT(l)")
.unwrap();
let count = result.iter().next().unwrap()[0].as_int64().unwrap();
println!("2-hop COUNT: {} (expected 2)", count);
assert_eq!(count, 2, "Should find 2 two-hop paths");
}
#[test]
fn test_factorized_aggregation_speedup_demonstration() {
let db_factorized = GrafeoDB::new_in_memory();
let db_flat = GrafeoDB::with_config(Config::default().without_factorized_execution()).unwrap();
for db in [&db_factorized, &db_flat] {
let session = db.session();
for i in 0..5 {
session
.create_node_with_props(&["Source"], [("id", Value::Int64(i))])
.unwrap();
}
for i in 0..5 {
for j in 0..10 {
let target_id = 100 + i * 10 + j;
session
.create_node_with_props(&["Hop1"], [("id", Value::Int64(target_id))])
.unwrap();
}
session.execute_cypher(&format!(
"MATCH (s:Source {{id: {}}}), (t:Hop1) WHERE t.id >= {} AND t.id < {} CREATE (s)-[:LINK]->(t)",
i, 100 + i * 10, 100 + (i + 1) * 10
)).unwrap();
}
for i in 0..50 {
let hop1_id = 100 + i;
for j in 0..5 {
let hop2_id = 1000 + i * 5 + j;
session
.create_node_with_props(&["Hop2"], [("id", Value::Int64(hop2_id))])
.unwrap();
}
session.execute_cypher(&format!(
"MATCH (h1:Hop1 {{id: {}}}), (h2:Hop2) WHERE h2.id >= {} AND h2.id < {} CREATE (h1)-[:LINK]->(h2)",
hop1_id, 1000 + i * 5, 1000 + (i + 1) * 5
)).unwrap();
}
}
let start_factorized = std::time::Instant::now();
let factorized_result = db_factorized
.session()
.execute("MATCH (s:Source)-[:LINK]->(h1)-[:LINK]->(h2) RETURN COUNT(h2)")
.unwrap();
let factorized_time = start_factorized.elapsed();
let start_flat = std::time::Instant::now();
let flat_result = db_flat
.session()
.execute("MATCH (s:Source)-[:LINK]->(h1)-[:LINK]->(h2) RETURN COUNT(h2)")
.unwrap();
let flat_time = start_flat.elapsed();
let factorized_count = factorized_result.iter().next().unwrap()[0]
.as_int64()
.unwrap();
let flat_count = flat_result.iter().next().unwrap()[0].as_int64().unwrap();
println!("========================================");
println!(" Factorized Aggregation Speedup Test");
println!("========================================");
println!();
println!("Graph: 5 sources * 10 hop1 * 5 hop2 = 250 paths");
println!();
println!(
"Factorized COUNT: {} in {:?}",
factorized_count, factorized_time
);
println!("Flat COUNT: {} in {:?}", flat_count, flat_time);
println!();
assert_eq!(factorized_count, flat_count, "Counts should match");
if factorized_time < flat_time {
let speedup = flat_time.as_nanos() as f64 / factorized_time.as_nanos() as f64;
println!("Speedup: {:.2}x", speedup);
}
}
fn create_chain_graph(db: &GrafeoDB) {
let session = db.session();
let r0 = session
.create_node_with_props(&["Root"], [("name", Value::String("R0".into()))])
.unwrap();
let r1 = session
.create_node_with_props(&["Root"], [("name", Value::String("R1".into()))])
.unwrap();
let h1_0 = session
.create_node_with_props(&["Hop1"], [("name", Value::String("H1_0".into()))])
.unwrap();
let h1_1 = session
.create_node_with_props(&["Hop1"], [("name", Value::String("H1_1".into()))])
.unwrap();
let h1_2 = session
.create_node_with_props(&["Hop1"], [("name", Value::String("H1_2".into()))])
.unwrap();
let h2_0 = session
.create_node_with_props(&["Hop2"], [("name", Value::String("H2_0".into()))])
.unwrap();
let h2_1 = session
.create_node_with_props(&["Hop2"], [("name", Value::String("H2_1".into()))])
.unwrap();
let h2_2 = session
.create_node_with_props(&["Hop2"], [("name", Value::String("H2_2".into()))])
.unwrap();
let h2_3 = session
.create_node_with_props(&["Hop2"], [("name", Value::String("H2_3".into()))])
.unwrap();
let h3_0 = session
.create_node_with_props(&["Hop3"], [("name", Value::String("H3_0".into()))])
.unwrap();
let h3_1 = session
.create_node_with_props(&["Hop3"], [("name", Value::String("H3_1".into()))])
.unwrap();
session.create_edge(r0, h1_0, "STEP");
session.create_edge(r0, h1_1, "STEP");
session.create_edge(r1, h1_2, "STEP");
session.create_edge(h1_0, h2_0, "STEP");
session.create_edge(h1_0, h2_1, "STEP");
session.create_edge(h1_1, h2_2, "STEP");
session.create_edge(h1_2, h2_3, "STEP");
session.create_edge(h2_0, h3_0, "STEP");
session.create_edge(h2_1, h3_0, "STEP");
session.create_edge(h2_1, h3_1, "STEP");
session.create_edge(h2_2, h3_1, "STEP");
}
#[test]
fn test_three_hop_factorized_count() {
let db = GrafeoDB::new_in_memory();
create_chain_graph(&db);
let session = db.session();
let result = session
.execute("MATCH (a:Root)-[:STEP]->(b)-[:STEP]->(c)-[:STEP]->(d) RETURN count(d) AS cnt")
.unwrap();
assert_eq!(result.row_count(), 1, "Aggregation should return one row");
let count = result.iter().next().unwrap()[0].as_int64().unwrap();
assert_eq!(count, 4, "Should find exactly 4 three-hop paths");
}
#[test]
fn test_three_hop_factorized_vs_flat() {
let db_factorized = GrafeoDB::new_in_memory();
let db_flat = GrafeoDB::with_config(Config::default().without_factorized_execution()).unwrap();
create_chain_graph(&db_factorized);
create_chain_graph(&db_flat);
let query = "MATCH (a:Root)-[:STEP]->(b)-[:STEP]->(c)-[:STEP]->(d) RETURN count(d) AS cnt";
let factorized_count = db_factorized
.session()
.execute(query)
.unwrap()
.iter()
.next()
.unwrap()[0]
.as_int64()
.unwrap();
let flat_count = db_flat
.session()
.execute(query)
.unwrap()
.iter()
.next()
.unwrap()[0]
.as_int64()
.unwrap();
println!("Factorized 3-hop count: {}", factorized_count);
println!("Flat 3-hop count: {}", flat_count);
assert_eq!(factorized_count, 4, "Factorized should find 4 paths");
assert_eq!(flat_count, 4, "Flat should find 4 paths");
assert_eq!(
factorized_count, flat_count,
"Factorized and flat must agree on 3-hop count"
);
}
#[test]
fn test_asymmetric_fanout_two_hop() {
let db_factorized = GrafeoDB::new_in_memory();
let db_flat = GrafeoDB::with_config(Config::default().without_factorized_execution()).unwrap();
for db in [&db_factorized, &db_flat] {
let session = db.session();
let star = session
.create_node_with_props(&["Hub"], [("name", Value::String("Star".into()))])
.unwrap();
let _leaf = session
.create_node_with_props(&["Hub"], [("name", Value::String("Leaf".into()))])
.unwrap();
let s1 = session
.create_node_with_props(&["Sat"], [("name", Value::String("S1".into()))])
.unwrap();
let s2 = session
.create_node_with_props(&["Sat"], [("name", Value::String("S2".into()))])
.unwrap();
let s3 = session
.create_node_with_props(&["Sat"], [("name", Value::String("S3".into()))])
.unwrap();
let s4 = session
.create_node_with_props(&["Sat"], [("name", Value::String("S4".into()))])
.unwrap();
let s5 = session
.create_node_with_props(&["Sat"], [("name", Value::String("S5".into()))])
.unwrap();
session.create_edge(star, s1, "ARM");
session.create_edge(star, s2, "ARM");
session.create_edge(star, s3, "ARM");
session.create_edge(star, s4, "ARM");
session.create_edge(star, s5, "ARM");
let t1 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T1".into()))])
.unwrap();
let t2 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T2".into()))])
.unwrap();
let t3 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T3".into()))])
.unwrap();
let t4 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T4".into()))])
.unwrap();
let t5 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T5".into()))])
.unwrap();
let t6 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T6".into()))])
.unwrap();
let t7 = session
.create_node_with_props(&["Tip"], [("name", Value::String("T7".into()))])
.unwrap();
session.create_edge(s1, t1, "ARM");
session.create_edge(s1, t2, "ARM");
session.create_edge(s2, t3, "ARM");
session.create_edge(s3, t4, "ARM");
session.create_edge(s3, t5, "ARM");
session.create_edge(s3, t6, "ARM");
session.create_edge(s5, t7, "ARM");
}
let query = "MATCH (a:Hub)-[:ARM]->(b)-[:ARM]->(c) RETURN count(c) AS cnt";
let factorized_count = db_factorized
.session()
.execute(query)
.unwrap()
.iter()
.next()
.unwrap()[0]
.as_int64()
.unwrap();
let flat_count = db_flat
.session()
.execute(query)
.unwrap()
.iter()
.next()
.unwrap()[0]
.as_int64()
.unwrap();
println!("Asymmetric fan-out, factorized: {}", factorized_count);
println!("Asymmetric fan-out, flat: {}", flat_count);
assert_eq!(
factorized_count, 7,
"Factorized should find 7 two-hop paths (all from Star, none from Leaf)"
);
assert_eq!(flat_count, 7, "Flat should find 7 two-hop paths");
assert_eq!(
factorized_count, flat_count,
"Factorized and flat must agree on asymmetric fan-out"
);
}