Skip to main content

oxgraph_postgres/
overlay.rs

1//! Overlay buffers and tombstones layered on immutable base snapshots.
2
3use alloc::{
4    collections::{BTreeMap, BTreeSet},
5    vec::Vec,
6};
7
8/// Edge insertion applied on top of the frozen artifact.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct OverlayEdge {
11    /// Source node id in dense index space.
12    pub source: u32,
13    /// Target node id in dense index space.
14    pub target: u32,
15}
16
17/// Mutable overlay state for sync replay and query freshness.
18///
19/// All collections are private so the `by_source`/`by_target` adjacency indexes
20/// cannot desync from `added_edges`: callers mutate exclusively through
21/// [`push_edge`](Self::push_edge), [`remove_edge`](Self::remove_edge),
22/// [`tombstone_edge`](Self::tombstone_edge), [`tombstone_node`](Self::tombstone_node),
23/// [`clear`](Self::clear), and the [`from_edges`](Self::from_edges) constructor,
24/// each of which keeps the indexes consistent.
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct OverlayState {
27    /// Edges inserted after the base artifact was published.
28    added_edges: Vec<OverlayEdge>,
29    /// Overlay targets grouped by source for `O(1)` forward expansion lookup.
30    by_source: BTreeMap<u32, Vec<u32>>,
31    /// Overlay sources grouped by target for `O(1)` inbound expansion lookup.
32    by_target: BTreeMap<u32, Vec<u32>>,
33    /// Base edge ids tombstoned by sync events.
34    tombstoned_edges: BTreeSet<u32>,
35    /// Node ids hidden from traversal results.
36    tombstoned_nodes: BTreeSet<u32>,
37}
38
39impl OverlayState {
40    /// Builds an overlay from a sequence of inserted edges, with the adjacency
41    /// indexes constructed once up front.
42    ///
43    /// # Performance
44    ///
45    /// This function is `O(a log a)` for `a` edges.
46    #[must_use]
47    pub fn from_edges(edges: impl IntoIterator<Item = OverlayEdge>) -> Self {
48        let mut overlay = Self::default();
49        for edge in edges {
50            overlay.push_edge(edge);
51        }
52        overlay
53    }
54
55    /// Returns the inserted overlay edges in insertion order.
56    ///
57    /// # Performance
58    ///
59    /// This method is `O(1)`.
60    #[must_use]
61    pub fn added_edges(&self) -> &[OverlayEdge] {
62        &self.added_edges
63    }
64
65    /// Returns the number of inserted overlay edges.
66    ///
67    /// # Performance
68    ///
69    /// This method is `O(1)`.
70    #[must_use]
71    pub const fn overlay_edge_count(&self) -> usize {
72        self.added_edges.len()
73    }
74
75    /// Returns the number of tombstoned base edges.
76    ///
77    /// # Performance
78    ///
79    /// This method is `O(1)`.
80    #[must_use]
81    pub fn tombstoned_edge_count(&self) -> usize {
82        self.tombstoned_edges.len()
83    }
84
85    /// Returns the number of tombstoned nodes.
86    ///
87    /// # Performance
88    ///
89    /// This method is `O(1)`.
90    #[must_use]
91    pub fn tombstoned_node_count(&self) -> usize {
92        self.tombstoned_nodes.len()
93    }
94
95    /// Records a base edge tombstone.
96    ///
97    /// # Performance
98    ///
99    /// This method is `O(log t)` for `t` tombstoned edge ids.
100    pub fn tombstone_edge(&mut self, edge_id: u32) {
101        self.tombstoned_edges.insert(edge_id);
102    }
103
104    /// Records a node tombstone.
105    ///
106    /// # Performance
107    ///
108    /// This method is `O(log t)` for `t` tombstoned node ids.
109    pub fn tombstone_node(&mut self, node_id: u32) {
110        self.tombstoned_nodes.insert(node_id);
111    }
112
113    /// Clears all overlay buffers and adjacency indexes.
114    pub fn clear(&mut self) {
115        self.added_edges.clear();
116        self.by_source.clear();
117        self.by_target.clear();
118        self.tombstoned_edges.clear();
119        self.tombstoned_nodes.clear();
120    }
121
122    /// Returns whether any base edge tombstones are recorded.
123    #[must_use]
124    pub fn has_edge_tombstones(&self) -> bool {
125        !self.tombstoned_edges.is_empty()
126    }
127
128    /// Returns whether any node tombstones are recorded.
129    #[must_use]
130    pub fn has_node_tombstones(&self) -> bool {
131        !self.tombstoned_nodes.is_empty()
132    }
133
134    /// Returns whether an edge id is visible to traversal.
135    ///
136    /// # Performance
137    ///
138    /// This method is `O(log t)` for `t` tombstoned edge ids.
139    #[must_use]
140    pub fn edge_visible(&self, edge_id: u32) -> bool {
141        !self.tombstoned_edges.contains(&edge_id)
142    }
143
144    /// Returns whether a node id is visible to traversal.
145    ///
146    /// # Performance
147    ///
148    /// This method is `O(log t)` for `t` tombstoned node ids.
149    #[must_use]
150    pub fn node_visible(&self, node_id: u32) -> bool {
151        !self.tombstoned_nodes.contains(&node_id)
152    }
153
154    /// Records an overlay edge and updates adjacency indexes.
155    ///
156    /// # Performance
157    ///
158    /// This method is `O(log s + log t)` for index map sizes `s` and `t`.
159    pub fn push_edge(&mut self, edge: OverlayEdge) {
160        self.added_edges.push(edge);
161        self.by_source
162            .entry(edge.source)
163            .or_default()
164            .push(edge.target);
165        self.by_target
166            .entry(edge.target)
167            .or_default()
168            .push(edge.source);
169    }
170
171    /// Returns overlay outgoing targets for `source` (empty when none).
172    ///
173    /// # Performance
174    ///
175    /// This method is `O(log s)` for `s` indexed sources.
176    #[must_use]
177    pub fn overlay_targets(&self, source: u32) -> &[u32] {
178        self.by_source
179            .get(&source)
180            .map_or(&[] as &[u32], Vec::as_slice)
181    }
182
183    /// Returns overlay incoming sources for `target` (empty when none).
184    ///
185    /// # Performance
186    ///
187    /// This method is `O(log t)` for `t` indexed targets.
188    #[must_use]
189    pub fn overlay_sources(&self, target: u32) -> &[u32] {
190        self.by_target
191            .get(&target)
192            .map_or(&[] as &[u32], Vec::as_slice)
193    }
194
195    /// Removes one overlay edge and rebuilds adjacency indexes.
196    ///
197    /// # Performance
198    ///
199    /// This method is `O(a)` for `a` overlay edges.
200    pub fn remove_edge(&mut self, source: u32, target: u32) {
201        self.added_edges
202            .retain(|edge| edge.source != source || edge.target != target);
203        self.rebuild_indexes();
204    }
205
206    /// Rebuilds `by_source` / `by_target` from `added_edges`.
207    ///
208    /// Crate-internal: external callers cannot desync the indexes because the
209    /// edge buffers are private, so this is only reached by [`remove_edge`].
210    ///
211    /// # Performance
212    ///
213    /// This method is `O(a)` for `a` overlay edges.
214    pub(crate) fn rebuild_indexes(&mut self) {
215        self.by_source.clear();
216        self.by_target.clear();
217        for edge in &self.added_edges {
218            self.by_source
219                .entry(edge.source)
220                .or_default()
221                .push(edge.target);
222            self.by_target
223                .entry(edge.target)
224                .or_default()
225                .push(edge.source);
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::{OverlayEdge, OverlayState};
233
234    #[test]
235    fn indexes_match_linear_scan() {
236        let mut overlay = OverlayState::default();
237        overlay.push_edge(OverlayEdge {
238            source: 1,
239            target: 2,
240        });
241        overlay.push_edge(OverlayEdge {
242            source: 1,
243            target: 3,
244        });
245        overlay.push_edge(OverlayEdge {
246            source: 4,
247            target: 1,
248        });
249        assert_eq!(overlay.overlay_targets(1), &[2, 3]);
250        assert_eq!(overlay.overlay_sources(1), &[4]);
251        overlay.clear();
252        assert!(overlay.overlay_targets(1).is_empty());
253    }
254
255    #[test]
256    fn from_edges_builds_indexes() {
257        let overlay = OverlayState::from_edges([
258            OverlayEdge {
259                source: 0,
260                target: 1,
261            },
262            OverlayEdge {
263                source: 2,
264                target: 0,
265            },
266        ]);
267        assert_eq!(overlay.overlay_targets(0), &[1]);
268        assert_eq!(overlay.overlay_sources(0), &[2]);
269    }
270}