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}