sqry_core/graph/unified/build/
incremental.rs1use super::super::edge::EdgeKind;
27use super::super::file::FileId;
28use super::super::node::NodeId;
29use super::super::storage::{AuxiliaryIndices, NodeArena};
30use super::identity::IdentityIndex;
31use super::pass3_intra::PendingEdge;
32
33#[derive(Debug, Clone, Default)]
35pub struct IncrementalStats {
36 pub nodes_removed: usize,
38 pub edges_removed: usize,
40 pub edges_added: usize,
42 pub identity_entries_removed: usize,
44}
45
46#[derive(Debug)]
48pub struct FileRemovalResult {
49 pub stats: IncrementalStats,
51 pub removed_nodes: Vec<NodeId>,
53}
54
55pub fn remove_file_nodes(
72 file_id: FileId,
73 identity_index: &mut IdentityIndex,
74 arena: &mut NodeArena,
75 indices: &mut AuxiliaryIndices,
76) -> FileRemovalResult {
77 let mut stats = IncrementalStats::default();
78
79 let removed_entries = identity_index.remove_file(file_id);
81 stats.identity_entries_removed = removed_entries.len();
82
83 let removed_nodes: Vec<NodeId> = removed_entries.iter().map(|(_, id)| *id).collect();
85 stats.nodes_removed = removed_nodes.len();
86
87 let node_metadata: Vec<_> = removed_nodes
89 .iter()
90 .filter_map(|&node_id| {
91 arena
92 .get(node_id)
93 .map(|entry| (node_id, entry.kind, entry.name, entry.qualified_name))
94 })
95 .collect();
96
97 indices.remove_file_with_info(file_id, node_metadata);
99
100 for &node_id in &removed_nodes {
102 let _ = arena.remove(node_id);
103 }
104
105 FileRemovalResult {
109 stats,
110 removed_nodes,
111 }
112}
113
114#[must_use]
133pub fn add_edge_incremental(
134 source: NodeId,
135 target: NodeId,
136 kind: EdgeKind,
137 file: FileId,
138) -> (IncrementalStats, PendingEdge) {
139 let stats = IncrementalStats {
140 edges_added: 1,
141 ..Default::default()
142 };
143
144 let edge = PendingEdge {
145 source,
146 target,
147 kind,
148 file,
149 spans: vec![], };
151
152 (stats, edge)
153}
154
155#[must_use]
159pub fn add_edges_incremental(edges: &[PendingEdge]) -> IncrementalStats {
160 IncrementalStats {
161 edges_added: edges.len(),
162 ..Default::default()
163 }
164}
165
166pub fn remove_node(
182 node_id: NodeId,
183 identity_index: &mut IdentityIndex,
184 arena: &mut NodeArena,
185) -> IncrementalStats {
186 let mut stats = IncrementalStats::default();
187
188 if identity_index.remove_node_id(node_id).is_some() {
189 stats.identity_entries_removed = 1;
190 }
191
192 if arena.remove(node_id).is_some() {
194 stats.nodes_removed = 1;
195 }
196
197 stats
198}
199
200pub fn remove_nodes_batch(
204 node_ids: &[NodeId],
205 identity_index: &mut IdentityIndex,
206 arena: &mut NodeArena,
207) -> IncrementalStats {
208 let mut stats = IncrementalStats::default();
209
210 for &node_id in node_ids {
211 if identity_index.remove_node_id(node_id).is_some() {
212 stats.identity_entries_removed += 1;
213 }
214 if arena.remove(node_id).is_some() {
215 stats.nodes_removed += 1;
216 }
217 }
218
219 stats
220}
221
222#[derive(Debug, Clone)]
236pub struct EdgeToRemove {
237 pub source: NodeId,
239 pub target: NodeId,
241 pub kind: EdgeKind,
243 pub file: FileId,
245}
246
247#[cfg(test)]
248mod tests {
249 use super::super::identity::IdentityKey;
250 use super::*;
251 use crate::graph::unified::StringId;
252 use crate::graph::unified::node::NodeKind;
253 use crate::graph::unified::storage::NodeEntry;
254
255 fn create_test_entry(name_id: StringId, file_id: FileId) -> NodeEntry {
256 NodeEntry::new(NodeKind::Function, name_id, file_id)
257 }
258
259 #[test]
260 fn test_remove_file_nodes() {
261 let mut arena = NodeArena::new();
262 let mut identity_index = IdentityIndex::new();
263 let mut indices = AuxiliaryIndices::new();
264 let file_id = FileId::new(5);
265
266 let name_id = StringId::new(1);
268 let entry1 = create_test_entry(name_id, file_id);
269 let entry2 = create_test_entry(name_id, file_id);
270 let node1 = arena.alloc(entry1).unwrap();
271 let node2 = arena.alloc(entry2).unwrap();
272
273 let key1 = IdentityKey::new(StringId::new(1), file_id, StringId::new(10));
275 let key2 = IdentityKey::new(StringId::new(1), file_id, StringId::new(11));
276 identity_index.insert(key1, node1);
277 identity_index.insert(key2, node2);
278
279 let result = remove_file_nodes(file_id, &mut identity_index, &mut arena, &mut indices);
281
282 assert_eq!(result.stats.nodes_removed, 2);
283 assert_eq!(result.stats.identity_entries_removed, 2);
284 assert_eq!(result.removed_nodes.len(), 2);
285
286 assert!(arena.get(node1).is_none());
288 assert!(arena.get(node2).is_none());
289 }
290
291 #[test]
292 fn test_add_edge_incremental() {
293 let source = NodeId::new(0, 1);
294 let target = NodeId::new(1, 1);
295 let file_id = FileId::new(0);
296
297 let (stats, edge) = add_edge_incremental(
298 source,
299 target,
300 EdgeKind::Calls {
301 argument_count: 0,
302 is_async: false,
303 },
304 file_id,
305 );
306
307 assert_eq!(stats.edges_added, 1);
308 assert_eq!(edge.source, source);
309 assert_eq!(edge.target, target);
310 assert!(matches!(
311 edge.kind,
312 EdgeKind::Calls {
313 argument_count: 0,
314 is_async: false
315 }
316 ));
317 }
318
319 #[test]
320 fn test_add_edges_incremental_batch() {
321 let file_id = FileId::new(0);
322 let edges = vec![
323 PendingEdge {
324 source: NodeId::new(0, 1),
325 target: NodeId::new(1, 1),
326 kind: EdgeKind::Calls {
327 argument_count: 0,
328 is_async: false,
329 },
330 file: file_id,
331 spans: vec![],
332 },
333 PendingEdge {
334 source: NodeId::new(1, 1),
335 target: NodeId::new(2, 1),
336 kind: EdgeKind::References,
337 file: file_id,
338 spans: vec![],
339 },
340 PendingEdge {
341 source: NodeId::new(2, 1),
342 target: NodeId::new(0, 1),
343 kind: EdgeKind::Calls {
344 argument_count: 0,
345 is_async: false,
346 },
347 file: file_id,
348 spans: vec![],
349 },
350 ];
351
352 let stats = add_edges_incremental(&edges);
353
354 assert_eq!(stats.edges_added, 3);
355 }
356
357 #[test]
358 fn test_remove_node() {
359 let mut arena = NodeArena::new();
360 let mut identity_index = IdentityIndex::new();
361 let file_id = FileId::new(0);
362 let name_id = StringId::new(1);
363
364 let entry = create_test_entry(name_id, file_id);
365 let node_id = arena.alloc(entry).unwrap();
366
367 let key = IdentityKey::new(StringId::new(1), file_id, StringId::new(10));
369 identity_index.insert(key, node_id);
370
371 let stats = remove_node(node_id, &mut identity_index, &mut arena);
372
373 assert_eq!(stats.nodes_removed, 1);
374 assert_eq!(stats.identity_entries_removed, 1);
375 assert!(arena.get(node_id).is_none());
376 }
377
378 #[test]
379 fn test_remove_nodes_batch() {
380 let mut arena = NodeArena::new();
381 let mut identity_index = IdentityIndex::new();
382 let file_id = FileId::new(0);
383 let name_id = StringId::new(1);
384
385 let node1 = arena.alloc(create_test_entry(name_id, file_id)).unwrap();
386 let node2 = arena.alloc(create_test_entry(name_id, file_id)).unwrap();
387 let node3 = arena.alloc(create_test_entry(name_id, file_id)).unwrap();
388
389 identity_index.insert(
390 IdentityKey::new(StringId::new(1), file_id, StringId::new(10)),
391 node1,
392 );
393 identity_index.insert(
394 IdentityKey::new(StringId::new(1), file_id, StringId::new(11)),
395 node2,
396 );
397
398 let stats = remove_nodes_batch(&[node1, node2, node3], &mut identity_index, &mut arena);
399
400 assert_eq!(stats.nodes_removed, 3);
401 assert_eq!(stats.identity_entries_removed, 2);
402 assert!(arena.get(node1).is_none());
403 assert!(arena.get(node2).is_none());
404 assert!(arena.get(node3).is_none());
405 }
406
407 #[test]
408 fn test_remove_nonexistent_node() {
409 let mut arena = NodeArena::new();
410 let mut identity_index = IdentityIndex::new();
411
412 let fake_id = NodeId::new(999, 1);
414 let stats = remove_node(fake_id, &mut identity_index, &mut arena);
415
416 assert_eq!(stats.nodes_removed, 0);
418 assert_eq!(stats.identity_entries_removed, 0);
419 }
420
421 #[test]
422 fn test_incremental_stats_default() {
423 let stats = IncrementalStats::default();
424
425 assert_eq!(stats.nodes_removed, 0);
426 assert_eq!(stats.edges_removed, 0);
427 assert_eq!(stats.edges_added, 0);
428 assert_eq!(stats.identity_entries_removed, 0);
429 }
430}