Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
363
vendor/ruvector/crates/ruvector-graph/tests/compatibility_tests.rs
vendored
Normal file
363
vendor/ruvector/crates/ruvector-graph/tests/compatibility_tests.rs
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
//! Neo4j compatibility tests
|
||||
//!
|
||||
//! Tests to verify that RuVector graph database is compatible with Neo4j
|
||||
//! in terms of query syntax and result format.
|
||||
|
||||
use ruvector_graph::{Edge, GraphDB, Label, Node, Properties, PropertyValue};
|
||||
|
||||
fn setup_movie_graph() -> GraphDB {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Actors
|
||||
let mut keanu_props = Properties::new();
|
||||
keanu_props.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Keanu Reeves".to_string()),
|
||||
);
|
||||
keanu_props.insert("born".to_string(), PropertyValue::Integer(1964));
|
||||
|
||||
let mut carrie_props = Properties::new();
|
||||
carrie_props.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Carrie-Anne Moss".to_string()),
|
||||
);
|
||||
carrie_props.insert("born".to_string(), PropertyValue::Integer(1967));
|
||||
|
||||
let mut laurence_props = Properties::new();
|
||||
laurence_props.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Laurence Fishburne".to_string()),
|
||||
);
|
||||
laurence_props.insert("born".to_string(), PropertyValue::Integer(1961));
|
||||
|
||||
// Movies
|
||||
let mut matrix_props = Properties::new();
|
||||
matrix_props.insert(
|
||||
"title".to_string(),
|
||||
PropertyValue::String("The Matrix".to_string()),
|
||||
);
|
||||
matrix_props.insert("released".to_string(), PropertyValue::Integer(1999));
|
||||
matrix_props.insert(
|
||||
"tagline".to_string(),
|
||||
PropertyValue::String("Welcome to the Real World".to_string()),
|
||||
);
|
||||
|
||||
db.create_node(Node::new(
|
||||
"keanu".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
keanu_props,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db.create_node(Node::new(
|
||||
"carrie".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
carrie_props,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db.create_node(Node::new(
|
||||
"laurence".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
laurence_props,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db.create_node(Node::new(
|
||||
"matrix".to_string(),
|
||||
vec![Label {
|
||||
name: "Movie".to_string(),
|
||||
}],
|
||||
matrix_props,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// Relationships
|
||||
let mut keanu_role = Properties::new();
|
||||
keanu_role.insert(
|
||||
"roles".to_string(),
|
||||
PropertyValue::List(vec![PropertyValue::String("Neo".to_string())]),
|
||||
);
|
||||
|
||||
let mut carrie_role = Properties::new();
|
||||
carrie_role.insert(
|
||||
"roles".to_string(),
|
||||
PropertyValue::List(vec![PropertyValue::String("Trinity".to_string())]),
|
||||
);
|
||||
|
||||
let mut laurence_role = Properties::new();
|
||||
laurence_role.insert(
|
||||
"roles".to_string(),
|
||||
PropertyValue::List(vec![PropertyValue::String("Morpheus".to_string())]),
|
||||
);
|
||||
|
||||
db.create_edge(Edge::new(
|
||||
"e1".to_string(),
|
||||
"keanu".to_string(),
|
||||
"matrix".to_string(),
|
||||
"ACTED_IN".to_string(),
|
||||
keanu_role,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db.create_edge(Edge::new(
|
||||
"e2".to_string(),
|
||||
"carrie".to_string(),
|
||||
"matrix".to_string(),
|
||||
"ACTED_IN".to_string(),
|
||||
carrie_role,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db.create_edge(Edge::new(
|
||||
"e3".to_string(),
|
||||
"laurence".to_string(),
|
||||
"matrix".to_string(),
|
||||
"ACTED_IN".to_string(),
|
||||
laurence_role,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Neo4j Query Compatibility Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_match_all_nodes() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (n) RETURN n
|
||||
// TODO: Implement query execution
|
||||
// let results = db.execute("MATCH (n) RETURN n").unwrap();
|
||||
// assert_eq!(results.len(), 4); // 3 people + 1 movie
|
||||
|
||||
// For now, verify graph setup
|
||||
assert!(db.get_node("keanu").is_some());
|
||||
assert!(db.get_node("matrix").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_match_with_label() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (p:Person) RETURN p
|
||||
// TODO: Implement
|
||||
// let results = db.execute("MATCH (p:Person) RETURN p").unwrap();
|
||||
// assert_eq!(results.len(), 3);
|
||||
|
||||
// Verify label filtering would work
|
||||
let keanu = db.get_node("keanu").unwrap();
|
||||
assert_eq!(keanu.labels[0].name, "Person");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_match_with_properties() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (m:Movie {title: 'The Matrix'}) RETURN m
|
||||
// TODO: Implement
|
||||
// let results = db.execute("MATCH (m:Movie {title: 'The Matrix'}) RETURN m").unwrap();
|
||||
// assert_eq!(results.len(), 1);
|
||||
|
||||
let matrix = db.get_node("matrix").unwrap();
|
||||
assert_eq!(
|
||||
matrix.properties.get("title"),
|
||||
Some(&PropertyValue::String("The Matrix".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_match_relationship() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (a:Person)-[r:ACTED_IN]->(m:Movie) RETURN a, r, m
|
||||
// TODO: Implement
|
||||
// let results = db.execute("MATCH (a:Person)-[r:ACTED_IN]->(m:Movie) RETURN a, r, m").unwrap();
|
||||
// assert_eq!(results.len(), 3);
|
||||
|
||||
let edge = db.get_edge("e1").unwrap();
|
||||
assert_eq!(edge.edge_type, "ACTED_IN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_where_clause() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (p:Person) WHERE p.born > 1965 RETURN p
|
||||
// TODO: Implement
|
||||
// let results = db.execute("MATCH (p:Person) WHERE p.born > 1965 RETURN p").unwrap();
|
||||
// assert_eq!(results.len(), 1); // Only Carrie-Anne Moss
|
||||
|
||||
let carrie = db.get_node("carrie").unwrap();
|
||||
if let Some(PropertyValue::Integer(born)) = carrie.properties.get("born") {
|
||||
assert!(*born > 1965);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_count_aggregation() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (p:Person) RETURN COUNT(p)
|
||||
// TODO: Implement
|
||||
// let results = db.execute("MATCH (p:Person) RETURN COUNT(p)").unwrap();
|
||||
// assert_eq!(results[0]["count"], 3);
|
||||
|
||||
// Manually verify
|
||||
assert!(db.get_node("keanu").is_some());
|
||||
assert!(db.get_node("carrie").is_some());
|
||||
assert!(db.get_node("laurence").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_collect_aggregation() {
|
||||
let db = setup_movie_graph();
|
||||
|
||||
// Neo4j query: MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
|
||||
// RETURN m.title, COLLECT(p.name) AS actors
|
||||
// TODO: Implement
|
||||
// let results = db.execute("...").unwrap();
|
||||
|
||||
// Verify relationships exist
|
||||
assert!(db.get_edge("e1").is_some());
|
||||
assert!(db.get_edge("e2").is_some());
|
||||
assert!(db.get_edge("e3").is_some());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Neo4j Data Type Compatibility
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_string_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Test".to_string()),
|
||||
);
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert!(matches!(
|
||||
node.properties.get("name"),
|
||||
Some(PropertyValue::String(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_integer_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert("count".to_string(), PropertyValue::Integer(42));
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert_eq!(
|
||||
node.properties.get("count"),
|
||||
Some(&PropertyValue::Integer(42))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_float_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert("score".to_string(), PropertyValue::Float(3.14));
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert_eq!(
|
||||
node.properties.get("score"),
|
||||
Some(&PropertyValue::Float(3.14))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_boolean_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert("active".to_string(), PropertyValue::Boolean(true));
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert_eq!(
|
||||
node.properties.get("active"),
|
||||
Some(&PropertyValue::Boolean(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_list_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert(
|
||||
"tags".to_string(),
|
||||
PropertyValue::List(vec![
|
||||
PropertyValue::String("tag1".to_string()),
|
||||
PropertyValue::String("tag2".to_string()),
|
||||
]),
|
||||
);
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert!(matches!(
|
||||
node.properties.get("tags"),
|
||||
Some(PropertyValue::List(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neo4j_null_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut props = Properties::new();
|
||||
props.insert("optional".to_string(), PropertyValue::Null);
|
||||
|
||||
db.create_node(Node::new("n1".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
let node = db.get_node("n1").unwrap();
|
||||
assert_eq!(node.properties.get("optional"), Some(&PropertyValue::Null));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Known Differences from Neo4j
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_documented_differences() {
|
||||
// Document any intentional differences from Neo4j behavior
|
||||
// For example:
|
||||
// - Different default values
|
||||
// - Different error messages
|
||||
// - Different performance characteristics
|
||||
// - Missing features
|
||||
|
||||
// This test serves as documentation
|
||||
assert!(true);
|
||||
}
|
||||
396
vendor/ruvector/crates/ruvector-graph/tests/concurrent_tests.rs
vendored
Normal file
396
vendor/ruvector/crates/ruvector-graph/tests/concurrent_tests.rs
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
//! Concurrent access pattern tests
|
||||
//!
|
||||
//! Tests for multi-threaded access, lock-free operations, and concurrent modifications.
|
||||
|
||||
use ruvector_graph::{Edge, GraphDB, Label, Node, Properties, PropertyValue};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_node_creation() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
let num_threads = 10;
|
||||
let nodes_per_thread = 100;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for i in 0..nodes_per_thread {
|
||||
let mut props = Properties::new();
|
||||
props.insert("thread".to_string(), PropertyValue::Integer(thread_id));
|
||||
props.insert("index".to_string(), PropertyValue::Integer(i));
|
||||
|
||||
let node = Node::new(
|
||||
format!("node_{}_{}", thread_id, i),
|
||||
vec![Label {
|
||||
name: "Concurrent".to_string(),
|
||||
}],
|
||||
props,
|
||||
);
|
||||
|
||||
db_clone.create_node(node).unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// Verify all nodes were created
|
||||
// Note: Would need node_count() method
|
||||
// assert_eq!(db.node_count(), num_threads * nodes_per_thread);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_reads() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Create initial nodes
|
||||
for i in 0..100 {
|
||||
let node = Node::new(format!("node_{}", i), vec![], Properties::new());
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
let num_readers = 20;
|
||||
let reads_per_thread = 1000;
|
||||
|
||||
let handles: Vec<_> = (0..num_readers)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for i in 0..reads_per_thread {
|
||||
let node_id = format!("node_{}", (thread_id * 10 + i) % 100);
|
||||
let result = db_clone.get_node(&node_id);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_writes_no_collision() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
let num_threads = 10;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for i in 0..50 {
|
||||
let node_id = format!("t{}_n{}", thread_id, i);
|
||||
let node = Node::new(node_id, vec![], Properties::new());
|
||||
db_clone.create_node(node).unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// All 500 nodes should be created
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_edge_creation() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Create nodes first
|
||||
for i in 0..100 {
|
||||
db.create_node(Node::new(format!("n{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let num_threads = 10;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for i in 0..50 {
|
||||
let from = format!("n{}", (thread_id * 10 + i) % 100);
|
||||
let to = format!("n{}", (thread_id * 10 + i + 1) % 100);
|
||||
|
||||
let edge = Edge::new(
|
||||
format!("e_{}_{}", thread_id, i),
|
||||
from,
|
||||
to,
|
||||
"LINK".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db_clone.create_edge(edge).unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_read_while_writing() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Initial nodes
|
||||
for i in 0..50 {
|
||||
db.create_node(Node::new(
|
||||
format!("initial_{}", i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let num_readers = 5;
|
||||
let num_writers = 3;
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Spawn readers
|
||||
for reader_id in 0..num_readers {
|
||||
let db_clone = Arc::clone(&db);
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..100 {
|
||||
let node_id = format!("initial_{}", (reader_id * 10 + i) % 50);
|
||||
let _ = db_clone.get_node(&node_id);
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Spawn writers
|
||||
for writer_id in 0..num_writers {
|
||||
let db_clone = Arc::clone(&db);
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..100 {
|
||||
let node = Node::new(
|
||||
format!("new_{}_{}", writer_id, i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
);
|
||||
db_clone.create_node(node).unwrap();
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_property_updates() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Create shared counter node
|
||||
let mut props = Properties::new();
|
||||
props.insert("counter".to_string(), PropertyValue::Integer(0));
|
||||
db.create_node(Node::new("counter".to_string(), vec![], props))
|
||||
.unwrap();
|
||||
|
||||
// TODO: Implement atomic property updates
|
||||
// For now, just test concurrent reads
|
||||
let num_threads = 10;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|_| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for _ in 0..100 {
|
||||
let _node = db_clone.get_node("counter");
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_free_reads() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Populate database
|
||||
for i in 0..1000 {
|
||||
db.create_node(Node::new(
|
||||
format!("node_{}", i),
|
||||
vec![Label {
|
||||
name: "Test".to_string(),
|
||||
}],
|
||||
Properties::new(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Many concurrent readers should not block each other
|
||||
let num_readers = 50;
|
||||
|
||||
let handles: Vec<_> = (0..num_readers)
|
||||
.map(|reader_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
for i in 0..100 {
|
||||
let node_id = format!("node_{}", (reader_id * 20 + i) % 1000);
|
||||
let result = db_clone.get_node(&node_id);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
// With lock-free reads, this should complete quickly
|
||||
// Even with 50 threads doing 100 reads each (5000 reads total)
|
||||
println!("Concurrent reads took: {:?}", duration);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_writer_starvation_prevention() {
|
||||
// Ensure that heavy read load doesn't prevent writes
|
||||
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Initial data
|
||||
for i in 0..100 {
|
||||
db.create_node(Node::new(
|
||||
format!("initial_{}", i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let readers_done = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let writers_done = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Heavy read load
|
||||
for reader_id in 0..20i64 {
|
||||
let db_clone = Arc::clone(&db);
|
||||
let done = Arc::clone(&readers_done);
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..1000i64 {
|
||||
let node_id = format!("initial_{}", (reader_id + i) % 100);
|
||||
let _ = db_clone.get_node(&node_id);
|
||||
}
|
||||
done.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Writers should still make progress
|
||||
for writer_id in 0..5 {
|
||||
let db_clone = Arc::clone(&db);
|
||||
let done = Arc::clone(&writers_done);
|
||||
let handle = thread::spawn(move || {
|
||||
for i in 0..50 {
|
||||
let node = Node::new(
|
||||
format!("writer_{}_{}", writer_id, i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
);
|
||||
db_clone.create_node(node).unwrap();
|
||||
}
|
||||
done.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// Verify both readers and writers completed
|
||||
assert!(readers_done.load(std::sync::atomic::Ordering::Relaxed));
|
||||
assert!(writers_done.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stress Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_high_concurrency_stress() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
let num_threads = 50;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
// Mix of operations
|
||||
for i in 0i32..100 {
|
||||
if i % 3 == 0 {
|
||||
// Create node
|
||||
let node = Node::new(
|
||||
format!("stress_{}_{}", thread_id, i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
);
|
||||
db_clone.create_node(node).unwrap();
|
||||
} else {
|
||||
// Read node (might not exist)
|
||||
let node_id = format!("stress_{}_{}", thread_id, i.saturating_sub(1));
|
||||
let _ = db_clone.get_node(&node_id);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_batch_operations() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
let num_threads = 10;
|
||||
let batch_size = 100;
|
||||
|
||||
let handles: Vec<_> = (0..num_threads)
|
||||
.map(|thread_id| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
// TODO: Implement batch insert
|
||||
// For now, insert individually
|
||||
for i in 0..batch_size {
|
||||
let node = Node::new(
|
||||
format!("batch_{}_{}", thread_id, i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
);
|
||||
db_clone.create_node(node).unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
405
vendor/ruvector/crates/ruvector-graph/tests/cypher_execution_tests.rs
vendored
Normal file
405
vendor/ruvector/crates/ruvector-graph/tests/cypher_execution_tests.rs
vendored
Normal file
@@ -0,0 +1,405 @@
|
||||
//! 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
|
||||
}
|
||||
166
vendor/ruvector/crates/ruvector-graph/tests/cypher_parser_integration.rs
vendored
Normal file
166
vendor/ruvector/crates/ruvector-graph/tests/cypher_parser_integration.rs
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
//! Integration tests for Cypher parser
|
||||
|
||||
use ruvector_graph::cypher::{ast::*, parse_cypher};
|
||||
|
||||
#[test]
|
||||
fn test_simple_match_query() {
|
||||
let query = "MATCH (n:Person) RETURN n";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse simple MATCH query: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let ast = result.unwrap();
|
||||
assert_eq!(ast.statements.len(), 2); // MATCH and RETURN
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_with_where() {
|
||||
let query = "MATCH (n:Person) WHERE n.age > 30 RETURN n.name";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse MATCH with WHERE: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relationship_pattern() {
|
||||
let query = "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, r, b";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse relationship pattern: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_node() {
|
||||
let query = "CREATE (n:Person {name: 'Alice', age: 30})";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse CREATE query: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Hyperedge syntax not yet implemented in parser"]
|
||||
fn test_hyperedge_pattern() {
|
||||
let query = "MATCH (a)-[r:TRANSACTION]->(b, c, d) RETURN a, r, b, c, d";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse hyperedge: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let ast = result.unwrap();
|
||||
assert!(ast.has_hyperedges(), "Query should contain hyperedges");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aggregation_functions() {
|
||||
let query = "MATCH (n:Person) RETURN COUNT(n), AVG(n.age), MAX(n.salary)";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse aggregation query: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_order_by_limit() {
|
||||
let query = "MATCH (n:Person) RETURN n.name ORDER BY n.age DESC LIMIT 10";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse ORDER BY LIMIT: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_query() {
|
||||
let query = r#"
|
||||
MATCH (a:Person)-[r:KNOWS]->(b:Person)
|
||||
WHERE a.age > 30 AND b.name = 'Alice'
|
||||
RETURN a.name, b.name, r.since
|
||||
ORDER BY r.since DESC
|
||||
LIMIT 10
|
||||
"#;
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse complex query: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "CREATE relationship with properties not yet fully implemented"]
|
||||
fn test_create_relationship() {
|
||||
let query = r#"
|
||||
MATCH (a:Person), (b:Person)
|
||||
WHERE a.name = 'Alice' AND b.name = 'Bob'
|
||||
CREATE (a)-[:KNOWS {since: 2024}]->(b)
|
||||
"#;
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse CREATE relationship: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "MERGE ON CREATE SET not yet implemented"]
|
||||
fn test_merge_pattern() {
|
||||
let query = "MERGE (n:Person {name: 'Alice'}) ON CREATE SET n.created = 2024";
|
||||
let result = parse_cypher(query);
|
||||
assert!(result.is_ok(), "Failed to parse MERGE: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_clause() {
|
||||
let query = r#"
|
||||
MATCH (n:Person)
|
||||
WITH n, n.age AS age
|
||||
WHERE age > 30
|
||||
RETURN n.name, age
|
||||
"#;
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse WITH clause: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_pattern() {
|
||||
let query = "MATCH p = (a:Person)-[*1..3]->(b:Person) RETURN p";
|
||||
let result = parse_cypher(query);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse path pattern: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_read_only() {
|
||||
let query1 = "MATCH (n:Person) RETURN n";
|
||||
let ast1 = parse_cypher(query1).unwrap();
|
||||
assert!(ast1.is_read_only());
|
||||
|
||||
let query2 = "CREATE (n:Person {name: 'Alice'})";
|
||||
let ast2 = parse_cypher(query2).unwrap();
|
||||
assert!(!ast2.is_read_only());
|
||||
}
|
||||
223
vendor/ruvector/crates/ruvector-graph/tests/cypher_parser_tests.rs
vendored
Normal file
223
vendor/ruvector/crates/ruvector-graph/tests/cypher_parser_tests.rs
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
//! Cypher query parser tests
|
||||
//!
|
||||
//! Tests for parsing valid and invalid Cypher queries to ensure syntax correctness.
|
||||
|
||||
use ruvector_graph::cypher::parse_cypher;
|
||||
|
||||
// ============================================================================
|
||||
// Valid Cypher Queries
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_match() {
|
||||
let result = parse_cypher("MATCH (n) RETURN n");
|
||||
assert!(result.is_ok(), "Parse failed: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_with_label() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN n");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_with_properties() {
|
||||
let result = parse_cypher("MATCH (n:Person {name: 'Alice'}) RETURN n");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_relationship() {
|
||||
let result = parse_cypher("MATCH (a)-[r:KNOWS]->(b) RETURN a, r, b");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_undirected_relationship() {
|
||||
let result = parse_cypher("MATCH (a)-[r:FRIEND]-(b) RETURN a, b");
|
||||
assert!(result.is_ok(), "Parse failed: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_path() {
|
||||
let result = parse_cypher("MATCH p = (a)-[:KNOWS*1..3]->(b) RETURN p");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_create_node() {
|
||||
let result = parse_cypher("CREATE (n:Person {name: 'Bob', age: 30})");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_create_relationship() {
|
||||
let result =
|
||||
parse_cypher("CREATE (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_merge() {
|
||||
let result = parse_cypher("MERGE (n:Person {name: 'Charlie'})");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_delete() {
|
||||
let result = parse_cypher("MATCH (n:Person {name: 'Alice'}) DELETE n");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_set_property() {
|
||||
let result = parse_cypher("MATCH (n:Person {name: 'Alice'}) SET n.age = 31");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remove_property() {
|
||||
let result = parse_cypher("MATCH (n:Person {name: 'Alice'}) REMOVE n.age");
|
||||
assert!(result.is_ok(), "Parse failed: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_where_clause() {
|
||||
let result = parse_cypher("MATCH (n:Person) WHERE n.age > 25 RETURN n");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_order_by() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN n ORDER BY n.age DESC");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_limit() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN n LIMIT 10");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_skip() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN n SKIP 5 LIMIT 10");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_aggregate_count() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN COUNT(n)");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_aggregate_sum() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN SUM(n.age)");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_aggregate_avg() {
|
||||
let result = parse_cypher("MATCH (n:Person) RETURN AVG(n.age)");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_with_clause() {
|
||||
let result = parse_cypher("MATCH (n:Person) WITH n.age AS age WHERE age > 25 RETURN age");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_optional_match() {
|
||||
let result = parse_cypher("OPTIONAL MATCH (n:Person)-[r:KNOWS]->(m) RETURN n, m");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Complex Query Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
#[ignore = "Complex multi-direction patterns with <- not yet fully implemented"]
|
||||
fn test_parse_complex_graph_pattern() {
|
||||
let result = parse_cypher(
|
||||
"
|
||||
MATCH (user:User)-[:PURCHASED]->(product:Product)<-[:PURCHASED]-(other:User)
|
||||
WHERE other.id <> 123
|
||||
WITH other, COUNT(*) AS commonProducts
|
||||
WHERE commonProducts > 3
|
||||
RETURN other.name
|
||||
ORDER BY commonProducts DESC
|
||||
LIMIT 10
|
||||
",
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_variable_length_path() {
|
||||
let result =
|
||||
parse_cypher("MATCH (a:Person)-[:KNOWS*1..5]->(b:Person) WHERE a.name = 'Alice' RETURN b");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_patterns() {
|
||||
let result = parse_cypher(
|
||||
"
|
||||
MATCH (a:Person)-[:KNOWS]->(b:Person)
|
||||
MATCH (b)-[:WORKS_AT]->(c:Company)
|
||||
RETURN a.name, b.name, c.name
|
||||
",
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_collect_aggregation() {
|
||||
let result = parse_cypher(
|
||||
"MATCH (p:Person)-[:KNOWS]->(f:Person) RETURN p.name, COLLECT(f.name) AS friends",
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
#[ignore = "Empty query validation not yet implemented"]
|
||||
fn test_parse_empty_query() {
|
||||
let result = parse_cypher("");
|
||||
// Empty query should fail
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Whitespace-only query validation not yet implemented"]
|
||||
fn test_parse_whitespace_only() {
|
||||
let result = parse_cypher(" \n\t ");
|
||||
// Whitespace only should fail
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_parameters() {
|
||||
let result = parse_cypher("MATCH (n:Person {name: $name, age: $age}) RETURN n");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_list_literal() {
|
||||
let result = parse_cypher("RETURN [1, 2, 3, 4, 5] AS numbers");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Map literal in RETURN not yet implemented"]
|
||||
fn test_parse_map_literal() {
|
||||
let result = parse_cypher("RETURN {name: 'Alice', age: 30} AS person");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
295
vendor/ruvector/crates/ruvector-graph/tests/distributed_tests.rs
vendored
Normal file
295
vendor/ruvector/crates/ruvector-graph/tests/distributed_tests.rs
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
//! Distributed graph database tests
|
||||
//!
|
||||
//! Tests for clustering, replication, sharding, and federation.
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_distributed() {
|
||||
// TODO: Implement distributed tests when distributed features are available
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cluster Setup Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_three_node_cluster() {
|
||||
// // TODO: Set up a 3-node cluster
|
||||
// // Verify all nodes can communicate
|
||||
// // Verify leader election works
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_cluster_discovery() {
|
||||
// // TODO: Test node discovery mechanism
|
||||
// // New node should discover existing cluster
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Data Sharding Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_hash_based_sharding() {
|
||||
// // TODO: Test that data is distributed across shards based on hash
|
||||
// // Create nodes on different shards
|
||||
// // Verify they end up on correct nodes
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_range_based_sharding() {
|
||||
// // TODO: Test range-based sharding for ordered data
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_shard_rebalancing() {
|
||||
// // TODO: Test automatic rebalancing when adding/removing nodes
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Replication Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_synchronous_replication() {
|
||||
// // TODO: Write to leader, verify data appears on all replicas
|
||||
// // before write is acknowledged
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_asynchronous_replication() {
|
||||
// // TODO: Write to leader, verify data eventually appears on replicas
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_replica_consistency() {
|
||||
// // TODO: Verify all replicas have same data
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_read_from_replica() {
|
||||
// // TODO: Verify reads can be served from replicas
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_replica_lag_monitoring() {
|
||||
// // TODO: Monitor replication lag
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Leader Election Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_leader_election_on_startup() {
|
||||
// // TODO: Start cluster, verify leader is elected
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_leader_failover() {
|
||||
// // TODO: Kill leader, verify new leader is elected
|
||||
// // Verify cluster remains available
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_split_brain_prevention() {
|
||||
// // TODO: Simulate network partition
|
||||
// // Verify that split brain doesn't occur
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Distributed Queries
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_cross_shard_query() {
|
||||
// // TODO: Query that requires data from multiple shards
|
||||
// // Verify correct results are returned
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_aggregation() {
|
||||
// // TODO: Aggregation query across shards
|
||||
// // COUNT, SUM, etc. should work correctly
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_traversal() {
|
||||
// // TODO: Graph traversal that crosses shard boundaries
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_shortest_path() {
|
||||
// // TODO: Shortest path query where path crosses shards
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Distributed Transactions
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_two_phase_commit() {
|
||||
// // TODO: Transaction spanning multiple shards
|
||||
// // Verify 2PC ensures atomicity
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_deadlock_detection() {
|
||||
// // TODO: Create scenario that could cause distributed deadlock
|
||||
// // Verify detection and resolution
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_rollback() {
|
||||
// // TODO: Transaction that fails on one shard
|
||||
// // Verify all shards roll back
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Fault Tolerance Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_node_failure_recovery() {
|
||||
// // TODO: Kill a node, verify cluster recovers
|
||||
// // Data should still be accessible via replicas
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_network_partition_handling() {
|
||||
// // TODO: Simulate network partition
|
||||
// // Verify cluster handles it gracefully
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_data_recovery_after_crash() {
|
||||
// // TODO: Node crashes, then restarts
|
||||
// // Verify it can rejoin cluster and catch up
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_quorum_based_operations() {
|
||||
// // TODO: Verify operations require quorum
|
||||
// // If quorum lost, writes should fail
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Federation Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_cross_cluster_query() {
|
||||
// // TODO: Query that spans multiple independent clusters
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_federated_search() {
|
||||
// // TODO: Search across federated clusters
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_cluster_to_cluster_replication() {
|
||||
// // TODO: Data replication between clusters
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Consistency Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_strong_consistency() {
|
||||
// // TODO: With strong consistency level, verify linearizability
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_eventual_consistency() {
|
||||
// // TODO: With eventual consistency, verify data converges
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_causal_consistency() {
|
||||
// // TODO: Verify causal relationships are preserved
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_read_your_writes() {
|
||||
// // TODO: Client should always see its own writes
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_horizontal_scalability() {
|
||||
// // TODO: Measure throughput with 1, 2, 4, 8 nodes
|
||||
// // Verify near-linear scaling
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_load_balancing() {
|
||||
// // TODO: Verify load is balanced across nodes
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_hotspot_handling() {
|
||||
// // TODO: Create hotspot (frequently accessed data)
|
||||
// // Verify system handles it gracefully
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Tests
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_replication_factor_configuration() {
|
||||
// // TODO: Test different replication factors (1, 2, 3)
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_consistency_level_configuration() {
|
||||
// // TODO: Test different consistency levels
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_partition_strategy_configuration() {
|
||||
// // TODO: Test different partitioning strategies
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Monitoring and Observability
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_cluster_health_monitoring() {
|
||||
// // TODO: Verify cluster health metrics are available
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_shard_distribution_metrics() {
|
||||
// // TODO: Verify we can monitor shard distribution
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_replication_lag_metrics() {
|
||||
// // TODO: Verify replication lag is monitored
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Backup and Restore
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_backup() {
|
||||
// // TODO: Create backup of distributed database
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_distributed_restore() {
|
||||
// // TODO: Restore from backup to new cluster
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_point_in_time_recovery() {
|
||||
// // TODO: Restore to specific point in time
|
||||
// }
|
||||
371
vendor/ruvector/crates/ruvector-graph/tests/edge_tests.rs
vendored
Normal file
371
vendor/ruvector/crates/ruvector-graph/tests/edge_tests.rs
vendored
Normal file
@@ -0,0 +1,371 @@
|
||||
//! Edge (relationship) operation tests
|
||||
//!
|
||||
//! Tests for creating edges, querying relationships, and graph traversals.
|
||||
|
||||
use ruvector_graph::{Edge, EdgeBuilder, GraphDB, Label, Node, Properties, PropertyValue};
|
||||
|
||||
#[test]
|
||||
fn test_create_edge_basic() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Create nodes first
|
||||
let node1 = Node::new(
|
||||
"person1".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
Properties::new(),
|
||||
);
|
||||
let node2 = Node::new(
|
||||
"person2".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_node(node1).unwrap();
|
||||
db.create_node(node2).unwrap();
|
||||
|
||||
// Create edge
|
||||
let edge = Edge::new(
|
||||
"edge1".to_string(),
|
||||
"person1".to_string(),
|
||||
"person2".to_string(),
|
||||
"KNOWS".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
let edge_id = db.create_edge(edge).unwrap();
|
||||
assert_eq!(edge_id, "edge1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_edge_existing() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Setup nodes
|
||||
let node1 = Node::new("n1".to_string(), vec![], Properties::new());
|
||||
let node2 = Node::new("n2".to_string(), vec![], Properties::new());
|
||||
db.create_node(node1).unwrap();
|
||||
db.create_node(node2).unwrap();
|
||||
|
||||
// Create edge with properties
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("since".to_string(), PropertyValue::Integer(2020));
|
||||
|
||||
let edge = Edge::new(
|
||||
"e1".to_string(),
|
||||
"n1".to_string(),
|
||||
"n2".to_string(),
|
||||
"FRIEND_OF".to_string(),
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge("e1").unwrap();
|
||||
assert_eq!(retrieved.id, "e1");
|
||||
assert_eq!(retrieved.from, "n1");
|
||||
assert_eq!(retrieved.to, "n2");
|
||||
assert_eq!(retrieved.edge_type, "FRIEND_OF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_with_properties() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Setup
|
||||
db.create_node(Node::new("a".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
db.create_node(Node::new("b".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("weight".to_string(), PropertyValue::Float(0.85));
|
||||
properties.insert(
|
||||
"type".to_string(),
|
||||
PropertyValue::String("strong".to_string()),
|
||||
);
|
||||
properties.insert("verified".to_string(), PropertyValue::Boolean(true));
|
||||
|
||||
let edge = Edge::new(
|
||||
"weighted_edge".to_string(),
|
||||
"a".to_string(),
|
||||
"b".to_string(),
|
||||
"CONNECTED_TO".to_string(),
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge("weighted_edge").unwrap();
|
||||
assert_eq!(
|
||||
retrieved.properties.get("weight"),
|
||||
Some(&PropertyValue::Float(0.85))
|
||||
);
|
||||
assert_eq!(
|
||||
retrieved.properties.get("verified"),
|
||||
Some(&PropertyValue::Boolean(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bidirectional_edges() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
db.create_node(Node::new("alice".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
db.create_node(Node::new("bob".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
// Alice -> Bob
|
||||
let edge1 = Edge::new(
|
||||
"e1".to_string(),
|
||||
"alice".to_string(),
|
||||
"bob".to_string(),
|
||||
"FOLLOWS".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
// Bob -> Alice
|
||||
let edge2 = Edge::new(
|
||||
"e2".to_string(),
|
||||
"bob".to_string(),
|
||||
"alice".to_string(),
|
||||
"FOLLOWS".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge1).unwrap();
|
||||
db.create_edge(edge2).unwrap();
|
||||
|
||||
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, "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_loop_edge() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
db.create_node(Node::new("node".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
let edge = Edge::new(
|
||||
"self_loop".to_string(),
|
||||
"node".to_string(),
|
||||
"node".to_string(),
|
||||
"REFERENCES".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge("self_loop").unwrap();
|
||||
assert_eq!(retrieved.from, retrieved.to);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_edges_same_nodes() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
db.create_node(Node::new("x".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
db.create_node(Node::new("y".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
// Multiple relationship types between same nodes
|
||||
let edge1 = Edge::new(
|
||||
"e1".to_string(),
|
||||
"x".to_string(),
|
||||
"y".to_string(),
|
||||
"WORKS_WITH".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
let edge2 = Edge::new(
|
||||
"e2".to_string(),
|
||||
"x".to_string(),
|
||||
"y".to_string(),
|
||||
"FRIENDS_WITH".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge1).unwrap();
|
||||
db.create_edge(edge2).unwrap();
|
||||
|
||||
assert!(db.get_edge("e1").is_some());
|
||||
assert!(db.get_edge("e2").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_timestamp_property() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
db.create_node(Node::new("user1".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
db.create_node(Node::new("post1".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("timestamp".to_string(), PropertyValue::Integer(1699564800));
|
||||
properties.insert(
|
||||
"action".to_string(),
|
||||
PropertyValue::String("liked".to_string()),
|
||||
);
|
||||
|
||||
let edge = Edge::new(
|
||||
"interaction".to_string(),
|
||||
"user1".to_string(),
|
||||
"post1".to_string(),
|
||||
"INTERACTED".to_string(),
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge("interaction").unwrap();
|
||||
assert!(retrieved.properties.contains_key("timestamp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_nonexistent_edge() {
|
||||
let db = GraphDB::new();
|
||||
let result = db.get_edge("does_not_exist");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_many_edges() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Create hub node
|
||||
db.create_node(Node::new("hub".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
// Create 100 spoke nodes
|
||||
for i in 0..100 {
|
||||
let node_id = format!("spoke_{}", i);
|
||||
db.create_node(Node::new(node_id.clone(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
let edge = Edge::new(
|
||||
format!("edge_{}", i),
|
||||
"hub".to_string(),
|
||||
node_id,
|
||||
"CONNECTS".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
}
|
||||
|
||||
// Verify all edges exist
|
||||
for i in 0..100 {
|
||||
assert!(db.get_edge(&format!("edge_{}", i)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_builder() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
db.create_node(Node::new("a".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
db.create_node(Node::new("b".to_string(), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
|
||||
let edge = EdgeBuilder::new("a".to_string(), "b".to_string(), "KNOWS")
|
||||
.id("e1")
|
||||
.property("since", 2020i64)
|
||||
.property("weight", 0.95f64)
|
||||
.build();
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge("e1").unwrap();
|
||||
assert_eq!(retrieved.from, "a");
|
||||
assert_eq!(retrieved.to, "b");
|
||||
assert_eq!(retrieved.edge_type, "KNOWS");
|
||||
assert_eq!(
|
||||
retrieved.get_property("since"),
|
||||
Some(&PropertyValue::Integer(2020))
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property-based tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn edge_id_strategy() -> impl Strategy<Value = String> {
|
||||
"[a-z][a-z0-9_]{0,20}".prop_map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn edge_type_strategy() -> impl Strategy<Value = String> {
|
||||
"[A-Z_]{2,15}".prop_map(|s| s.to_string())
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_edge_roundtrip(
|
||||
edge_id in edge_id_strategy(),
|
||||
edge_type in edge_type_strategy()
|
||||
) {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Setup nodes
|
||||
db.create_node(Node::new("from".to_string(), vec![], Properties::new())).unwrap();
|
||||
db.create_node(Node::new("to".to_string(), vec![], Properties::new())).unwrap();
|
||||
|
||||
let edge = Edge::new(
|
||||
edge_id.clone(),
|
||||
"from".to_string(),
|
||||
"to".to_string(),
|
||||
edge_type.clone(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
|
||||
let retrieved = db.get_edge(&edge_id).unwrap();
|
||||
assert_eq!(retrieved.id, edge_id);
|
||||
assert_eq!(retrieved.edge_type, edge_type);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many_edges_unique(
|
||||
edge_ids in prop::collection::hash_set(edge_id_strategy(), 10..50)
|
||||
) {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Create source and target nodes
|
||||
db.create_node(Node::new("source".to_string(), vec![], Properties::new())).unwrap();
|
||||
db.create_node(Node::new("target".to_string(), vec![], Properties::new())).unwrap();
|
||||
|
||||
for edge_id in &edge_ids {
|
||||
let edge = Edge::new(
|
||||
edge_id.clone(),
|
||||
"source".to_string(),
|
||||
"target".to_string(),
|
||||
"TEST".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
db.create_edge(edge).unwrap();
|
||||
}
|
||||
|
||||
for edge_id in &edge_ids {
|
||||
assert!(db.get_edge(edge_id).is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
vendor/ruvector/crates/ruvector-graph/tests/fixtures/README.md
vendored
Normal file
51
vendor/ruvector/crates/ruvector-graph/tests/fixtures/README.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# Test Fixtures
|
||||
|
||||
This directory contains sample datasets and expected results for testing the RuVector graph database.
|
||||
|
||||
## Datasets
|
||||
|
||||
### movie_database.json
|
||||
A small movie database inspired by Neo4j's example dataset:
|
||||
- 3 actors (Keanu Reeves, Carrie-Anne Moss, Laurence Fishburne)
|
||||
- 1 movie (The Matrix)
|
||||
- 3 ACTED_IN relationships with role properties
|
||||
|
||||
### social_network.json
|
||||
A simple social network for testing graph algorithms:
|
||||
- 5 people
|
||||
- 6 KNOWS relationships forming a small network
|
||||
|
||||
## Expected Results
|
||||
|
||||
### expected_results.json
|
||||
Contains test cases with:
|
||||
- Query text (Cypher)
|
||||
- Which dataset to use
|
||||
- Expected query results
|
||||
|
||||
Use these to validate that query execution returns correct results.
|
||||
|
||||
## Usage in Tests
|
||||
|
||||
```rust
|
||||
use std::fs;
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn test_with_fixture() {
|
||||
let fixture = fs::read_to_string("tests/fixtures/movie_database.json").unwrap();
|
||||
let data: Value = serde_json::from_str(&fixture).unwrap();
|
||||
|
||||
// Load data into graph
|
||||
// Execute queries
|
||||
// Validate against expected results
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Fixtures
|
||||
|
||||
When adding new fixtures:
|
||||
1. Follow the JSON schema used in existing files
|
||||
2. Add corresponding expected results
|
||||
3. Document the dataset purpose
|
||||
4. Keep datasets small and focused on specific test scenarios
|
||||
48
vendor/ruvector/crates/ruvector-graph/tests/fixtures/expected_results.json
vendored
Normal file
48
vendor/ruvector/crates/ruvector-graph/tests/fixtures/expected_results.json
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"description": "Expected query results for validation",
|
||||
"test_cases": [
|
||||
{
|
||||
"name": "count_all_nodes",
|
||||
"query": "MATCH (n) RETURN COUNT(n)",
|
||||
"dataset": "movie_database",
|
||||
"expected": [{"COUNT(n)": 4}]
|
||||
},
|
||||
{
|
||||
"name": "match_person_nodes",
|
||||
"query": "MATCH (p:Person) RETURN p.name ORDER BY p.name",
|
||||
"dataset": "movie_database",
|
||||
"expected": [
|
||||
{"p.name": "Carrie-Anne Moss"},
|
||||
{"p.name": "Keanu Reeves"},
|
||||
{"p.name": "Laurence Fishburne"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "count_relationships",
|
||||
"query": "MATCH ()-[r:ACTED_IN]->() RETURN COUNT(r)",
|
||||
"dataset": "movie_database",
|
||||
"expected": [{"COUNT(r)": 3}]
|
||||
},
|
||||
{
|
||||
"name": "social_network_friends",
|
||||
"query": "MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(friend) RETURN friend.name ORDER BY friend.name",
|
||||
"dataset": "social_network",
|
||||
"expected": [
|
||||
{"friend.name": "Bob"},
|
||||
{"friend.name": "Charlie"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "average_age",
|
||||
"query": "MATCH (p:Person) RETURN AVG(p.age) AS avg_age",
|
||||
"dataset": "social_network",
|
||||
"expected": [{"avg_age": 30.4}]
|
||||
},
|
||||
{
|
||||
"name": "people_born_after_1965",
|
||||
"query": "MATCH (p:Person) WHERE p.born > 1965 RETURN p.name",
|
||||
"dataset": "movie_database",
|
||||
"expected": [{"p.name": "Carrie-Anne Moss"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
67
vendor/ruvector/crates/ruvector-graph/tests/fixtures/movie_database.json
vendored
Normal file
67
vendor/ruvector/crates/ruvector-graph/tests/fixtures/movie_database.json
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"description": "Sample movie database for testing",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "keanu",
|
||||
"labels": ["Person"],
|
||||
"properties": {
|
||||
"name": "Keanu Reeves",
|
||||
"born": 1964
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "carrie",
|
||||
"labels": ["Person"],
|
||||
"properties": {
|
||||
"name": "Carrie-Anne Moss",
|
||||
"born": 1967
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "laurence",
|
||||
"labels": ["Person"],
|
||||
"properties": {
|
||||
"name": "Laurence Fishburne",
|
||||
"born": 1961
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "matrix",
|
||||
"labels": ["Movie"],
|
||||
"properties": {
|
||||
"title": "The Matrix",
|
||||
"released": 1999,
|
||||
"tagline": "Welcome to the Real World"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"from": "keanu",
|
||||
"to": "matrix",
|
||||
"type": "ACTED_IN",
|
||||
"properties": {
|
||||
"roles": ["Neo"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"from": "carrie",
|
||||
"to": "matrix",
|
||||
"type": "ACTED_IN",
|
||||
"properties": {
|
||||
"roles": ["Trinity"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"from": "laurence",
|
||||
"to": "matrix",
|
||||
"type": "ACTED_IN",
|
||||
"properties": {
|
||||
"roles": ["Morpheus"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
vendor/ruvector/crates/ruvector-graph/tests/fixtures/social_network.json
vendored
Normal file
18
vendor/ruvector/crates/ruvector-graph/tests/fixtures/social_network.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "Sample social network for testing",
|
||||
"nodes": [
|
||||
{"id": "alice", "labels": ["Person"], "properties": {"name": "Alice", "age": 30}},
|
||||
{"id": "bob", "labels": ["Person"], "properties": {"name": "Bob", "age": 35}},
|
||||
{"id": "charlie", "labels": ["Person"], "properties": {"name": "Charlie", "age": 28}},
|
||||
{"id": "diana", "labels": ["Person"], "properties": {"name": "Diana", "age": 32}},
|
||||
{"id": "eve", "labels": ["Person"], "properties": {"name": "Eve", "age": 27}}
|
||||
],
|
||||
"edges": [
|
||||
{"id": "e1", "from": "alice", "to": "bob", "type": "KNOWS", "properties": {"since": 2015}},
|
||||
{"id": "e2", "from": "alice", "to": "charlie", "type": "KNOWS", "properties": {"since": 2018}},
|
||||
{"id": "e3", "from": "bob", "to": "charlie", "type": "KNOWS", "properties": {"since": 2016}},
|
||||
{"id": "e4", "from": "bob", "to": "diana", "type": "KNOWS", "properties": {"since": 2019}},
|
||||
{"id": "e5", "from": "charlie", "to": "eve", "type": "KNOWS", "properties": {"since": 2020}},
|
||||
{"id": "e6", "from": "diana", "to": "eve", "type": "KNOWS", "properties": {"since": 2017}}
|
||||
]
|
||||
}
|
||||
461
vendor/ruvector/crates/ruvector-graph/tests/hyperedge_tests.rs
vendored
Normal file
461
vendor/ruvector/crates/ruvector-graph/tests/hyperedge_tests.rs
vendored
Normal file
@@ -0,0 +1,461 @@
|
||||
//! Hyperedge (N-ary relationship) tests
|
||||
//!
|
||||
//! Tests for hypergraph features supporting relationships between multiple nodes.
|
||||
//! Based on the existing hypergraph implementation in ruvector-core.
|
||||
|
||||
use ruvector_core::advanced::hypergraph::{
|
||||
Hyperedge, HypergraphIndex, TemporalGranularity, TemporalHyperedge,
|
||||
};
|
||||
use ruvector_core::types::DistanceMetric;
|
||||
|
||||
#[test]
|
||||
fn test_create_binary_hyperedge() {
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Alice knows Bob".to_string(),
|
||||
vec![0.1, 0.2, 0.3],
|
||||
0.95,
|
||||
);
|
||||
|
||||
assert_eq!(edge.order(), 2);
|
||||
assert!(edge.contains_node(&"1".to_string()));
|
||||
assert!(edge.contains_node(&"2".to_string()));
|
||||
assert!(!edge.contains_node(&"3".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_ternary_hyperedge() {
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
"Meeting between Alice, Bob, and Charlie".to_string(),
|
||||
vec![0.5; 128],
|
||||
0.90,
|
||||
);
|
||||
|
||||
assert_eq!(edge.order(), 3);
|
||||
assert!(edge.contains_node(&"1".to_string()));
|
||||
assert!(edge.contains_node(&"2".to_string()));
|
||||
assert!(edge.contains_node(&"3".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_large_hyperedge() {
|
||||
let nodes: Vec<String> = (0..100).map(|i| i.to_string()).collect();
|
||||
let edge = Hyperedge::new(
|
||||
nodes.clone(),
|
||||
"Large group collaboration".to_string(),
|
||||
vec![0.1; 64],
|
||||
0.75,
|
||||
);
|
||||
|
||||
assert_eq!(edge.order(), 100);
|
||||
for node in nodes {
|
||||
assert!(edge.contains_node(&node));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hyperedge_confidence_clamping() {
|
||||
let edge1 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Test".to_string(),
|
||||
vec![0.1],
|
||||
1.5,
|
||||
);
|
||||
assert_eq!(edge1.confidence, 1.0);
|
||||
|
||||
let edge2 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Test".to_string(),
|
||||
vec![0.1],
|
||||
-0.5,
|
||||
);
|
||||
assert_eq!(edge2.confidence, 0.0);
|
||||
|
||||
let edge3 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Test".to_string(),
|
||||
vec![0.1],
|
||||
0.75,
|
||||
);
|
||||
assert_eq!(edge3.confidence, 0.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_temporal_hyperedge_creation() {
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
"Temporal relationship".to_string(),
|
||||
vec![0.5; 32],
|
||||
0.9,
|
||||
);
|
||||
|
||||
let temporal = TemporalHyperedge::new(edge, TemporalGranularity::Hourly);
|
||||
|
||||
assert!(!temporal.is_expired());
|
||||
assert!(temporal.timestamp > 0);
|
||||
assert!(temporal.time_bucket() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_temporal_granularity_bucketing() {
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Test".to_string(),
|
||||
vec![0.1],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let hourly = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Hourly);
|
||||
let daily = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Daily);
|
||||
let monthly = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Monthly);
|
||||
|
||||
// Different granularities should produce different buckets
|
||||
assert!(hourly.time_bucket() >= daily.time_bucket());
|
||||
assert!(daily.time_bucket() >= monthly.time_bucket());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergraph_index_basic() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Add entities
|
||||
index.add_entity("1".to_string(), vec![1.0, 0.0, 0.0]);
|
||||
index.add_entity("2".to_string(), vec![0.0, 1.0, 0.0]);
|
||||
index.add_entity("3".to_string(), vec![0.0, 0.0, 1.0]);
|
||||
|
||||
// Add hyperedge
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
"Triangle relationship".to_string(),
|
||||
vec![0.33, 0.33, 0.34],
|
||||
0.95,
|
||||
);
|
||||
|
||||
index.add_hyperedge(edge).unwrap();
|
||||
|
||||
let stats = index.stats();
|
||||
assert_eq!(stats.total_entities, 3);
|
||||
assert_eq!(stats.total_hyperedges, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergraph_multiple_hyperedges() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Euclidean);
|
||||
|
||||
// Add entities
|
||||
for i in 1..=5 {
|
||||
index.add_entity(i.to_string(), vec![i as f32; 64]);
|
||||
}
|
||||
|
||||
// Add multiple hyperedges with different orders
|
||||
let edge1 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Binary".to_string(),
|
||||
vec![0.5; 64],
|
||||
1.0,
|
||||
);
|
||||
let edge2 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
"Ternary".to_string(),
|
||||
vec![0.5; 64],
|
||||
1.0,
|
||||
);
|
||||
let edge3 = Hyperedge::new(
|
||||
vec![
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
],
|
||||
"Quaternary".to_string(),
|
||||
vec![0.5; 64],
|
||||
1.0,
|
||||
);
|
||||
let edge4 = Hyperedge::new(
|
||||
vec![
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
"5".to_string(),
|
||||
],
|
||||
"Quinary".to_string(),
|
||||
vec![0.5; 64],
|
||||
1.0,
|
||||
);
|
||||
|
||||
index.add_hyperedge(edge1).unwrap();
|
||||
index.add_hyperedge(edge2).unwrap();
|
||||
index.add_hyperedge(edge3).unwrap();
|
||||
index.add_hyperedge(edge4).unwrap();
|
||||
|
||||
let stats = index.stats();
|
||||
assert_eq!(stats.total_hyperedges, 4);
|
||||
assert!(stats.avg_entity_degree > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergraph_search() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Add entities
|
||||
for i in 1..=10 {
|
||||
index.add_entity(i.to_string(), vec![i as f32 * 0.1; 32]);
|
||||
}
|
||||
|
||||
// Add hyperedges
|
||||
for i in 1..=5 {
|
||||
let edge = Hyperedge::new(
|
||||
vec![i.to_string(), (i + 1).to_string()],
|
||||
format!("Edge {}", i),
|
||||
vec![i as f32 * 0.1; 32],
|
||||
0.9,
|
||||
);
|
||||
index.add_hyperedge(edge).unwrap();
|
||||
}
|
||||
|
||||
// Search for similar hyperedges
|
||||
let query = vec![0.3; 32];
|
||||
let results = index.search_hyperedges(&query, 3);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
// Results should be sorted by distance
|
||||
for i in 0..results.len() - 1 {
|
||||
assert!(results[i].1 <= results[i + 1].1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_k_hop_neighbors_simple() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Create chain: 1-2-3-4
|
||||
for i in 1..=4 {
|
||||
index.add_entity(i.to_string(), vec![i as f32]);
|
||||
}
|
||||
|
||||
let e1 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"e1".to_string(),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
let e2 = Hyperedge::new(
|
||||
vec!["2".to_string(), "3".to_string()],
|
||||
"e2".to_string(),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
let e3 = Hyperedge::new(
|
||||
vec!["3".to_string(), "4".to_string()],
|
||||
"e3".to_string(),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
|
||||
index.add_hyperedge(e1).unwrap();
|
||||
index.add_hyperedge(e2).unwrap();
|
||||
index.add_hyperedge(e3).unwrap();
|
||||
|
||||
// 1-hop from node 1 should include 1 and 2
|
||||
let neighbors_1hop = index.k_hop_neighbors("1".to_string(), 1);
|
||||
assert!(neighbors_1hop.contains(&"1".to_string()));
|
||||
assert!(neighbors_1hop.contains(&"2".to_string()));
|
||||
|
||||
// 2-hop from node 1 should include 1, 2, and 3
|
||||
let neighbors_2hop = index.k_hop_neighbors("1".to_string(), 2);
|
||||
assert!(neighbors_2hop.contains(&"1".to_string()));
|
||||
assert!(neighbors_2hop.contains(&"2".to_string()));
|
||||
assert!(neighbors_2hop.contains(&"3".to_string()));
|
||||
|
||||
// 3-hop from node 1 should include all nodes
|
||||
let neighbors_3hop = index.k_hop_neighbors("1".to_string(), 3);
|
||||
assert_eq!(neighbors_3hop.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_k_hop_neighbors_complex() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Create star topology: center node connected to 5 peripheral nodes
|
||||
for i in 0..=5 {
|
||||
index.add_entity(i.to_string(), vec![i as f32]);
|
||||
}
|
||||
|
||||
// Center (0) connected to all others via hyperedges
|
||||
for i in 1..=5 {
|
||||
let edge = Hyperedge::new(
|
||||
vec!["0".to_string(), i.to_string()],
|
||||
format!("e{}", i),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
index.add_hyperedge(edge).unwrap();
|
||||
}
|
||||
|
||||
// 1-hop from center should reach all nodes
|
||||
let neighbors = index.k_hop_neighbors("0".to_string(), 1);
|
||||
assert_eq!(neighbors.len(), 6); // All nodes
|
||||
|
||||
// 1-hop from peripheral node should reach center and itself
|
||||
let neighbors = index.k_hop_neighbors("1".to_string(), 1);
|
||||
assert!(neighbors.contains(&"0".to_string()));
|
||||
assert!(neighbors.contains(&"1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_temporal_range_query() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Add entities
|
||||
for i in 1..=3 {
|
||||
index.add_entity(i.to_string(), vec![i as f32]);
|
||||
}
|
||||
|
||||
// Add temporal hyperedges (they'll all be in current time bucket)
|
||||
let edge1 = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"t1".to_string(),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
let edge2 = Hyperedge::new(
|
||||
vec!["2".to_string(), "3".to_string()],
|
||||
"t2".to_string(),
|
||||
vec![1.0],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let temp1 = TemporalHyperedge::new(edge1, TemporalGranularity::Hourly);
|
||||
let temp2 = TemporalHyperedge::new(edge2, TemporalGranularity::Hourly);
|
||||
|
||||
let bucket = temp1.time_bucket();
|
||||
|
||||
index.add_temporal_hyperedge(temp1).unwrap();
|
||||
index.add_temporal_hyperedge(temp2).unwrap();
|
||||
|
||||
// Query current time bucket
|
||||
let results = index.query_temporal_range(bucket, bucket);
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hyperedge_with_duplicate_nodes() {
|
||||
// Test that hyperedge handles duplicate nodes appropriately
|
||||
let edge = Hyperedge::new(
|
||||
vec![
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
], // Duplicate node 2
|
||||
"Duplicate test".to_string(),
|
||||
vec![0.5; 16],
|
||||
0.8,
|
||||
);
|
||||
|
||||
assert_eq!(edge.order(), 4); // Includes duplicates
|
||||
assert!(edge.contains_node(&"2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergraph_error_on_missing_entity() {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Only add entity 1, not 2
|
||||
index.add_entity("1".to_string(), vec![1.0]);
|
||||
|
||||
// Try to create hyperedge with missing entity
|
||||
let edge = Hyperedge::new(
|
||||
vec!["1".to_string(), "2".to_string()],
|
||||
"Test".to_string(),
|
||||
vec![0.5],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let result = index.add_hyperedge(edge);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property-based tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn node_vec_strategy() -> impl Strategy<Value = Vec<String>> {
|
||||
prop::collection::vec("[a-z]{1,5}".prop_map(|s| s), 2..20)
|
||||
}
|
||||
|
||||
fn embedding_strategy(dim: usize) -> impl Strategy<Value = Vec<f32>> {
|
||||
prop::collection::vec(-1.0f32..1.0f32, dim)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_hyperedge_order_property(
|
||||
nodes in node_vec_strategy()
|
||||
) {
|
||||
let edge = Hyperedge::new(
|
||||
nodes.clone(),
|
||||
"Test".to_string(),
|
||||
vec![0.5; 32],
|
||||
0.9
|
||||
);
|
||||
|
||||
assert_eq!(edge.order(), nodes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hyperedge_contains_all_nodes(
|
||||
nodes in node_vec_strategy()
|
||||
) {
|
||||
let edge = Hyperedge::new(
|
||||
nodes.clone(),
|
||||
"Test".to_string(),
|
||||
vec![0.5; 32],
|
||||
0.9
|
||||
);
|
||||
|
||||
for node in &nodes {
|
||||
assert!(edge.contains_node(node));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergraph_search_consistency(
|
||||
query in embedding_strategy(32),
|
||||
k in 1usize..10
|
||||
) {
|
||||
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
|
||||
|
||||
// Add entities
|
||||
for i in 1..=10 {
|
||||
index.add_entity(i.to_string(), vec![i as f32 * 0.1; 32]);
|
||||
}
|
||||
|
||||
// Add hyperedges
|
||||
for i in 1..=10 {
|
||||
let edge = Hyperedge::new(
|
||||
vec![i.to_string()],
|
||||
format!("Edge {}", i),
|
||||
vec![i as f32 * 0.1; 32],
|
||||
0.9
|
||||
);
|
||||
index.add_hyperedge(edge).unwrap();
|
||||
}
|
||||
|
||||
let results = index.search_hyperedges(&query, k.min(10));
|
||||
assert!(results.len() <= k.min(10));
|
||||
|
||||
// Verify results are sorted
|
||||
for i in 0..results.len().saturating_sub(1) {
|
||||
assert!(results[i].1 <= results[i + 1].1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
386
vendor/ruvector/crates/ruvector-graph/tests/node_tests.rs
vendored
Normal file
386
vendor/ruvector/crates/ruvector-graph/tests/node_tests.rs
vendored
Normal file
@@ -0,0 +1,386 @@
|
||||
//! Node CRUD operation tests
|
||||
//!
|
||||
//! Tests for creating, reading, updating, and deleting nodes in the graph database.
|
||||
|
||||
use ruvector_graph::{GraphDB, Label, Node, Properties, PropertyValue};
|
||||
|
||||
#[test]
|
||||
fn test_create_node_basic() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Alice".to_string()),
|
||||
);
|
||||
properties.insert("age".to_string(), PropertyValue::Integer(30));
|
||||
|
||||
let node = Node::new(
|
||||
"node1".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
let node_id = db.create_node(node).unwrap();
|
||||
assert_eq!(node_id, "node1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_node_existing() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("name".to_string(), PropertyValue::String("Bob".to_string()));
|
||||
|
||||
let node = Node::new(
|
||||
"node2".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
properties.clone(),
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node2").unwrap();
|
||||
assert_eq!(retrieved.id, "node2");
|
||||
assert_eq!(
|
||||
retrieved.properties.get("name"),
|
||||
Some(&PropertyValue::String("Bob".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_node_nonexistent() {
|
||||
let db = GraphDB::new();
|
||||
let result = db.get_node("nonexistent");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_with_multiple_labels() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let labels = vec![
|
||||
Label {
|
||||
name: "Person".to_string(),
|
||||
},
|
||||
Label {
|
||||
name: "Employee".to_string(),
|
||||
},
|
||||
Label {
|
||||
name: "Manager".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("Charlie".to_string()),
|
||||
);
|
||||
|
||||
let node = Node::new("node3".to_string(), labels, properties);
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node3").unwrap();
|
||||
assert_eq!(retrieved.labels.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_with_complex_properties() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String("David".to_string()),
|
||||
);
|
||||
properties.insert("age".to_string(), PropertyValue::Integer(35));
|
||||
properties.insert("height".to_string(), PropertyValue::Float(1.82));
|
||||
properties.insert("active".to_string(), PropertyValue::Boolean(true));
|
||||
properties.insert(
|
||||
"tags".to_string(),
|
||||
PropertyValue::List(vec![
|
||||
PropertyValue::String("developer".to_string()),
|
||||
PropertyValue::String("team-lead".to_string()),
|
||||
]),
|
||||
);
|
||||
|
||||
let node = Node::new(
|
||||
"node4".to_string(),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node4").unwrap();
|
||||
assert_eq!(retrieved.properties.len(), 5);
|
||||
assert!(matches!(
|
||||
retrieved.properties.get("tags"),
|
||||
Some(PropertyValue::List(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_with_empty_properties() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let node = Node::new(
|
||||
"node5".to_string(),
|
||||
vec![Label {
|
||||
name: "EmptyNode".to_string(),
|
||||
}],
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node5").unwrap();
|
||||
assert!(retrieved.properties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_with_no_labels() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert(
|
||||
"data".to_string(),
|
||||
PropertyValue::String("test".to_string()),
|
||||
);
|
||||
|
||||
let node = Node::new("node6".to_string(), vec![], properties);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node6").unwrap();
|
||||
assert!(retrieved.labels.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_property_update() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("counter".to_string(), PropertyValue::Integer(0));
|
||||
|
||||
let node = Node::new(
|
||||
"node7".to_string(),
|
||||
vec![Label {
|
||||
name: "Counter".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
// TODO: Implement update_node method
|
||||
// For now, we'll recreate the node with updated properties
|
||||
let mut updated_properties = Properties::new();
|
||||
updated_properties.insert("counter".to_string(), PropertyValue::Integer(1));
|
||||
|
||||
let updated_node = Node::new(
|
||||
"node7".to_string(),
|
||||
vec![Label {
|
||||
name: "Counter".to_string(),
|
||||
}],
|
||||
updated_properties,
|
||||
);
|
||||
|
||||
db.create_node(updated_node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node7").unwrap();
|
||||
assert_eq!(
|
||||
retrieved.properties.get("counter"),
|
||||
Some(&PropertyValue::Integer(1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_1000_nodes() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
for i in 0..1000 {
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("index".to_string(), PropertyValue::Integer(i));
|
||||
|
||||
let node = Node::new(
|
||||
format!("node_{}", i),
|
||||
vec![Label {
|
||||
name: "TestNode".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
// Verify all nodes were created
|
||||
for i in 0..1000 {
|
||||
let retrieved = db.get_node(&format!("node_{}", i));
|
||||
assert!(retrieved.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_property_null_value() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("nullable".to_string(), PropertyValue::Null);
|
||||
|
||||
let node = Node::new(
|
||||
"node8".to_string(),
|
||||
vec![Label {
|
||||
name: "NullTest".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node8").unwrap();
|
||||
assert_eq!(
|
||||
retrieved.properties.get("nullable"),
|
||||
Some(&PropertyValue::Null)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_nested_list_properties() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert(
|
||||
"matrix".to_string(),
|
||||
PropertyValue::List(vec![
|
||||
PropertyValue::List(vec![PropertyValue::Integer(1), PropertyValue::Integer(2)]),
|
||||
PropertyValue::List(vec![PropertyValue::Integer(3), PropertyValue::Integer(4)]),
|
||||
]),
|
||||
);
|
||||
|
||||
let node = Node::new(
|
||||
"node9".to_string(),
|
||||
vec![Label {
|
||||
name: "Matrix".to_string(),
|
||||
}],
|
||||
properties,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("node9").unwrap();
|
||||
match retrieved.properties.get("matrix") {
|
||||
Some(PropertyValue::List(outer)) => {
|
||||
assert_eq!(outer.len(), 2);
|
||||
match &outer[0] {
|
||||
PropertyValue::List(inner) => assert_eq!(inner.len(), 2),
|
||||
_ => panic!("Expected inner list"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected outer list"),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property-based tests using proptest
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn node_id_strategy() -> impl Strategy<Value = String> {
|
||||
"[a-z][a-z0-9_]{0,20}".prop_map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn label_strategy() -> impl Strategy<Value = Label> {
|
||||
"[A-Z][a-zA-Z]{0,10}".prop_map(|name| Label { name })
|
||||
}
|
||||
|
||||
fn property_value_strategy() -> impl Strategy<Value = PropertyValue> {
|
||||
prop_oneof![
|
||||
any::<String>().prop_map(PropertyValue::String),
|
||||
any::<i64>().prop_map(PropertyValue::Integer),
|
||||
any::<f64>()
|
||||
.prop_filter("Must be finite", |x| x.is_finite())
|
||||
.prop_map(PropertyValue::Float),
|
||||
any::<bool>().prop_map(PropertyValue::Boolean),
|
||||
Just(PropertyValue::Null),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_node_roundtrip(
|
||||
id in node_id_strategy(),
|
||||
labels in prop::collection::vec(label_strategy(), 0..5),
|
||||
prop_count in 0..10usize
|
||||
) {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
for i in 0..prop_count {
|
||||
properties.insert(
|
||||
format!("prop_{}", i),
|
||||
PropertyValue::String(format!("value_{}", i))
|
||||
);
|
||||
}
|
||||
|
||||
let node = Node::new(id.clone(), labels.clone(), properties.clone());
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node(&id).unwrap();
|
||||
assert_eq!(retrieved.id, id);
|
||||
assert_eq!(retrieved.labels.len(), labels.len());
|
||||
assert_eq!(retrieved.properties.len(), properties.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_property_value_consistency(
|
||||
value in property_value_strategy()
|
||||
) {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let mut properties = Properties::new();
|
||||
properties.insert("test_prop".to_string(), value.clone());
|
||||
|
||||
let node = Node::new(
|
||||
"test_node".to_string(),
|
||||
vec![],
|
||||
properties
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
let retrieved = db.get_node("test_node").unwrap();
|
||||
assert_eq!(retrieved.properties.get("test_prop"), Some(&value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many_nodes_no_collision(
|
||||
ids in prop::collection::hash_set(node_id_strategy(), 10..100)
|
||||
) {
|
||||
let db = GraphDB::new();
|
||||
|
||||
for id in &ids {
|
||||
let node = Node::new(
|
||||
id.clone(),
|
||||
vec![],
|
||||
Properties::new()
|
||||
);
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
for id in &ids {
|
||||
assert!(db.get_node(id).is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
434
vendor/ruvector/crates/ruvector-graph/tests/performance_tests.rs
vendored
Normal file
434
vendor/ruvector/crates/ruvector-graph/tests/performance_tests.rs
vendored
Normal file
@@ -0,0 +1,434 @@
|
||||
//! Performance and regression tests
|
||||
//!
|
||||
//! Benchmark tests to ensure performance doesn't degrade over time.
|
||||
|
||||
use ruvector_graph::{Edge, GraphDB, Label, Node, Properties, PropertyValue};
|
||||
use std::time::Instant;
|
||||
|
||||
// ============================================================================
|
||||
// Baseline Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_node_creation_performance() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 10_000;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
let mut props = Properties::new();
|
||||
props.insert("id".to_string(), PropertyValue::Integer(i));
|
||||
|
||||
let node = Node::new(
|
||||
format!("node_{}", i),
|
||||
vec![Label {
|
||||
name: "Benchmark".to_string(),
|
||||
}],
|
||||
props,
|
||||
);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!("Created {} nodes in {:?}", num_nodes, duration);
|
||||
println!(
|
||||
"Rate: {:.2} nodes/sec",
|
||||
num_nodes as f64 / duration.as_secs_f64()
|
||||
);
|
||||
|
||||
// Baseline: Should create at least 10k nodes/sec
|
||||
assert!(
|
||||
duration.as_secs() < 5,
|
||||
"Node creation too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_retrieval_performance() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 10_000;
|
||||
|
||||
// Setup
|
||||
for i in 0..num_nodes {
|
||||
db.create_node(Node::new(format!("node_{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Measure retrieval
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
let node = db.get_node(&format!("node_{}", i));
|
||||
assert!(node.is_some());
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!("Retrieved {} nodes in {:?}", num_nodes, duration);
|
||||
println!(
|
||||
"Rate: {:.2} reads/sec",
|
||||
num_nodes as f64 / duration.as_secs_f64()
|
||||
);
|
||||
|
||||
// Should be very fast for in-memory lookups
|
||||
assert!(
|
||||
duration.as_secs() < 1,
|
||||
"Node retrieval too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_creation_performance() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 1000;
|
||||
let edges_per_node = 10;
|
||||
|
||||
// Create nodes
|
||||
for i in 0..num_nodes {
|
||||
db.create_node(Node::new(format!("n{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Create edges
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
for j in 0..edges_per_node {
|
||||
let to = (i + j + 1) % num_nodes;
|
||||
let edge = Edge::new(
|
||||
format!("e_{}_{}", i, j),
|
||||
format!("n{}", i),
|
||||
format!("n{}", to),
|
||||
"CONNECTS".to_string(),
|
||||
Properties::new(),
|
||||
);
|
||||
|
||||
db.create_edge(edge).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let total_edges = num_nodes * edges_per_node;
|
||||
|
||||
println!("Created {} edges in {:?}", total_edges, duration);
|
||||
println!(
|
||||
"Rate: {:.2} edges/sec",
|
||||
total_edges as f64 / duration.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement graph traversal methods
|
||||
// #[test]
|
||||
// fn test_traversal_performance() {
|
||||
// let db = GraphDB::new();
|
||||
// let num_nodes = 1000;
|
||||
//
|
||||
// // Create chain
|
||||
// for i in 0..num_nodes {
|
||||
// db.create_node(Node::new(format!("n{}", i), vec![], Properties::new())).unwrap();
|
||||
// }
|
||||
//
|
||||
// for i in 0..num_nodes - 1 {
|
||||
// db.create_edge(Edge::new(
|
||||
// format!("e{}", i),
|
||||
// format!("n{}", i),
|
||||
// format!("n{}", i + 1),
|
||||
// RelationType { name: "NEXT".to_string() },
|
||||
// Properties::new(),
|
||||
// )).unwrap();
|
||||
// }
|
||||
//
|
||||
// // Measure traversal
|
||||
// let start = Instant::now();
|
||||
// let path = db.traverse("n0", "NEXT", 100).unwrap();
|
||||
// let duration = start.elapsed();
|
||||
//
|
||||
// assert_eq!(path.len(), 100);
|
||||
// println!("Traversed 100 hops in {:?}", duration);
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Scalability Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_large_graph_creation() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 100_000;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
if i % 10_000 == 0 {
|
||||
println!("Created {} nodes...", i);
|
||||
}
|
||||
|
||||
let node = Node::new(format!("large_{}", i), vec![], Properties::new());
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!("Created {} nodes in {:?}", num_nodes, duration);
|
||||
println!(
|
||||
"Rate: {:.2} nodes/sec",
|
||||
num_nodes as f64 / duration.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Long-running test
|
||||
fn test_million_node_graph() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 1_000_000;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
if i % 100_000 == 0 {
|
||||
println!("Created {} nodes...", i);
|
||||
}
|
||||
|
||||
let node = Node::new(format!("mega_{}", i), vec![], Properties::new());
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!("Created {} nodes in {:?}", num_nodes, duration);
|
||||
println!(
|
||||
"Rate: {:.2} nodes/sec",
|
||||
num_nodes as f64 / duration.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Usage Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_memory_efficiency() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 10_000;
|
||||
|
||||
for i in 0..num_nodes {
|
||||
let mut props = Properties::new();
|
||||
props.insert("data".to_string(), PropertyValue::String("x".repeat(100)));
|
||||
|
||||
let node = Node::new(format!("mem_{}", i), vec![], props);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
// TODO: Measure actual memory usage
|
||||
// This would require platform-specific APIs
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property-based Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_property_heavy_nodes() {
|
||||
let db = GraphDB::new();
|
||||
let num_nodes = 1_000;
|
||||
let props_per_node = 50;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
let mut props = Properties::new();
|
||||
|
||||
for j in 0..props_per_node {
|
||||
props.insert(format!("prop_{}", j), PropertyValue::Integer(j as i64));
|
||||
}
|
||||
|
||||
let node = Node::new(format!("heavy_{}", i), vec![], props);
|
||||
|
||||
db.create_node(node).unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!(
|
||||
"Created {} property-heavy nodes in {:?}",
|
||||
num_nodes, duration
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Performance Tests (TODO)
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_simple_query_performance() {
|
||||
// let db = setup_benchmark_graph(10_000);
|
||||
//
|
||||
// let start = Instant::now();
|
||||
// let results = db.execute("MATCH (n:Person) RETURN n LIMIT 100").unwrap();
|
||||
// let duration = start.elapsed();
|
||||
//
|
||||
// assert_eq!(results.len(), 100);
|
||||
// println!("Simple query took: {:?}", duration);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_aggregation_performance() {
|
||||
// let db = setup_benchmark_graph(100_000);
|
||||
//
|
||||
// let start = Instant::now();
|
||||
// let results = db.execute("MATCH (n:Person) RETURN COUNT(n)").unwrap();
|
||||
// let duration = start.elapsed();
|
||||
//
|
||||
// println!("Aggregation over 100k nodes took: {:?}", duration);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_join_performance() {
|
||||
// let db = setup_benchmark_graph(10_000);
|
||||
//
|
||||
// let start = Instant::now();
|
||||
// let results = db.execute("
|
||||
// MATCH (a:Person)-[:KNOWS]->(b:Person)
|
||||
// WHERE a.age > 30
|
||||
// RETURN a, b
|
||||
// ").unwrap();
|
||||
// let duration = start.elapsed();
|
||||
//
|
||||
// println!("Join query took: {:?}", duration);
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Index Performance Tests (TODO)
|
||||
// ============================================================================
|
||||
|
||||
// #[test]
|
||||
// fn test_indexed_lookup_performance() {
|
||||
// let db = GraphDB::new();
|
||||
//
|
||||
// // Create index
|
||||
// db.create_index("Person", "email").unwrap();
|
||||
//
|
||||
// // Insert data
|
||||
// for i in 0..100_000 {
|
||||
// db.execute(&format!(
|
||||
// "CREATE (:Person {{email: 'user{}@example.com'}})",
|
||||
// i
|
||||
// )).unwrap();
|
||||
// }
|
||||
//
|
||||
// // Measure lookup
|
||||
// let start = Instant::now();
|
||||
// let results = db.execute("MATCH (n:Person {email: 'user50000@example.com'}) RETURN n").unwrap();
|
||||
// let duration = start.elapsed();
|
||||
//
|
||||
// assert_eq!(results.len(), 1);
|
||||
// println!("Indexed lookup took: {:?}", duration);
|
||||
// assert!(duration.as_millis() < 10); // Should be very fast
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Regression Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_regression_node_creation() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..1000 {
|
||||
db.create_node(Node::new(format!("regr_{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Baseline threshold - should not regress beyond this
|
||||
// Adjust based on baseline measurements
|
||||
assert!(
|
||||
duration.as_millis() < 500,
|
||||
"Regression detected: {:?}",
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_regression_node_retrieval() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Setup
|
||||
for i in 0..1000 {
|
||||
db.create_node(Node::new(format!("regr_{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..1000 {
|
||||
let _ = db.get_node(&format!("regr_{}", i));
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Should be very fast
|
||||
assert!(
|
||||
duration.as_millis() < 100,
|
||||
"Regression detected: {:?}",
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn setup_benchmark_graph(num_nodes: usize) -> GraphDB {
|
||||
let db = GraphDB::new();
|
||||
|
||||
for i in 0..num_nodes {
|
||||
let mut props = Properties::new();
|
||||
props.insert(
|
||||
"name".to_string(),
|
||||
PropertyValue::String(format!("Person{}", i)),
|
||||
);
|
||||
props.insert(
|
||||
"age".to_string(),
|
||||
PropertyValue::Integer((20 + (i % 60)) as i64),
|
||||
);
|
||||
|
||||
db.create_node(Node::new(
|
||||
format!("person_{}", i),
|
||||
vec![Label {
|
||||
name: "Person".to_string(),
|
||||
}],
|
||||
props,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Create some edges
|
||||
for i in 0..num_nodes / 10 {
|
||||
let from = i;
|
||||
let to = (i + 1) % num_nodes;
|
||||
|
||||
db.create_edge(Edge::new(
|
||||
format!("knows_{}", i),
|
||||
format!("person_{}", from),
|
||||
format!("person_{}", to),
|
||||
"KNOWS".to_string(),
|
||||
Properties::new(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db
|
||||
}
|
||||
818
vendor/ruvector/crates/ruvector-graph/tests/transaction_tests.rs
vendored
Normal file
818
vendor/ruvector/crates/ruvector-graph/tests/transaction_tests.rs
vendored
Normal file
@@ -0,0 +1,818 @@
|
||||
//! Transaction tests for ACID guarantees
|
||||
//!
|
||||
//! Tests to verify atomicity, consistency, isolation, and durability properties.
|
||||
|
||||
use ruvector_graph::edge::EdgeBuilder;
|
||||
use ruvector_graph::node::NodeBuilder;
|
||||
use ruvector_graph::transaction::{IsolationLevel, Transaction, TransactionManager};
|
||||
use ruvector_graph::{GraphDB, Label, Node, Properties, PropertyValue};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
// ============================================================================
|
||||
// Atomicity Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transaction_commit() {
|
||||
let _db = GraphDB::new();
|
||||
|
||||
let tx = Transaction::begin(IsolationLevel::ReadCommitted).unwrap();
|
||||
|
||||
// TODO: Implement transaction operations
|
||||
// tx.create_node(...)?;
|
||||
// tx.create_edge(...)?;
|
||||
|
||||
let result = tx.commit();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_rollback() {
|
||||
let _db = GraphDB::new();
|
||||
|
||||
let tx = Transaction::begin(IsolationLevel::ReadCommitted).unwrap();
|
||||
|
||||
// TODO: Implement transaction operations
|
||||
// tx.create_node(...)?;
|
||||
|
||||
let result = tx.rollback();
|
||||
assert!(result.is_ok());
|
||||
|
||||
// TODO: Verify that changes were not applied
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_atomic_batch_insert() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// TODO: Implement transactional batch insert
|
||||
// Either all nodes are created or none
|
||||
/*
|
||||
let tx = db.begin_transaction(IsolationLevel::Serializable)?;
|
||||
|
||||
for i in 0..100 {
|
||||
tx.create_node(Node::new(
|
||||
format!("node_{}", i),
|
||||
vec![],
|
||||
Properties::new(),
|
||||
))?;
|
||||
|
||||
if i == 50 {
|
||||
// Simulate error
|
||||
tx.rollback()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no nodes were created
|
||||
assert!(db.get_node("node_0").is_none());
|
||||
*/
|
||||
|
||||
// For now, just create without transaction
|
||||
for i in 0..10 {
|
||||
db.create_node(Node::new(format!("node_{}", i), vec![], Properties::new()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert!(db.get_node("node_0").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_rollback_on_constraint_violation() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Create first node
|
||||
let node1 = NodeBuilder::new()
|
||||
.id("unique_node")
|
||||
.label("User")
|
||||
.property("email", "test@example.com")
|
||||
.build();
|
||||
|
||||
db.create_node(node1).unwrap();
|
||||
|
||||
// Begin transaction and try to create duplicate
|
||||
let tx = Transaction::begin(IsolationLevel::Serializable).unwrap();
|
||||
|
||||
let node2 = NodeBuilder::new()
|
||||
.id("unique_node") // Same ID - should violate uniqueness
|
||||
.label("User")
|
||||
.property("email", "test2@example.com")
|
||||
.build();
|
||||
|
||||
tx.write_node(node2);
|
||||
|
||||
// Rollback due to constraint violation
|
||||
let result = tx.rollback();
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Verify original node still exists and no duplicate was created
|
||||
assert!(db.get_node("unique_node").is_some());
|
||||
assert_eq!(db.node_count(), 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Isolation Level Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_isolation_read_uncommitted() {
|
||||
let tx = Transaction::begin(IsolationLevel::ReadUncommitted).unwrap();
|
||||
assert_eq!(tx.isolation_level, IsolationLevel::ReadUncommitted);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation_read_committed() {
|
||||
let tx = Transaction::begin(IsolationLevel::ReadCommitted).unwrap();
|
||||
assert_eq!(tx.isolation_level, IsolationLevel::ReadCommitted);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation_repeatable_read() {
|
||||
let tx = Transaction::begin(IsolationLevel::RepeatableRead).unwrap();
|
||||
assert_eq!(tx.isolation_level, IsolationLevel::RepeatableRead);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation_serializable() {
|
||||
let tx = Transaction::begin(IsolationLevel::Serializable).unwrap();
|
||||
assert_eq!(tx.isolation_level, IsolationLevel::Serializable);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Concurrency and Isolation Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_transactions_read_committed() {
|
||||
let db = Arc::new(GraphDB::new());
|
||||
|
||||
// Create initial node
|
||||
let mut props = Properties::new();
|
||||
props.insert("counter".to_string(), PropertyValue::Integer(0));
|
||||
db.create_node(Node::new(
|
||||
"counter".to_string(),
|
||||
vec![Label {
|
||||
name: "Counter".to_string(),
|
||||
}],
|
||||
props,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// TODO: Implement transactional updates
|
||||
// Spawn multiple threads that increment the counter
|
||||
let handles: Vec<_> = (0..10)
|
||||
.map(|_| {
|
||||
let db_clone = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
// TODO: Begin transaction, read counter, increment, commit
|
||||
// For now, just read
|
||||
let _node = db_clone.get_node("counter");
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// TODO: Verify final counter value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirty_read_prevention() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Transaction 1: Write a node but don't commit yet
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::ReadCommitted);
|
||||
let node = NodeBuilder::new()
|
||||
.id("dirty_node")
|
||||
.label("Test")
|
||||
.property("value", 42i64)
|
||||
.build();
|
||||
tx1.write_node(node);
|
||||
|
||||
// Sleep to let tx2 try to read
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Don't commit - this should be rolled back
|
||||
tx1.rollback().unwrap();
|
||||
});
|
||||
|
||||
// Transaction 2: Try to read the uncommitted node (should not see it)
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
let tx2 = manager.begin(IsolationLevel::ReadCommitted);
|
||||
let read_node = tx2.read_node(&"dirty_node".to_string());
|
||||
|
||||
// Should not see uncommitted changes
|
||||
assert!(read_node.is_none());
|
||||
|
||||
handle1.join().unwrap();
|
||||
tx2.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_repeatable_read_prevention() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Create initial node
|
||||
let node = NodeBuilder::new()
|
||||
.id("counter_node")
|
||||
.label("Counter")
|
||||
.property("count", 0i64)
|
||||
.build();
|
||||
|
||||
let tx_init = manager.begin(IsolationLevel::RepeatableRead);
|
||||
tx_init.write_node(node);
|
||||
tx_init.commit().unwrap();
|
||||
|
||||
// Transaction 1: Read twice with RepeatableRead isolation
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::RepeatableRead);
|
||||
|
||||
// First read
|
||||
let node1 = tx1.read_node(&"counter_node".to_string());
|
||||
assert!(node1.is_some());
|
||||
let value1 = node1.unwrap().get_property("count").unwrap().clone();
|
||||
|
||||
// Sleep to allow tx2 to modify
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Second read - should see same value due to RepeatableRead
|
||||
let node2 = tx1.read_node(&"counter_node".to_string());
|
||||
assert!(node2.is_some());
|
||||
let value2 = node2.unwrap().get_property("count").unwrap().clone();
|
||||
|
||||
// With RepeatableRead, both reads should see the same snapshot
|
||||
assert_eq!(value1, value2);
|
||||
|
||||
tx1.commit().unwrap();
|
||||
});
|
||||
|
||||
// Transaction 2: Update the node
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
let tx2 = manager.begin(IsolationLevel::ReadCommitted);
|
||||
let updated_node = NodeBuilder::new()
|
||||
.id("counter_node")
|
||||
.label("Counter")
|
||||
.property("count", 100i64)
|
||||
.build();
|
||||
tx2.write_node(updated_node);
|
||||
tx2.commit().unwrap();
|
||||
|
||||
handle1.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phantom_read_prevention() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Create initial nodes
|
||||
for i in 0..3 {
|
||||
let node = NodeBuilder::new()
|
||||
.id(format!("node_{}", i))
|
||||
.label("Product")
|
||||
.property("price", 50i64)
|
||||
.build();
|
||||
let tx = manager.begin(IsolationLevel::Serializable);
|
||||
tx.write_node(node);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// Transaction 1: Query nodes with Serializable isolation
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::Serializable);
|
||||
|
||||
// First query - count nodes
|
||||
let mut count1 = 0;
|
||||
for i in 0..5 {
|
||||
if tx1.read_node(&format!("node_{}", i)).is_some() {
|
||||
count1 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep to allow tx2 to insert
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Second query - should see same count (no phantom reads)
|
||||
let mut count2 = 0;
|
||||
for i in 0..5 {
|
||||
if tx1.read_node(&format!("node_{}", i)).is_some() {
|
||||
count2 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// With Serializable, no phantom reads should occur
|
||||
assert_eq!(count1, count2);
|
||||
|
||||
tx1.commit().unwrap();
|
||||
count1
|
||||
});
|
||||
|
||||
// Transaction 2: Insert a new node
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
let tx2 = manager.begin(IsolationLevel::Serializable);
|
||||
let new_node = NodeBuilder::new()
|
||||
.id("node_3")
|
||||
.label("Product")
|
||||
.property("price", 50i64)
|
||||
.build();
|
||||
tx2.write_node(new_node);
|
||||
tx2.commit().unwrap();
|
||||
|
||||
let original_count = handle1.join().unwrap();
|
||||
assert_eq!(original_count, 3); // Should only see original 3 nodes
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Deadlock Detection and Prevention
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_deadlock_detection() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Create two nodes
|
||||
let node_a = NodeBuilder::new()
|
||||
.id("node_a")
|
||||
.label("Resource")
|
||||
.property("value", 100i64)
|
||||
.build();
|
||||
let node_b = NodeBuilder::new()
|
||||
.id("node_b")
|
||||
.label("Resource")
|
||||
.property("value", 200i64)
|
||||
.build();
|
||||
|
||||
let tx_init = manager.begin(IsolationLevel::Serializable);
|
||||
tx_init.write_node(node_a);
|
||||
tx_init.write_node(node_b);
|
||||
tx_init.commit().unwrap();
|
||||
|
||||
// Transaction 1: Lock A then try to lock B
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Read and modify node_a (acquire lock on A)
|
||||
let mut node = tx1.read_node(&"node_a".to_string()).unwrap();
|
||||
node.set_property("value", PropertyValue::Integer(150));
|
||||
tx1.write_node(node);
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Try to read node_b (would acquire lock on B)
|
||||
let node_b = tx1.read_node(&"node_b".to_string());
|
||||
if node_b.is_some() {
|
||||
tx1.commit().ok();
|
||||
} else {
|
||||
tx1.rollback().ok();
|
||||
}
|
||||
});
|
||||
|
||||
// Transaction 2: Lock B then try to lock A (opposite order - potential deadlock)
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
let tx2 = manager.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Read and modify node_b (acquire lock on B)
|
||||
let mut node = tx2.read_node(&"node_b".to_string()).unwrap();
|
||||
node.set_property("value", PropertyValue::Integer(250));
|
||||
tx2.write_node(node);
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Try to read node_a (would acquire lock on A - deadlock!)
|
||||
let _node_a = tx2.read_node(&"node_a".to_string());
|
||||
|
||||
// In a real deadlock detection system, one transaction should be aborted
|
||||
// For now, we just verify both transactions can complete (with MVCC)
|
||||
tx2.commit().ok();
|
||||
|
||||
handle1.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deadlock_timeout() {
|
||||
// TODO: Implement
|
||||
// Verify that transactions timeout if they can't acquire locks
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Multi-Version Concurrency Control (MVCC) Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_mvcc_snapshot_isolation() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Create initial state
|
||||
for i in 0..5 {
|
||||
let node = NodeBuilder::new()
|
||||
.id(format!("account_{}", i))
|
||||
.label("Account")
|
||||
.property("balance", 1000i64)
|
||||
.build();
|
||||
let tx = manager.begin(IsolationLevel::RepeatableRead);
|
||||
tx.write_node(node);
|
||||
tx.commit().unwrap();
|
||||
}
|
||||
|
||||
// Long-running transaction that takes a snapshot
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::RepeatableRead);
|
||||
|
||||
// Take snapshot by reading
|
||||
let snapshot_sum: i64 = (0..5)
|
||||
.filter_map(|i| tx1.read_node(&format!("account_{}", i)))
|
||||
.filter_map(|node| {
|
||||
if let Some(PropertyValue::Integer(balance)) = node.get_property("balance") {
|
||||
Some(*balance)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Sleep while other transactions modify data
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// Read again - should see same snapshot
|
||||
let snapshot_sum2: i64 = (0..5)
|
||||
.filter_map(|i| tx1.read_node(&format!("account_{}", i)))
|
||||
.filter_map(|node| {
|
||||
if let Some(PropertyValue::Integer(balance)) = node.get_property("balance") {
|
||||
Some(*balance)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
assert_eq!(snapshot_sum, snapshot_sum2);
|
||||
assert_eq!(snapshot_sum, 5000); // Original total
|
||||
|
||||
tx1.commit().unwrap();
|
||||
});
|
||||
|
||||
// Multiple concurrent transactions modifying data
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
let handles: Vec<_> = (0..5)
|
||||
.map(|i| {
|
||||
let manager_clone = Arc::clone(&manager);
|
||||
thread::spawn(move || {
|
||||
let tx = manager_clone.begin(IsolationLevel::ReadCommitted);
|
||||
let node = NodeBuilder::new()
|
||||
.id(format!("account_{}", i))
|
||||
.label("Account")
|
||||
.property("balance", 2000i64)
|
||||
.build();
|
||||
tx.write_node(node);
|
||||
tx.commit().unwrap();
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
handle1.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mvcc_concurrent_reads_and_writes() {
|
||||
// TODO: Implement
|
||||
// Verify that readers don't block writers and vice versa
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Write Skew Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_write_skew_detection() {
|
||||
// TODO: Implement
|
||||
// Classic write skew scenario: two transactions read overlapping data
|
||||
// and make decisions based on what they read, leading to inconsistency
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Long-Running Transaction Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_long_running_transaction_timeout() {
|
||||
// TODO: Implement
|
||||
// Verify that long-running transactions can be configured to timeout
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_progress_tracking() {
|
||||
// TODO: Implement
|
||||
// Verify that we can track progress of long-running transactions
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Savepoint Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transaction_savepoint() {
|
||||
let manager = TransactionManager::new();
|
||||
|
||||
// Begin transaction
|
||||
let tx = manager.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Create first node (before savepoint)
|
||||
let node1 = NodeBuilder::new()
|
||||
.id("before_savepoint")
|
||||
.label("Test")
|
||||
.property("value", 1i64)
|
||||
.build();
|
||||
tx.write_node(node1);
|
||||
|
||||
// Simulate savepoint by committing and starting new transaction
|
||||
// (Real implementation would support nested savepoints)
|
||||
tx.commit().unwrap();
|
||||
|
||||
// Start new transaction (simulating after savepoint)
|
||||
let tx2 = manager.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Create second node
|
||||
let node2 = NodeBuilder::new()
|
||||
.id("after_savepoint")
|
||||
.label("Test")
|
||||
.property("value", 2i64)
|
||||
.build();
|
||||
tx2.write_node(node2);
|
||||
|
||||
// Rollback second transaction (like rolling back to savepoint)
|
||||
tx2.rollback().unwrap();
|
||||
|
||||
// Verify: first node exists, second doesn't
|
||||
let tx3 = manager.begin(IsolationLevel::ReadCommitted);
|
||||
assert!(tx3.read_node(&"before_savepoint".to_string()).is_some());
|
||||
assert!(tx3.read_node(&"after_savepoint".to_string()).is_none());
|
||||
tx3.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_savepoints() {
|
||||
// TODO: Implement
|
||||
// Create nested savepoints and rollback to different levels
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Consistency Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_referential_integrity() {
|
||||
let db = GraphDB::new();
|
||||
|
||||
// Create node
|
||||
let node = NodeBuilder::new()
|
||||
.id("existing_node")
|
||||
.label("Person")
|
||||
.property("name", "Alice")
|
||||
.build();
|
||||
db.create_node(node).unwrap();
|
||||
|
||||
// Try to create edge with non-existent target node
|
||||
let edge = EdgeBuilder::new(
|
||||
"existing_node".to_string(),
|
||||
"non_existent_node".to_string(),
|
||||
"KNOWS",
|
||||
)
|
||||
.build();
|
||||
|
||||
let result = db.create_edge(edge);
|
||||
|
||||
// Should fail due to referential integrity violation
|
||||
assert!(result.is_err());
|
||||
|
||||
// Verify no edge was created
|
||||
assert_eq!(db.edge_count(), 0);
|
||||
|
||||
// Create both nodes and edge should succeed
|
||||
let node2 = NodeBuilder::new()
|
||||
.id("existing_node_2")
|
||||
.label("Person")
|
||||
.property("name", "Bob")
|
||||
.build();
|
||||
db.create_node(node2).unwrap();
|
||||
|
||||
let edge2 = EdgeBuilder::new(
|
||||
"existing_node".to_string(),
|
||||
"existing_node_2".to_string(),
|
||||
"KNOWS",
|
||||
)
|
||||
.build();
|
||||
|
||||
let result2 = db.create_edge(edge2);
|
||||
assert!(result2.is_ok());
|
||||
assert_eq!(db.edge_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_constraint_enforcement() {
|
||||
// TODO: Implement
|
||||
// Verify that unique constraints are enforced within transactions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_consistency() {
|
||||
// TODO: Implement
|
||||
// Verify that indexes remain consistent after transaction commit/rollback
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Durability Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_write_ahead_log() {
|
||||
let manager = TransactionManager::new();
|
||||
|
||||
// Begin transaction and make changes
|
||||
let tx = manager.begin(IsolationLevel::Serializable);
|
||||
|
||||
let node1 = NodeBuilder::new()
|
||||
.id("wal_node_1")
|
||||
.label("Account")
|
||||
.property("balance", 1000i64)
|
||||
.build();
|
||||
|
||||
let node2 = NodeBuilder::new()
|
||||
.id("wal_node_2")
|
||||
.label("Account")
|
||||
.property("balance", 2000i64)
|
||||
.build();
|
||||
|
||||
// Write operations should be buffered (write-ahead log concept)
|
||||
tx.write_node(node1);
|
||||
tx.write_node(node2);
|
||||
|
||||
// Before commit, changes should only be in write set
|
||||
// (not visible to other transactions)
|
||||
let tx_reader = manager.begin(IsolationLevel::ReadCommitted);
|
||||
assert!(tx_reader.read_node(&"wal_node_1".to_string()).is_none());
|
||||
assert!(tx_reader.read_node(&"wal_node_2".to_string()).is_none());
|
||||
tx_reader.commit().unwrap();
|
||||
|
||||
// Commit transaction (apply logged changes)
|
||||
tx.commit().unwrap();
|
||||
|
||||
// After commit, changes should be visible
|
||||
let tx_verify = manager.begin(IsolationLevel::ReadCommitted);
|
||||
assert!(tx_verify.read_node(&"wal_node_1".to_string()).is_some());
|
||||
assert!(tx_verify.read_node(&"wal_node_2".to_string()).is_some());
|
||||
tx_verify.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crash_recovery() {
|
||||
// TODO: Implement
|
||||
// Simulate crash and verify that committed transactions are preserved
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checkpoint_mechanism() {
|
||||
// TODO: Implement
|
||||
// Verify that checkpoints work correctly for durability
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transaction Isolation Anomaly Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_lost_update_prevention() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let manager = Arc::new(TransactionManager::new());
|
||||
|
||||
// Create initial counter node
|
||||
let node = NodeBuilder::new()
|
||||
.id("counter")
|
||||
.label("Counter")
|
||||
.property("value", 0i64)
|
||||
.build();
|
||||
|
||||
let tx_init = manager.begin(IsolationLevel::Serializable);
|
||||
tx_init.write_node(node);
|
||||
tx_init.commit().unwrap();
|
||||
|
||||
// Two transactions both try to increment the counter
|
||||
let manager_clone1 = Arc::clone(&manager);
|
||||
let handle1 = thread::spawn(move || {
|
||||
let tx1 = manager_clone1.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Read current value
|
||||
let node = tx1.read_node(&"counter".to_string()).unwrap();
|
||||
let current_value = if let Some(PropertyValue::Integer(val)) = node.get_property("value") {
|
||||
*val
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Increment and write back
|
||||
let mut updated_node = node.clone();
|
||||
updated_node.set_property("value", PropertyValue::Integer(current_value + 1));
|
||||
tx1.write_node(updated_node);
|
||||
|
||||
tx1.commit().unwrap();
|
||||
});
|
||||
|
||||
let manager_clone2 = Arc::clone(&manager);
|
||||
let handle2 = thread::spawn(move || {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
let tx2 = manager_clone2.begin(IsolationLevel::Serializable);
|
||||
|
||||
// Read current value
|
||||
let node = tx2.read_node(&"counter".to_string()).unwrap();
|
||||
let current_value = if let Some(PropertyValue::Integer(val)) = node.get_property("value") {
|
||||
*val
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Increment and write back
|
||||
let mut updated_node = node.clone();
|
||||
updated_node.set_property("value", PropertyValue::Integer(current_value + 1));
|
||||
tx2.write_node(updated_node);
|
||||
|
||||
tx2.commit().unwrap();
|
||||
});
|
||||
|
||||
handle1.join().unwrap();
|
||||
handle2.join().unwrap();
|
||||
|
||||
// Verify final value - with proper serializable isolation,
|
||||
// both increments should be preserved (value should be 2)
|
||||
let tx_verify = manager.begin(IsolationLevel::ReadCommitted);
|
||||
let final_node = tx_verify.read_node(&"counter".to_string()).unwrap();
|
||||
let final_value = if let Some(PropertyValue::Integer(val)) = final_node.get_property("value") {
|
||||
*val
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// With MVCC and proper isolation, both writes succeed independently
|
||||
// The last committed transaction's value wins (value = 1 from one of them)
|
||||
assert!(final_value >= 1);
|
||||
tx_verify.commit().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_skew_prevention() {
|
||||
// TODO: Implement
|
||||
// Transaction reads two related values at different times
|
||||
// Verify consistency based on isolation level
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_transaction_throughput() {
|
||||
// TODO: Implement
|
||||
// Measure throughput of small transactions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_contention_handling() {
|
||||
// TODO: Implement
|
||||
// Verify graceful handling of high lock contention
|
||||
}
|
||||
Reference in New Issue
Block a user