Skip to main content

linkrs_storage/edge/immutable_csr/
read.rs

1use super::super::csr_shared::{decode_endpoint_pair, is_reclaimable_cold};
2use super::super::{EdgeId, EdgePosition, HotNbr, Nbr, Timestamp, VertexId, INVALID_EDGE_ID};
3use super::pack::frozen_key_range;
4use super::ImmutableCsr;
5
6impl ImmutableCsr {
7    pub(crate) fn row_window(&self, src_vid: u32) -> Option<(usize, usize)> {
8        let idx = src_vid as usize;
9        if idx >= self.degrees.len() {
10            return None;
11        }
12        let start = self.offsets[idx] as usize;
13        let degree = self.degrees[idx] as usize;
14        if start.saturating_add(degree) > self.hot_entries.len() {
15            return None;
16        }
17        Some((start, start + degree))
18    }
19
20    /// Assembled slot copy at a packed index.
21    #[inline]
22    pub(crate) fn slot_at(&self, idx: usize) -> Option<Nbr> {
23        Some(Nbr::from_parts(
24            *self.hot_entries.get(idx)?,
25            *self.cold_entries.get(idx)?,
26        ))
27    }
28
29    /// Row length of one vertex. Out-of-range rows report zero.
30    pub fn row_degree(&self, src_vid: u32) -> usize {
31        let idx = src_vid as usize;
32        if idx >= self.degrees.len() {
33            0
34        } else {
35            self.degrees[idx] as usize
36        }
37    }
38
39    /// Timestamp-filtered read of one row.
40    ///
41    /// UNFILTERED single-predicate read for tests and offline tools only:
42    /// filters by row delete stamp without consulting the version authority.
43    /// Production traversals must use the row iterator or caller-buffer fill
44    /// paths with the authority gate.
45    pub fn edges_of(&self, src_vid: u32, ts: Timestamp) -> Vec<Nbr> {
46        let Some((start, end)) = self.row_window(src_vid) else {
47            return Vec::new();
48        };
49        self.hot_entries[start..end]
50            .iter()
51            .zip(&self.cold_entries[start..end])
52            .filter_map(|(hot, cold)| {
53                let nbr = Nbr::from_parts(*hot, *cold);
54                nbr.is_alive_at(ts).then_some(nbr)
55            })
56            .collect()
57    }
58
59    /// First timestamp-visible entry matching an endpoint key.
60    ///
61    /// Bisects the sorted row to the `(endpoint, rank)` key range, then
62    /// filters inside the range by timestamp. The row sort makes edge-id
63    /// order the total version order within each key.
64    pub fn get_edge(&self, src_vid: u32, dst: VertexId, ts: Timestamp) -> Option<Nbr> {
65        let (decoded_endpoint, decoded_rank) = decode_endpoint_pair(dst)?;
66        let (start, end) = self.row_window(src_vid)?;
67        let hot = &self.hot_entries[start..end];
68        let cold = &self.cold_entries[start..end];
69        let (lo, hi) = frozen_key_range(hot, decoded_endpoint, decoded_rank);
70        hot[lo..hi]
71            .iter()
72            .zip(&cold[lo..hi])
73            .find_map(|(hot, cold)| {
74                let nbr = Nbr::from_parts(*hot, *cold);
75                nbr.is_alive_at(ts).then_some(nbr)
76            })
77    }
78
79    /// First live entry matching an endpoint key without consulting snapshots.
80    ///
81    /// Same key-range bisection as [`Self::get_edge`]; liveness is the raw
82    /// open-deletion-stamp check instead of a timestamp filter.
83    pub fn get_edge_physical(&self, src_vid: u32, dst: VertexId) -> Option<Nbr> {
84        let (decoded_endpoint, decoded_rank) = decode_endpoint_pair(dst)?;
85        let (start, end) = self.row_window(src_vid)?;
86        let hot = &self.hot_entries[start..end];
87        let cold = &self.cold_entries[start..end];
88        let (lo, hi) = frozen_key_range(hot, decoded_endpoint, decoded_rank);
89        hot[lo..hi]
90            .iter()
91            .zip(&cold[lo..hi])
92            .find_map(|(hot, cold)| {
93                if hot.edge_id != INVALID_EDGE_ID && cold.is_live() {
94                    Some(Nbr::from_parts(*hot, *cold))
95                } else {
96                    None
97                }
98            })
99    }
100
101    /// Every physically stored entry of one row without timestamp filtering.
102    ///
103    /// Test and offline use; production scans use `fill_physical_into` or
104    /// the row iterator instead of this allocating accessor.
105    pub fn physical_edges_of(&self, src_vid: u32) -> Vec<Nbr> {
106        let mut out = Vec::new();
107        self.fill_physical_into(src_vid, &mut out);
108        out
109    }
110
111    /// Fill a caller buffer with every physically stored entry of one row.
112    pub fn fill_physical_into(&self, src_vid: u32, out: &mut Vec<Nbr>) {
113        out.clear();
114        let Some((start, end)) = self.row_window(src_vid) else {
115            return;
116        };
117        out.reserve(end - start);
118        for (hot, cold) in self.hot_entries[start..end]
119            .iter()
120            .zip(&self.cold_entries[start..end])
121        {
122            out.push(Nbr::from_parts(*hot, *cold));
123        }
124    }
125
126    /// Visit every physically stored entry of one row without allocating.
127    pub fn visit_physical<F>(&self, src_vid: u32, mut f: F)
128    where
129        F: FnMut(Nbr) -> bool,
130    {
131        let Some((start, end)) = self.row_window(src_vid) else {
132            return;
133        };
134        for (hot, cold) in self.hot_entries[start..end]
135            .iter()
136            .zip(&self.cold_entries[start..end])
137        {
138            if !f(Nbr::from_parts(*hot, *cold)) {
139                return;
140            }
141        }
142    }
143
144    /// Visit every physically stored hot half of one row without allocating
145    /// and without touching the stamp lines.
146    ///
147    /// Hot-only counterpart of [`Self::visit_physical`] for traversals that
148    /// resolve visibility through the version authority by `edge_id`.
149    pub fn visit_hot<F>(&self, src_vid: u32, mut f: F)
150    where
151        F: FnMut(HotNbr) -> bool,
152    {
153        let Some((start, end)) = self.row_window(src_vid) else {
154            return;
155        };
156        for hot in &self.hot_entries[start..end] {
157            if !f(*hot) {
158                return;
159            }
160        }
161    }
162
163    /// Packed rows are always sorted by `(endpoint, rank, edge_id)`.
164    pub fn is_row_sorted(&self, _src_vid: u32) -> bool {
165        true
166    }
167
168    /// Visit live entries whose `(endpoint, rank)` key falls in the inclusive
169    /// `[lower, upper]` range. The packed row is sorted, so the window is
170    /// bisected and only the window is scanned for liveness.
171    pub fn visit_threshold<F>(
172        &self,
173        src_vid: u32,
174        lower: Option<(u32, i64)>,
175        upper: Option<(u32, i64)>,
176        mut f: F,
177    ) where
178        F: FnMut(Nbr) -> bool,
179    {
180        let Some((start, end)) = self.row_window(src_vid) else {
181            return;
182        };
183        let hot = &self.hot_entries[start..end];
184        let cold = &self.cold_entries[start..end];
185        let lo = match lower {
186            Some((lo_ep, lo_rank)) => {
187                hot.partition_point(|h| (h.endpoint, h.rank) < (lo_ep, lo_rank))
188            }
189            None => 0,
190        };
191        let hi = match upper {
192            Some((hi_ep, hi_rank)) => {
193                hot.partition_point(|h| (h.endpoint, h.rank) <= (hi_ep, hi_rank))
194            }
195            None => hot.len(),
196        };
197        for (h, c) in hot[lo..hi].iter().zip(&cold[lo..hi]) {
198            if c.is_live() && !f(Nbr::from_parts(*h, *c)) {
199                return;
200            }
201        }
202    }
203
204    /// Fill a caller buffer with the same range content as `visit_threshold`.
205    pub fn fill_threshold_into(
206        &self,
207        src_vid: u32,
208        lower: Option<(u32, i64)>,
209        upper: Option<(u32, i64)>,
210        out: &mut Vec<Nbr>,
211    ) {
212        out.clear();
213        self.visit_threshold(src_vid, lower, upper, |nbr| {
214            out.push(nbr);
215            true
216        });
217    }
218
219    /// Read-only view of one packed slot without mutating state.
220    pub fn nbr_at_offset(&self, src_vid: u32, offset: i32) -> Option<Nbr> {
221        if offset < 0 {
222            return None;
223        }
224        let (start, end) = self.row_window(src_vid)?;
225        if start.saturating_add(offset as usize) >= end {
226            return None;
227        }
228        self.slot_at(start + offset as usize)
229    }
230
231    /// Whether one row holds any physically stored entry.
232    pub fn has_physical_entries(&self, vid: u32) -> bool {
233        self.row_degree(vid) > 0
234    }
235
236    /// Whether one row holds `edge_id`.
237    pub fn primary_contains(&self, src_vid: u32, edge_id: EdgeId) -> bool {
238        let Some((start, end)) = self.row_window(src_vid) else {
239            return false;
240        };
241        self.hot_entries[start..end]
242            .iter()
243            .any(|hot| hot.edge_id == edge_id)
244    }
245
246    /// Locate the first entry with `edge_id`, returning its packed slot.
247    pub fn locate_edge(&self, src_vid: u32, edge_id: EdgeId) -> Option<(EdgePosition, Nbr)> {
248        let (start, end) = self.row_window(src_vid)?;
249        for (slot, (hot, cold)) in self.hot_entries[start..end]
250            .iter()
251            .zip(&self.cold_entries[start..end])
252            .enumerate()
253        {
254            if hot.edge_id == edge_id {
255                return Some((
256                    EdgePosition::Primary { slot: slot as u32 },
257                    Nbr::from_parts(*hot, *cold),
258                ));
259            }
260        }
261        None
262    }
263
264    /// Whether this frozen group carries a bundled inline value column.
265    ///
266    /// Only groups packed from a valued bundled group do; every other pack
267    /// leaves the value columns empty and reads every value as NULL.
268    pub fn has_valued_entries(&self) -> bool {
269        !self.values.is_empty()
270    }
271
272    /// Raw value word plus validity at a packed index.
273    ///
274    /// Out-of-range indexes and groups without value columns read as a zero
275    /// word with a cleared bit, matching the bundled NULL convention.
276    pub(crate) fn value_at_index(&self, idx: usize) -> (u64, bool) {
277        if idx >= self.values.len() {
278            return (0, false);
279        }
280        let raw = self.values[idx];
281        let valid = self.valid.get(idx).map(|b| *b).unwrap_or(false);
282        (raw, valid)
283    }
284
285    /// Read the inline value of one edge by id within its source row.
286    ///
287    /// Paired value half of a frozen topology walk: resolve the edge from
288    /// the topology walk first, then read its value here.
289    pub fn value_by_edge_id(&self, src_vid: u32, edge_id: EdgeId) -> Option<(u64, bool)> {
290        let (start, end) = self.row_window(src_vid)?;
291        for idx in start..end {
292            if self.hot_entries[idx].edge_id == edge_id {
293                return Some(self.value_at_index(idx));
294            }
295        }
296        None
297    }
298
299    /// Read the inline value of the first physical entry for one endpoint.
300    ///
301    /// First-match walk like the bundled accessor: tombstones are physical
302    /// entries and may match, mirroring the bundled layout where deletion
303    /// clears validity but keeps the slot position.
304    pub fn value_by_endpoint(&self, src_vid: u32, endpoint: u32) -> Option<(u64, bool)> {
305        let (start, end) = self.row_window(src_vid)?;
306        for idx in start..end {
307            let hot = self.hot_entries[idx];
308            if hot.endpoint == endpoint && hot.edge_id != INVALID_EDGE_ID {
309                return Some(self.value_at_index(idx));
310            }
311        }
312        None
313    }
314
315    /// Visit every physically stored entry of one row with its inline value
316    /// (`None` for NULL slots).
317    ///
318    /// Frozen counterpart of the bundled paired traversal: topology and
319    /// value stay slot-parallel after the pack reorder, so one index serves
320    /// both halves.
321    pub fn visit_physical_with_values<F>(&self, src_vid: u32, mut f: F)
322    where
323        F: FnMut(Nbr, Option<u64>) -> bool,
324    {
325        let Some((start, end)) = self.row_window(src_vid) else {
326            return;
327        };
328        for idx in start..end {
329            let nbr = Nbr::from_parts(self.hot_entries[idx], self.cold_entries[idx]);
330            let (raw, valid) = self.value_at_index(idx);
331            if !f(nbr, valid.then_some(raw)) {
332                return;
333            }
334        }
335    }
336
337    /// Whether any slot holds a valid inline value.
338    pub fn any_valid_values(&self) -> bool {
339        self.valid.count_ones() > 0
340    }
341
342    /// Packed value words for snapshot-file writers.
343    pub(crate) fn packed_values(&self) -> &[u64] {
344        &self.values
345    }
346
347    /// Packed validity bytes for snapshot-file writers.
348    ///
349    /// Exactly covering the packed entries: trailing bits of the last byte
350    /// are zero padding, never edges.
351    pub(crate) fn packed_valid_bytes(&self) -> &[u8] {
352        let want = self.values.len().div_ceil(8);
353        self.valid.as_raw_slice().get(..want).unwrap_or(&[])
354    }
355
356    /// Physical entry census of one row: `(live, dead, capacity)`.
357    ///
358    /// Same live/dead predicates as the mutable census; capacity equals the
359    /// row length because frozen rows carry no reserved gaps.
360    pub fn vertex_census(&self, vid: u32) -> (usize, usize, usize) {
361        let Some((start, end)) = self.row_window(vid) else {
362            return (0, 0, 0);
363        };
364        let mut live = 0usize;
365        let mut dead = 0usize;
366        for cold in &self.cold_entries[start..end] {
367            if cold.is_live() {
368                live += 1;
369            } else {
370                dead += 1;
371            }
372        }
373        (live, dead, end - start)
374    }
375
376    /// Count entries of one row reclaimable at `cutoff`.
377    pub fn reclaimable_count(&self, vid: u32, cutoff: Timestamp) -> usize {
378        if cutoff == Timestamp::MAX {
379            return 0;
380        }
381        let Some((start, end)) = self.row_window(vid) else {
382            return 0;
383        };
384        self.cold_entries[start..end]
385            .iter()
386            .filter(|cold| is_reclaimable_cold(cold, cutoff))
387            .count()
388    }
389}