use std::path::PathBuf;
use tempfile::TempDir;
use velesdb_core::collection::graph::GraphSchema;
use velesdb_core::{Database, DistanceMetric, GraphCollection, GraphEdge};
use crate::graph::GraphAction;
fn setup_graph_db() -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("test: create temp dir");
let db_path = dir.path().join("test_db");
let db = Database::open(&db_path).expect("test: open database");
db.create_graph_collection("kg", GraphSchema::schemaless())
.expect("test: create graph collection");
drop(db);
(dir, db_path)
}
fn setup_graph_db_with_embeddings() -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("test: create temp dir");
let db_path = dir.path().join("test_db");
let db = Database::open(&db_path).expect("test: open database");
db.create_graph_collection_with_embeddings(
"kg",
GraphSchema::schemaless(),
4,
DistanceMetric::Cosine,
)
.expect("test: create graph collection with embeddings");
drop(db);
(dir, db_path)
}
fn open_graph(path: &PathBuf) -> GraphCollection {
let db = Database::open(path).expect("test: open database");
db.get_graph_collection("kg")
.expect("test: get graph collection")
}
fn populate_edges(path: &PathBuf) {
let col = open_graph(path);
for id in [1, 2, 3, 4, 5] {
col.upsert_node_payload(id, &serde_json::json!({}))
.expect("test: store node payload");
}
for (id, src, tgt, lbl) in [
(100, 1, 2, "KNOWS"),
(101, 2, 3, "KNOWS"),
(102, 3, 4, "KNOWS"),
(103, 2, 5, "WROTE"),
] {
col.add_edge(GraphEdge::new(id, src, tgt, lbl).expect("valid edge"))
.expect("test: add edge");
}
col.flush().expect("test: flush");
}
#[test]
fn test_remove_edge_existing_edge_removes_it() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
assert_eq!(open_graph(&path).edge_count(), 4);
crate::graph::handle(GraphAction::RemoveEdge {
path: path.clone(),
collection: "kg".to_string(),
edge_id: 100,
})
.expect("remove-edge should succeed");
assert_eq!(open_graph(&path).edge_count(), 3);
}
#[test]
fn test_remove_edge_nonexistent_edge_succeeds_silently() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
let result = crate::graph::handle(GraphAction::RemoveEdge {
path: path.clone(),
collection: "kg".to_string(),
edge_id: 999,
});
assert!(
result.is_ok(),
"removing non-existent edge should not error"
);
assert_eq!(open_graph(&path).edge_count(), 4);
}
#[test]
fn test_remove_edge_then_readd_same_id() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
crate::graph::handle(GraphAction::RemoveEdge {
path: path.clone(),
collection: "kg".to_string(),
edge_id: 100,
})
.expect("remove should succeed");
let col = open_graph(&path);
col.upsert_node_payload(10, &serde_json::json!({}))
.expect("test: store node 10");
col.upsert_node_payload(20, &serde_json::json!({}))
.expect("test: store node 20");
col.flush().expect("test: flush");
drop(col);
crate::graph::handle(GraphAction::AddEdge {
path: path.clone(),
collection: "kg".to_string(),
id: 100,
source: 10,
target: 20,
label: "NEW_LABEL".to_string(),
})
.expect("re-add should succeed");
let col = open_graph(&path);
let edges = col.get_edges(Some("NEW_LABEL"));
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].source(), 10);
}
#[test]
fn test_remove_all_edges_leaves_empty_graph() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
for id in [100, 101, 102, 103] {
crate::graph::handle(GraphAction::RemoveEdge {
path: path.clone(),
collection: "kg".to_string(),
edge_id: id,
})
.expect("remove should succeed");
}
assert_eq!(open_graph(&path).edge_count(), 0);
}
#[test]
fn test_remove_edge_nonexistent_collection_fails() {
let (_dir, path) = setup_graph_db();
let result = crate::graph::handle(GraphAction::RemoveEdge {
path: path.clone(),
collection: "ghost".to_string(),
edge_id: 1,
});
assert!(result.is_err());
}
#[test]
fn test_count_populated_graph_shows_correct_counts() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
crate::graph::handle(GraphAction::Count {
path: path.clone(),
collection: "kg".to_string(),
format: "table".to_string(),
})
.expect("count (table) should succeed");
crate::graph::handle(GraphAction::Count {
path: path.clone(),
collection: "kg".to_string(),
format: "json".to_string(),
})
.expect("count (json) should succeed");
assert_eq!(open_graph(&path).edge_count(), 4);
}
#[test]
fn test_count_empty_graph_shows_zero() {
let (_dir, path) = setup_graph_db();
let col = open_graph(&path);
assert_eq!(col.edge_count(), 0);
assert_eq!(col.all_node_ids().len(), 0);
}
#[test]
fn test_count_nonexistent_collection_fails() {
let (_dir, path) = setup_graph_db();
let result = crate::graph::handle(GraphAction::Count {
path: path.clone(),
collection: "ghost".to_string(),
format: "table".to_string(),
});
assert!(result.is_err());
}
#[test]
fn test_search_graph_with_embeddings_returns_results() {
let (_dir, path) = setup_graph_db_with_embeddings();
let db = Database::open(&path).expect("test: open db");
let col = velesdb_core::VectorCollection::open(db.data_dir().join("kg"))
.expect("test: open collection");
use velesdb_core::Point;
col.upsert(vec![
Point::new(1, vec![1.0, 0.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0, 0.0, 0.0], None),
Point::new(3, vec![0.9, 0.1, 0.0, 0.0], None),
])
.expect("test: upsert points");
drop(col);
drop(db);
let col = open_graph(&path);
let results = col
.search_by_embedding(&[1.0, 0.0, 0.0, 0.0], 2)
.expect("search should succeed");
assert_eq!(results.len(), 2);
assert_eq!(results[0].point.id, 1, "closest match should be id=1");
}
#[test]
fn test_search_graph_empty_collection_returns_empty() {
let (_dir, path) = setup_graph_db_with_embeddings();
let col = open_graph(&path);
let results = col
.search_by_embedding(&[1.0, 0.0, 0.0, 0.0], 10)
.expect("search on empty should succeed");
assert!(results.is_empty());
}
#[test]
fn test_search_graph_top_k_larger_than_collection() {
let (_dir, path) = setup_graph_db_with_embeddings();
let db = Database::open(&path).expect("test: open db");
let col = velesdb_core::VectorCollection::open(db.data_dir().join("kg"))
.expect("test: open collection");
use velesdb_core::Point;
col.upsert(vec![
Point::new(1, vec![1.0, 0.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0, 0.0, 0.0], None),
])
.expect("test: upsert");
drop(col);
drop(db);
let col = open_graph(&path);
let results = col
.search_by_embedding(&[1.0, 0.0, 0.0, 0.0], 100)
.expect("search should succeed");
assert_eq!(results.len(), 2);
}
#[test]
fn test_search_graph_without_embeddings_fails() {
let (_dir, path) = setup_graph_db();
let col = open_graph(&path);
let result = col.search_by_embedding(&[1.0, 0.0, 0.0, 0.0], 10);
assert!(result.is_err());
}
#[test]
fn test_traverse_bfs_parallel_multiple_sources_deduplicates() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
let col = open_graph(&path);
let config = velesdb_core::collection::graph::TraversalConfig::with_range(1, 3).with_limit(100);
let results = col.traverse_bfs_parallel(&[1, 3], &config);
let ids: Vec<u64> = results.iter().map(|r| r.target_id).collect();
assert!(ids.contains(&2), "node 2 reachable from source 1");
assert!(ids.contains(&4), "node 4 reachable from source 3");
}
#[test]
fn test_traverse_bfs_parallel_empty_sources_returns_empty() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
let col = open_graph(&path);
let config = velesdb_core::collection::graph::TraversalConfig::with_range(1, 3).with_limit(100);
let results = col.traverse_bfs_parallel(&[], &config);
assert!(results.is_empty());
}
#[test]
fn test_traverse_bfs_parallel_single_source_same_as_regular() {
let (_dir, path) = setup_graph_db();
populate_edges(&path);
let col = open_graph(&path);
let config = velesdb_core::collection::graph::TraversalConfig::with_range(1, 3).with_limit(100);
let parallel = col.traverse_bfs_parallel(&[1], &config);
let regular = col.traverse_bfs(1, &config);
let par_ids: std::collections::HashSet<u64> = parallel.iter().map(|r| r.target_id).collect();
let reg_ids: std::collections::HashSet<u64> = regular.iter().map(|r| r.target_id).collect();
assert_eq!(par_ids, reg_ids);
}
#[test]
fn test_store_payload_and_get_payload_roundtrip() {
let (_dir, path) = setup_graph_db();
crate::graph::handle(GraphAction::StorePayload {
path: path.clone(),
collection: "kg".to_string(),
node_id: 42,
payload: r#"{"name": "Alice", "age": 30}"#.to_string(),
})
.expect("store-payload should succeed");
let col = open_graph(&path);
let payload = col
.get_node_payload(42)
.expect("get should succeed")
.expect("payload should exist");
assert_eq!(payload["name"], "Alice");
assert_eq!(payload["age"], 30);
}
#[test]
fn test_store_payload_overwrites_existing() {
let (_dir, path) = setup_graph_db();
crate::graph::handle(GraphAction::StorePayload {
path: path.clone(),
collection: "kg".to_string(),
node_id: 1,
payload: r#"{"v": 1}"#.to_string(),
})
.expect("first store");
crate::graph::handle(GraphAction::StorePayload {
path: path.clone(),
collection: "kg".to_string(),
node_id: 1,
payload: r#"{"v": 2}"#.to_string(),
})
.expect("second store");
let col = open_graph(&path);
let payload = col.get_node_payload(1).unwrap().unwrap();
assert_eq!(payload["v"], 2);
}
#[test]
fn test_store_payload_invalid_json_fails() {
let (_dir, path) = setup_graph_db();
let result = crate::graph::handle(GraphAction::StorePayload {
path: path.clone(),
collection: "kg".to_string(),
node_id: 1,
payload: "not valid json".to_string(),
});
assert!(result.is_err());
}
#[test]
fn test_get_payload_nonexistent_node_returns_null() {
let (_dir, path) = setup_graph_db();
let col = open_graph(&path);
let payload = col.get_node_payload(999).expect("should not error");
assert!(payload.is_none());
}