Skip to main content

linkrs_storage/edge/node_group/
read.rs

1//! Routed physical read helpers over the shard set.
2//!
3//! Reads resolve rows through `route`, never creating groups: missing groups
4//! read as empty.
5//!
6//! Batch scans group consecutive same-group vertices into runs so each run
7//! borrows its shard once, then walks rows through the `CsrVariant`
8//! single-row entries. Dispatch itself stays converged in the variant: these
9//! callers never match on the form, so adding a form only touches the
10//! variant read entries.
11
12use linkrs_core::types::EdgeId;
13
14use super::super::{HotNbr, MutableCsrTrait, Nbr};
15use super::CsrShardSet;
16
17impl CsrShardSet {
18    /// Whether the primary row of one vertex holds `edge_id`.
19    pub fn primary_contains(&self, src_vid: u32, edge_id: EdgeId) -> bool {
20        let Some((gid, local)) = self.route(src_vid) else {
21            return false;
22        };
23        self.shards.get(&gid).is_some_and(|shard| {
24            if let Some(mapped) = &shard.mapped {
25                return mapped.primary_contains(local, edge_id);
26            }
27            shard.variant.primary_contains(local, edge_id)
28        })
29    }
30
31    /// Owner group of one global vertex id without creating groups.
32    ///
33    /// Shared row-location entry point for point lookups, adjacency batches
34    /// and full scans: every read path resolves rows through this routing
35    /// instead of duplicating the group arithmetic.
36    pub fn group_of(&self, vid: u32) -> Option<usize> {
37        self.route(vid).map(|(gid, _)| gid)
38    }
39
40    /// Visit every physically stored entry of one vertex without allocating.
41    pub fn visit_physical<F>(&self, src_vid: u32, mut f: F)
42    where
43        F: FnMut(Nbr) -> bool,
44    {
45        let Some((gid, local)) = self.route(src_vid) else {
46            return;
47        };
48        if let Some(shard) = self.shards.get(&gid) {
49            let mut visited = 0usize;
50            let wrapped = |nbr: Nbr| {
51                visited += 1;
52                f(nbr)
53            };
54            if let Some(mapped) = &shard.mapped {
55                mapped.visit_physical(local, wrapped);
56            } else {
57                shard.variant.visit_physical(local, wrapped);
58            }
59            if visited > super::super::mutable_csr::row::HIGH_DEGREE_LIVE_THRESHOLD {
60                log::warn!(
61                    "slow topology traversal at row {} (group {} local {}): degree {} exceeds wide threshold",
62                    src_vid,
63                    gid,
64                    local,
65                    visited
66                );
67            }
68        }
69    }
70
71    /// Visit every physically stored hot half of one vertex without
72    /// allocating and without touching the stamp lines.
73    ///
74    /// Hot-only counterpart of [`Self::visit_physical`] for traversals that
75    /// resolve visibility through the version authority by `edge_id`.
76    pub fn visit_hot<F>(&self, src_vid: u32, f: F)
77    where
78        F: FnMut(HotNbr) -> bool,
79    {
80        let Some((gid, local)) = self.route(src_vid) else {
81            return;
82        };
83        if let Some(shard) = self.shards.get(&gid) {
84            if let Some(mapped) = &shard.mapped {
85                mapped.visit_hot(local, f);
86            } else {
87                shard.variant.visit_hot(local, f);
88            }
89        }
90    }
91
92    /// Fill a caller buffer with every physically stored entry of one vertex.
93    ///
94    /// Same content as the allocating trait accessor, without the per-vertex
95    /// allocation. Missing groups read as empty and never create groups.
96    pub fn fill_physical_into(&self, src_vid: u32, out: &mut Vec<Nbr>) {
97        let Some((gid, local)) = self.route(src_vid) else {
98            out.clear();
99            return;
100        };
101        match self.shards.get(&gid) {
102            Some(shard) => {
103                if let Some(mapped) = &shard.mapped {
104                    mapped.fill_physical_into(local, out);
105                } else {
106                    shard.variant.fill_physical_into(local, out);
107                }
108            }
109            None => out.clear(),
110        }
111    }
112
113    /// Fill one shared buffer with the physical entries of many vertices.
114    ///
115    /// Records one start offset per vertex plus a trailing end offset, so
116    /// `out[offsets[i]..offsets[i + 1]]` is vertex `vids[i]` in order. One
117    /// pass over the vertices, no per-vertex allocation; missing groups and
118    /// empty rows contribute empty slices. Read paths never create groups.
119    ///
120    /// Dispatch stays converged in `CsrVariant`: each run borrows its shard
121    /// once (one map lookup per run), then walks rows through the variant
122    /// single-row entry. The row loop pays one variant match per row, which
123    /// is noise next to the per-row routing plus the row walk itself; in
124    /// return there are no per-form arms to keep in sync here, so adding a
125    /// form only touches the variant read entries.
126    pub fn fill_physical_batch_into(
127        &self,
128        vids: &[u32],
129        out: &mut Vec<Nbr>,
130        offsets: &mut Vec<usize>,
131    ) {
132        out.clear();
133        offsets.clear();
134        offsets.reserve(vids.len() + 1);
135        let mut idx = 0usize;
136        while idx < vids.len() {
137            let Some((gid, _)) = self.route(vids[idx]) else {
138                offsets.push(out.len());
139                idx += 1;
140                continue;
141            };
142            let mut run_end = idx + 1;
143            while run_end < vids.len() {
144                match self.route(vids[run_end]) {
145                    Some((next_gid, _)) if next_gid == gid => run_end += 1,
146                    _ => break,
147                }
148            }
149            let Some(shard) = self.shards.get(&gid) else {
150                for _ in idx..run_end {
151                    offsets.push(out.len());
152                }
153                idx = run_end;
154                continue;
155            };
156            let variant = &shard.variant;
157            let mapped = shard.mapped.clone();
158            for vid in &vids[idx..run_end] {
159                offsets.push(out.len());
160                let (_, local) = self.route(*vid).expect("run shares one group");
161                if let Some(mapped) = &mapped {
162                    mapped.visit_physical(local, |nbr| {
163                        out.push(nbr);
164                        true
165                    });
166                } else {
167                    variant.visit_physical(local, |nbr| {
168                        out.push(nbr);
169                        true
170                    });
171                }
172            }
173            idx = run_end;
174        }
175        offsets.push(out.len());
176    }
177
178    /// Fill one shared buffer with paired topology plus inline values.
179    ///
180    /// Batched counterpart of the single-row paired fill: same offsets
181    /// contract as the topology batch above, but each entry carries its
182    /// inline value. Non-bundled forms fill with `None` values through the
183    /// same walk. Like the topology batch, each run borrows its shard once
184    /// and walks rows through the variant paired entry, so there are no
185    /// per-form arms to keep in sync here either.
186    pub fn fill_physical_with_values_batch_into(
187        &self,
188        vids: &[u32],
189        out: &mut Vec<(Nbr, Option<u64>)>,
190        offsets: &mut Vec<usize>,
191    ) {
192        out.clear();
193        offsets.clear();
194        offsets.reserve(vids.len() + 1);
195        let mut idx = 0usize;
196        while idx < vids.len() {
197            let Some((gid, _)) = self.route(vids[idx]) else {
198                offsets.push(out.len());
199                idx += 1;
200                continue;
201            };
202            let mut run_end = idx + 1;
203            while run_end < vids.len() {
204                match self.route(vids[run_end]) {
205                    Some((next_gid, _)) if next_gid == gid => run_end += 1,
206                    _ => break,
207                }
208            }
209            let Some(shard) = self.shards.get(&gid) else {
210                for _ in idx..run_end {
211                    offsets.push(out.len());
212                }
213                idx = run_end;
214                continue;
215            };
216            let variant = &shard.variant;
217            let mapped = shard.mapped.clone();
218            for vid in &vids[idx..run_end] {
219                offsets.push(out.len());
220                let (_, local) = self.route(*vid).expect("run shares one group");
221                if let Some(mapped) = &mapped {
222                    mapped.visit_physical_with_values(local, |nbr, value| {
223                        out.push((nbr, value));
224                        true
225                    });
226                } else {
227                    variant.visit_physical_with_values(local, |nbr, value| {
228                        out.push((nbr, value));
229                        true
230                    });
231                }
232            }
233            idx = run_end;
234        }
235        offsets.push(out.len());
236    }
237
238    /// Whether the live entries of one row arrive in key order.
239    ///
240    /// Only frozen and single-slot rows promise order and may use bisection;
241    /// other variants report an observation that is memory-only, rebuilt on
242    /// load and never cached across restarts. Planning paths prefer
243    /// `should_use_bisection` below so the promise plus the live observation
244    /// stay in one place. Frozen groups serve the same sorted order from the
245    /// heap and from a derived mapping alike.
246    pub fn is_row_sorted(&self, src_vid: u32) -> bool {
247        let Some((gid, local)) = self.route(src_vid) else {
248            return true;
249        };
250        self.shards
251            .get(&gid)
252            .is_some_and(|shard| shard.variant.is_row_sorted(local))
253    }
254
255    /// Whether a range scan should bisect this row.
256    ///
257    /// Central plan selection combining the order promise with the live
258    /// sorted observation. Missing groups read as sorted empty. The result
259    /// is memory-only and must never be cached across restarts.
260    pub fn should_use_bisection(&self, src_vid: u32) -> bool {
261        let Some((gid, local)) = self.route(src_vid) else {
262            return true;
263        };
264        self.shards
265            .get(&gid)
266            .is_some_and(|shard| shard.variant.should_use_bisection(local))
267    }
268
269    /// Sort one row on the maintenance path. Positions for the row go stale.
270    pub fn sort_row(&mut self, src_vid: u32) -> bool {
271        let Some((gid, local)) = self.route(src_vid) else {
272            return false;
273        };
274        self.shards
275            .get_mut(&gid)
276            .is_some_and(|shard| shard.variant.sort_row(local))
277    }
278
279    /// Visit live entries of one row whose key falls in the inclusive range.
280    ///
281    /// Pure and bundled rows ignore the rank halves by contract: callers
282    /// pass endpoint intervals there.
283    pub fn visit_threshold<F>(
284        &self,
285        src_vid: u32,
286        lower: Option<(u32, i64)>,
287        upper: Option<(u32, i64)>,
288        f: F,
289    ) where
290        F: FnMut(Nbr) -> bool,
291    {
292        let Some((gid, local)) = self.route(src_vid) else {
293            return;
294        };
295        if let Some(shard) = self.shards.get(&gid) {
296            if let Some(mapped) = &shard.mapped {
297                mapped.visit_threshold(local, lower, upper, f);
298            } else {
299                shard.variant.visit_threshold(local, lower, upper, f);
300            }
301        }
302    }
303
304    /// Fill a caller buffer with the same range content as `visit_threshold`.
305    pub fn fill_threshold_into(
306        &self,
307        src_vid: u32,
308        lower: Option<(u32, i64)>,
309        upper: Option<(u32, i64)>,
310        out: &mut Vec<Nbr>,
311    ) {
312        out.clear();
313        self.visit_threshold(src_vid, lower, upper, |nbr| {
314            out.push(nbr);
315            true
316        });
317    }
318
319    /// Visit entries whose endpoint falls in the inclusive endpoint range.
320    ///
321    /// Endpoint-only counterpart of `visit_threshold` sharing one interval
322    /// across forms. Missing groups read as empty and never create groups.
323    pub fn visit_threshold_endpoint_only<F>(
324        &self,
325        src_vid: u32,
326        lower: Option<u32>,
327        upper: Option<u32>,
328        f: F,
329    ) where
330        F: FnMut(Nbr) -> bool,
331    {
332        let Some((gid, local)) = self.route(src_vid) else {
333            return;
334        };
335        if let Some(shard) = self.shards.get(&gid) {
336            if let Some(mapped) = &shard.mapped {
337                mapped.visit_threshold(
338                    local,
339                    lower.map(|endpoint| (endpoint, i64::MIN)),
340                    upper.map(|endpoint| (endpoint, i64::MAX)),
341                    f,
342                );
343            } else {
344                shard
345                    .variant
346                    .visit_threshold_endpoint_only(local, lower, upper, f);
347            }
348        }
349    }
350
351    /// Fill a caller buffer with the same endpoint-range content.
352    pub fn fill_threshold_endpoint_only_into(
353        &self,
354        src_vid: u32,
355        lower: Option<u32>,
356        upper: Option<u32>,
357        out: &mut Vec<Nbr>,
358    ) {
359        out.clear();
360        self.visit_threshold_endpoint_only(src_vid, lower, upper, |nbr| {
361            out.push(nbr);
362            true
363        });
364    }
365}