1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Node-deletion's above-storage half.
//!
//! `GraphWrite::remove_node` takes the node out of the backend. Three pieces
//! of state that a deleted node owns live one layer *above* the backend, on
//! `DirGraph` itself, so nothing inside storage can see them go — and each is
//! readable only while the node still exists. They are removed here, in the
//! same loop as the backend removal and in the order that keeps each read
//! valid.
//!
//! Extracted from `maintain.rs` to keep that file under the god-file LoC
//! ceiling. The index/bucket sweeps that follow a deletion stay there, in
//! `detach_delete_nodes`, which is this module's only caller.
use HashSet;
use NodeIndex;
use crateDirGraph;
use crateGraphWrite;
/// Drop `node_idx`'s vector from every embedding store that holds one, and
/// journal each removal so a statement rollback can put it back.
///
/// **Why deletion must reach this map.** `EmbeddingStore` is keyed by the
/// global `NodeIndex`, and `StableDiGraph` hands a freed index straight to
/// the next node created. A vector left behind is therefore not merely stale
/// bookkeeping: the next node to land on that slot — of *any* type, embedded
/// or not — inherits it and comes back as a full-similarity top hit from
/// `vector_search`, on both the scan and the HNSW path.
///
/// Costs one hash probe per store, and stores are per `(node_type, property)`
/// — a handful, independent of graph size. The `is_empty` guard keeps the
/// overwhelmingly common un-embedded graph at zero cost per deleted node.
/// Remove each doomed node from storage, carrying the three pieces of state
/// that live *above* storage and so cannot be recovered afterwards.
///
/// All three are read while the node still exists and are lost the moment it
/// does not, which is why they are here rather than in `detach_delete_nodes`'
/// sweeps:
///
/// - **The change-capture before-image's labels.** The capture wrapper reads
/// the node's properties and title as it removes it, but secondary labels
/// live in `DirGraph::secondary_label_index`, one layer above the backend.
/// A delete is the one event whose only informative half is `before`, so an
/// image missing its labels is the whole loss.
/// - **The dropped timeseries entry.** `timeseries_store` is O(V) and so is
/// deliberately not part of the checkpoint's schema clone; statement
/// rollback recovers it from the undo journal instead.
/// - **The node's embeddings.** Same ownership story as the timeseries, with a
/// sharper failure mode: the freed `NodeIndex` is reused, so a vector left
/// behind is inherited rather than merely orphaned. See
/// [`prune_doomed_embeddings`].
pub