From e04b780c667efb6f69964655dca1b05c59bbcb22 Mon Sep 17 00:00:00 2001 From: kiki-kanri Date: Sun, 19 Apr 2026 13:27:45 +0800 Subject: [PATCH] feat(ruvector-graph): add delete_edges_batch for single-transaction edge deletion GraphDB and GraphStorage: add delete_edges_batch(ids: &[EdgeId]) -> Result - Single transaction for all deletes (vs N transactions in sequential delete_edge loop) - Returns count of edges actually deleted (skips IDs not found) - Updates edge_type_index and adjacency_index in single pass - All 17 edge tests pass --- crates/ruvector-graph/src/graph.rs | 26 +++++++++ crates/ruvector-graph/src/storage.rs | 16 ++++++ crates/ruvector-graph/tests/edge_tests.rs | 70 +++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/crates/ruvector-graph/src/graph.rs b/crates/ruvector-graph/src/graph.rs index 78e9aea6d..56faeadbc 100644 --- a/crates/ruvector-graph/src/graph.rs +++ b/crates/ruvector-graph/src/graph.rs @@ -218,6 +218,32 @@ impl GraphDB { } } + /// Delete multiple edges (batch) + pub fn delete_edges_batch(&self, ids: &[EdgeId]) -> Result { + let mut deleted = 0; + let mut edges_to_update = Vec::with_capacity(ids.len()); + + for id in ids { + let key: &str = id.as_ref(); + if let Some((_, edge)) = self.edges.remove(key) { + edges_to_update.push(edge); + deleted += 1; + } + } + + for edge in &edges_to_update { + self.edge_type_index.remove_edge(edge); + self.adjacency_index.remove_edge(edge); + } + + #[cfg(feature = "storage")] + if let Some(storage) = &self.storage { + storage.delete_edges_batch(ids)?; + } + + Ok(deleted) + } + /// Get edges by type pub fn get_edges_by_type(&self, edge_type: &str) -> Vec { self.edge_type_index diff --git a/crates/ruvector-graph/src/storage.rs b/crates/ruvector-graph/src/storage.rs index 559e8793c..df765f251 100644 --- a/crates/ruvector-graph/src/storage.rs +++ b/crates/ruvector-graph/src/storage.rs @@ -261,6 +261,22 @@ impl GraphStorage { Ok(deleted) } + pub fn delete_edges_batch(&self, ids: &[EdgeId]) -> Result { + let write_txn = self.db.begin_write()?; + let mut deleted = 0; + { + let mut table = write_txn.open_table(EDGES_TABLE)?; + for id in ids { + if table.remove(id.as_str())?.is_some() { + deleted += 1; + } + } + } + + write_txn.commit()?; + Ok(deleted) + } + /// Get all edge IDs pub fn all_edge_ids(&self) -> Result> { let read_txn = self.db.begin_read()?; diff --git a/crates/ruvector-graph/tests/edge_tests.rs b/crates/ruvector-graph/tests/edge_tests.rs index 90e9244cf..3038f710b 100644 --- a/crates/ruvector-graph/tests/edge_tests.rs +++ b/crates/ruvector-graph/tests/edge_tests.rs @@ -405,3 +405,73 @@ fn test_get_edges_for_nodes() { let empty = db.get_edges_for_nodes(&[]); assert_eq!(empty.len(), 0); } + +#[test] +fn test_delete_edges_batch_basic() { + 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(); + + for i in 0..5 { + let edge = Edge::new( + format!("e{}", i), + "a".to_string(), + "b".to_string(), + "LINKS".to_string(), + Properties::new(), + ); + db.create_edge(edge).unwrap(); + } + + let ids = vec!["e0".to_string(), "e2".to_string(), "e4".to_string()]; + let deleted = db.delete_edges_batch(&ids).unwrap(); + assert_eq!(deleted, 3); + + assert!(db.get_edge("e0").is_none()); + assert!(db.get_edge("e2").is_none()); + assert!(db.get_edge("e4").is_none()); + assert!(db.get_edge("e1").is_some()); + assert!(db.get_edge("e3").is_some()); +} + +#[test] +fn test_delete_edges_batch_partial_not_found() { + 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(); + + let edge = Edge::new("e1".to_string(), "x".to_string(), "y".to_string(), "TO".to_string(), Properties::new()); + db.create_edge(edge).unwrap(); + + let ids = vec!["e1".to_string(), "does_not_exist".to_string()]; + let deleted = db.delete_edges_batch(&ids).unwrap(); + assert_eq!(deleted, 1); + + assert!(db.get_edge("e1").is_none()); +} + +#[test] +fn test_delete_edges_batch_updates_indexes() { + let db = GraphDB::new(); + + db.create_node(Node::new("src".to_string(), vec![], Properties::new())).unwrap(); + db.create_node(Node::new("dst".to_string(), vec![], Properties::new())).unwrap(); + + let edge = Edge::new("edge1".to_string(), "src".to_string(), "dst".to_string(), "T".to_string(), Properties::new()); + db.create_edge(edge).unwrap(); + + assert!(db.get_edges_for_nodes(&["src".to_string()]).len() == 1); + + db.delete_edges_batch(&["edge1".to_string()]).unwrap(); + + assert!(db.get_edges_for_nodes(&["src".to_string()]).is_empty()); +} + +#[test] +fn test_delete_edges_batch_empty() { + let db = GraphDB::new(); + let deleted = db.delete_edges_batch(&[]).unwrap(); + assert_eq!(deleted, 0); +}