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}