git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
406 lines
11 KiB
Rust
406 lines
11 KiB
Rust
//! Cypher query execution correctness tests
|
|
//!
|
|
//! Tests to verify that Cypher queries execute correctly and return expected results.
|
|
|
|
use ruvector_graph::{Edge, GraphDB, Label, Node, Properties, PropertyValue};
|
|
|
|
fn setup_test_graph() -> GraphDB {
|
|
let db = GraphDB::new();
|
|
|
|
// Create people
|
|
let mut alice_props = Properties::new();
|
|
alice_props.insert(
|
|
"name".to_string(),
|
|
PropertyValue::String("Alice".to_string()),
|
|
);
|
|
alice_props.insert("age".to_string(), PropertyValue::Integer(30));
|
|
|
|
let mut bob_props = Properties::new();
|
|
bob_props.insert("name".to_string(), PropertyValue::String("Bob".to_string()));
|
|
bob_props.insert("age".to_string(), PropertyValue::Integer(35));
|
|
|
|
let mut charlie_props = Properties::new();
|
|
charlie_props.insert(
|
|
"name".to_string(),
|
|
PropertyValue::String("Charlie".to_string()),
|
|
);
|
|
charlie_props.insert("age".to_string(), PropertyValue::Integer(28));
|
|
|
|
db.create_node(Node::new(
|
|
"alice".to_string(),
|
|
vec![Label {
|
|
name: "Person".to_string(),
|
|
}],
|
|
alice_props,
|
|
))
|
|
.unwrap();
|
|
|
|
db.create_node(Node::new(
|
|
"bob".to_string(),
|
|
vec![Label {
|
|
name: "Person".to_string(),
|
|
}],
|
|
bob_props,
|
|
))
|
|
.unwrap();
|
|
|
|
db.create_node(Node::new(
|
|
"charlie".to_string(),
|
|
vec![Label {
|
|
name: "Person".to_string(),
|
|
}],
|
|
charlie_props,
|
|
))
|
|
.unwrap();
|
|
|
|
// Create relationships
|
|
db.create_edge(Edge::new(
|
|
"e1".to_string(),
|
|
"alice".to_string(),
|
|
"bob".to_string(),
|
|
"KNOWS".to_string(),
|
|
Properties::new(),
|
|
))
|
|
.unwrap();
|
|
|
|
db.create_edge(Edge::new(
|
|
"e2".to_string(),
|
|
"bob".to_string(),
|
|
"charlie".to_string(),
|
|
"KNOWS".to_string(),
|
|
Properties::new(),
|
|
))
|
|
.unwrap();
|
|
|
|
db
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_simple_match_all_nodes() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement query execution
|
|
// let results = db.execute("MATCH (n) RETURN n").unwrap();
|
|
// assert_eq!(results.len(), 3);
|
|
|
|
// For now, just verify the graph was set up correctly
|
|
assert!(db.get_node("alice").is_some());
|
|
assert!(db.get_node("bob").is_some());
|
|
assert!(db.get_node("charlie").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_match_with_label_filter() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN n").unwrap();
|
|
// assert_eq!(results.len(), 3);
|
|
|
|
assert!(db.get_node("alice").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_match_with_property_filter() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person {name: 'Alice'}) RETURN n").unwrap();
|
|
// assert_eq!(results.len(), 1);
|
|
|
|
let alice = db.get_node("alice").unwrap();
|
|
assert_eq!(
|
|
alice.properties.get("name"),
|
|
Some(&PropertyValue::String("Alice".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_match_with_where_clause() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) WHERE n.age > 30 RETURN n").unwrap();
|
|
// Should return Bob (35)
|
|
// assert_eq!(results.len(), 1);
|
|
|
|
let bob = db.get_node("bob").unwrap();
|
|
if let Some(PropertyValue::Integer(age)) = bob.properties.get("age") {
|
|
assert!(*age > 30);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_match_relationship() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (a)-[r:KNOWS]->(b) RETURN a, r, b").unwrap();
|
|
// Should return 2 relationships
|
|
|
|
assert!(db.get_edge("e1").is_some());
|
|
assert!(db.get_edge("e2").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_create_node() {
|
|
let db = GraphDB::new();
|
|
|
|
// TODO: Implement
|
|
// db.execute("CREATE (n:Person {name: 'David', age: 40})").unwrap();
|
|
|
|
// For now, create manually
|
|
let mut props = Properties::new();
|
|
props.insert(
|
|
"name".to_string(),
|
|
PropertyValue::String("David".to_string()),
|
|
);
|
|
props.insert("age".to_string(), PropertyValue::Integer(40));
|
|
|
|
db.create_node(Node::new(
|
|
"david".to_string(),
|
|
vec![Label {
|
|
name: "Person".to_string(),
|
|
}],
|
|
props,
|
|
))
|
|
.unwrap();
|
|
|
|
let david = db.get_node("david").unwrap();
|
|
assert_eq!(
|
|
david.properties.get("name"),
|
|
Some(&PropertyValue::String("David".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_count_aggregation() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN COUNT(n) AS count").unwrap();
|
|
// assert_eq!(results[0]["count"], 3);
|
|
|
|
// Manual verification
|
|
assert!(db.get_node("alice").is_some());
|
|
assert!(db.get_node("bob").is_some());
|
|
assert!(db.get_node("charlie").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_sum_aggregation() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN SUM(n.age) AS total_age").unwrap();
|
|
// assert_eq!(results[0]["total_age"], 93); // 30 + 35 + 28
|
|
|
|
// Manual verification
|
|
let ages: Vec<i64> = ["alice", "bob", "charlie"]
|
|
.iter()
|
|
.filter_map(|id| {
|
|
db.get_node(*id).and_then(|n| {
|
|
if let Some(PropertyValue::Integer(age)) = n.properties.get("age") {
|
|
Some(*age)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
assert_eq!(ages.iter().sum::<i64>(), 93);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_avg_aggregation() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN AVG(n.age) AS avg_age").unwrap();
|
|
// assert_eq!(results[0]["avg_age"], 31.0); // (30 + 35 + 28) / 3
|
|
|
|
let ages: Vec<i64> = ["alice", "bob", "charlie"]
|
|
.iter()
|
|
.filter_map(|id| {
|
|
db.get_node(*id).and_then(|n| {
|
|
if let Some(PropertyValue::Integer(age)) = n.properties.get("age") {
|
|
Some(*age)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let avg = ages.iter().sum::<i64>() as f64 / ages.len() as f64;
|
|
assert!((avg - 31.0).abs() < 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_order_by() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN n ORDER BY n.age ASC").unwrap();
|
|
// First should be Charlie (28), last should be Bob (35)
|
|
|
|
let mut ages: Vec<i64> = ["alice", "bob", "charlie"]
|
|
.iter()
|
|
.filter_map(|id| {
|
|
db.get_node(*id).and_then(|n| {
|
|
if let Some(PropertyValue::Integer(age)) = n.properties.get("age") {
|
|
Some(*age)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
ages.sort();
|
|
assert_eq!(ages, vec![28, 30, 35]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_limit() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH (n:Person) RETURN n LIMIT 2").unwrap();
|
|
// assert_eq!(results.len(), 2);
|
|
|
|
assert!(db.get_node("alice").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_path_query() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("MATCH p = (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN p").unwrap();
|
|
// Should find paths: Alice->Bob, Bob->Charlie, Alice->Bob->Charlie
|
|
|
|
let e1 = db.get_edge("e1").unwrap();
|
|
let e2 = db.get_edge("e2").unwrap();
|
|
|
|
assert_eq!(e1.from, "alice");
|
|
assert_eq!(e1.to, "bob");
|
|
assert_eq!(e2.from, "bob");
|
|
assert_eq!(e2.to, "charlie");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Complex Query Execution Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_execute_multi_hop_traversal() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// Find all people connected to Alice within 2 hops
|
|
// let results = db.execute("
|
|
// MATCH (alice:Person {name: 'Alice'})-[:KNOWS*1..2]->(connected)
|
|
// RETURN DISTINCT connected.name
|
|
// ").unwrap();
|
|
|
|
// Should find Bob (1 hop) and Charlie (2 hops)
|
|
|
|
assert!(db.get_node("bob").is_some());
|
|
assert!(db.get_node("charlie").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_pattern_matching() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("
|
|
// MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)
|
|
// RETURN a.name, c.name
|
|
// ").unwrap();
|
|
|
|
// Should find Alice knows Charlie through Bob
|
|
|
|
assert!(db.get_edge("e1").is_some());
|
|
assert!(db.get_edge("e2").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_collect_aggregation() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("
|
|
// MATCH (p:Person)-[:KNOWS]->(friend)
|
|
// RETURN p.name, COLLECT(friend.name) AS friends
|
|
// ").unwrap();
|
|
|
|
// Alice: [Bob], Bob: [Charlie], Charlie: []
|
|
|
|
assert!(db.get_edge("e1").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_optional_match() {
|
|
let db = setup_test_graph();
|
|
|
|
// TODO: Implement
|
|
// let results = db.execute("
|
|
// MATCH (p:Person)
|
|
// OPTIONAL MATCH (p)-[:KNOWS]->(friend)
|
|
// RETURN p.name, friend.name
|
|
// ").unwrap();
|
|
|
|
// Should return all people, some with null friends
|
|
|
|
assert!(db.get_node("charlie").is_some());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Result Verification Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_query_result_schema() {
|
|
// TODO: Implement
|
|
// Verify that query results have correct schema
|
|
// let db = setup_test_graph();
|
|
// let results = db.execute("MATCH (n:Person) RETURN n.name AS name, n.age AS age").unwrap();
|
|
// assert!(results.has_column("name"));
|
|
// assert!(results.has_column("age"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_result_ordering() {
|
|
// TODO: Implement
|
|
// Verify that ORDER BY is correctly applied
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_result_pagination() {
|
|
// TODO: Implement
|
|
// Verify SKIP and LIMIT work correctly together
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error Handling Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_execute_invalid_property_access() {
|
|
// TODO: Implement
|
|
// let db = setup_test_graph();
|
|
// let result = db.execute("MATCH (n:Person) WHERE n.nonexistent > 5 RETURN n");
|
|
// Should handle gracefully (return no results or error depending on semantics)
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_type_mismatch() {
|
|
// TODO: Implement
|
|
// let db = setup_test_graph();
|
|
// let result = db.execute("MATCH (n:Person) WHERE n.name > 5 RETURN n");
|
|
// Should handle type mismatch error
|
|
}
|