feat(ruvector-graph): add delete_edges_batch for single-transaction edge deletion

GraphDB and GraphStorage: add delete_edges_batch(ids: &[EdgeId]) -> Result<usize>
- 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
This commit is contained in:
kiki-kanri 2026-04-19 13:27:45 +08:00
parent 9e08b74c1a
commit e04b780c66
3 changed files with 112 additions and 0 deletions

View file

@ -218,6 +218,32 @@ impl GraphDB {
}
}
/// Delete multiple edges (batch)
pub fn delete_edges_batch(&self, ids: &[EdgeId]) -> Result<usize> {
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<Edge> {
self.edge_type_index

View file

@ -261,6 +261,22 @@ impl GraphStorage {
Ok(deleted)
}
pub fn delete_edges_batch(&self, ids: &[EdgeId]) -> Result<usize> {
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<Vec<EdgeId>> {
let read_txn = self.db.begin_read()?;

View file

@ -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);
}