linkrs_storage/edge/edge_table/
iterator.rs1use std::collections::HashSet;
2use std::sync::Arc;
3
4use linkrs_core::types::Timestamp;
5
6use super::core::EdgeStore;
7use super::stats::ScanPruneReport;
8use crate::cursor::ScanPredicate;
9use crate::edge::node_group::ShardCsrIterator;
10use crate::edge::{EdgeRecord, Nbr};
11
12pub const DEFAULT_ADJACENCY_BATCH: usize = 256;
14
15pub struct AdjacencyBatchAccessor<'a> {
24 table: &'a EdgeStore,
25 outgoing: bool,
26 ts: Timestamp,
27}
28
29impl<'a> AdjacencyBatchAccessor<'a> {
30 pub fn new(table: &'a EdgeStore, outgoing: bool, ts: Timestamp) -> Self {
31 Self {
32 table,
33 outgoing,
34 ts,
35 }
36 }
37
38 fn csr(&self) -> &crate::edge::node_group::CsrShardSet {
39 if self.outgoing {
40 &self.table.out_csr
41 } else {
42 &self.table.in_csr
43 }
44 }
45
46 pub fn fill_into(&self, src: u32, out: &mut Vec<Nbr>) {
51 out.clear();
52 if !self.table.is_direction_available(self.outgoing) {
53 return;
54 }
55 self.table.fill_visible_into(self.csr(), src, self.ts, out);
56 }
57
58 pub fn fill_limited(&self, src: u32, out: &mut Vec<Nbr>, limit: usize) {
63 out.clear();
64 if limit == 0 || !self.table.is_direction_available(self.outgoing) {
65 return;
66 }
67 out.reserve(limit.min(32));
68 let table = self.table;
69 let ts = self.ts;
70 self.csr().visit_physical(src, |nbr| {
71 if table.is_visible(nbr.edge_id, ts) {
72 out.push(nbr);
73 out.len() < limit
74 } else {
75 true
76 }
77 });
78 }
79
80 pub fn visit_batched<F>(&self, src: u32, scratch: &mut Vec<Nbr>, batch_size: usize, mut f: F)
85 where
86 F: FnMut(&[Nbr]) -> bool,
87 {
88 let batch_size = if batch_size == 0 {
89 DEFAULT_ADJACENCY_BATCH
90 } else {
91 batch_size
92 };
93 scratch.clear();
94 if !self.table.is_direction_available(self.outgoing) {
95 return;
96 }
97 let table = self.table;
98 let ts = self.ts;
99 let mut done = false;
100 self.csr().visit_physical(src, |nbr| {
101 if done {
102 return false;
103 }
104 if table.is_visible(nbr.edge_id, ts) {
105 scratch.push(nbr);
106 if scratch.len() >= batch_size {
107 if !f(scratch.as_slice()) {
108 done = true;
109 return false;
110 }
111 scratch.clear();
112 }
113 }
114 true
115 });
116 if !done && !scratch.is_empty() {
117 f(scratch.as_slice());
118 scratch.clear();
119 }
120 }
121
122 pub fn lookup(&self, src: u32, dst: u32, rank: i64) -> Option<Nbr> {
124 if !self.table.is_direction_available(self.outgoing) {
125 return None;
126 }
127 let csr = self.csr();
128 let ts = self.ts;
129 let table = self.table;
130 let mut found = None;
131 csr.visit_physical(src, |nbr| {
132 if nbr.endpoint == dst && nbr.rank == rank && table.is_visible(nbr.edge_id, ts) {
133 found = Some(nbr);
134 false
135 } else {
136 true
137 }
138 });
139 found
140 }
141
142 pub fn fill_many_into(&self, srcs: &[u32], out: &mut Vec<Nbr>, scratch: &mut Vec<Nbr>) {
149 out.clear();
150 if !self.table.is_direction_available(self.outgoing) {
151 return;
152 }
153 let csr = self.csr();
154 for src in srcs {
155 self.table.fill_visible_into(csr, *src, self.ts, scratch);
156 out.extend(scratch.iter().copied());
157 }
158 }
159
160 pub fn fill_many_with_values_into(
168 &self,
169 srcs: &[u32],
170 out: &mut Vec<(Nbr, Option<u64>)>,
171 scratch: &mut Vec<(Nbr, Option<u64>)>,
172 ) {
173 out.clear();
174 if !self.table.is_direction_available(self.outgoing) {
175 return;
176 }
177 let csr = self.csr();
178 for src in srcs {
179 self.table
180 .fill_visible_with_values_into(csr, *src, self.ts, scratch);
181 out.extend(scratch.iter().copied());
182 }
183 }
184}
185
186pub struct EdgeTableScanIterator<'a> {
200 table: &'a EdgeStore,
201 inner: ShardCsrIterator<'a>,
202 outgoing: bool,
203 ts: Timestamp,
204 max_records: Option<usize>,
206 current_count: usize,
208 projection: Option<Vec<Arc<str>>>,
209 predicates: Vec<ScanPredicate>,
210 pruned_groups: HashSet<usize>,
211 segments_total: usize,
212 segments_pruned: usize,
213 rows_scanned: usize,
214 rows_filtered: usize,
215 last_row: Option<u32>,
219 last_row_pruned: bool,
220}
221
222impl<'a> EdgeTableScanIterator<'a> {
223 pub fn new(table: &'a EdgeStore, ts: Timestamp) -> Self {
224 Self::with_limit(table, ts, None)
225 }
226
227 pub fn with_projection(
228 table: &'a EdgeStore,
229 ts: Timestamp,
230 projection: Option<Vec<Arc<str>>>,
231 ) -> Self {
232 Self::with_predicates(table, ts, projection, Vec::new())
233 }
234
235 pub fn with_predicates(
243 table: &'a EdgeStore,
244 ts: Timestamp,
245 projection: Option<Vec<Arc<str>>>,
246 predicates: Vec<ScanPredicate>,
247 ) -> Self {
248 let outgoing = table.has_out_edges();
249 let shards = if outgoing {
250 &table.out_csr
251 } else {
252 &table.in_csr
253 };
254 let existing = shards.existing_group_ids();
255 let segments_total = existing.len();
256 let mut pruned_groups = HashSet::new();
257 if !predicates.is_empty() {
258 for gid in &existing {
259 if !table.segment_may_contain(*gid as u32, &predicates) {
260 pruned_groups.insert(*gid);
261 }
262 }
263 }
264 let segments_pruned = pruned_groups.len();
265 Self {
266 table,
267 inner: shards.iter_all(),
268 outgoing,
269 ts,
270 max_records: None,
271 current_count: 0,
272 projection,
273 predicates,
274 pruned_groups,
275 segments_total,
276 segments_pruned,
277 rows_scanned: 0,
278 rows_filtered: 0,
279 last_row: None,
280 last_row_pruned: false,
281 }
282 }
283
284 pub fn prune_report(&self) -> ScanPruneReport {
286 ScanPruneReport {
287 segments_total: self.segments_total,
288 segments_pruned: self.segments_pruned,
289 rows_scanned: self.rows_scanned,
290 rows_filtered: self.rows_filtered,
291 }
292 }
293
294 pub fn with_limit(table: &'a EdgeStore, ts: Timestamp, max_records: Option<usize>) -> Self {
301 let outgoing = table.has_out_edges();
305 let shards = if outgoing {
306 &table.out_csr
307 } else {
308 &table.in_csr
309 };
310 let existing = shards.existing_group_ids();
311 let segments_total = existing.len();
312 Self {
313 table,
314 inner: shards.iter_all(),
315 outgoing,
316 ts,
317 max_records,
318 current_count: 0,
319 projection: None,
320 predicates: Vec::new(),
321 pruned_groups: HashSet::new(),
322 segments_total,
323 segments_pruned: 0,
324 rows_scanned: 0,
325 rows_filtered: 0,
326 last_row: None,
327 last_row_pruned: false,
328 }
329 }
330}
331
332impl<'a> Iterator for EdgeTableScanIterator<'a> {
333 type Item = EdgeRecord;
334
335 fn next(&mut self) -> Option<Self::Item> {
336 if let Some(max) = self.max_records {
337 if self.current_count >= max {
338 return None;
339 }
340 }
341
342 for (row_vid, nbr) in self.inner.by_ref() {
343 self.rows_scanned += 1;
344 let Some(row) = row_vid.as_internal_u32() else {
345 continue;
346 };
347 let pruned = match self.last_row {
352 Some(cached) if cached == row => self.last_row_pruned,
353 _ => {
354 let shards = if self.outgoing {
355 &self.table.out_csr
356 } else {
357 &self.table.in_csr
358 };
359 let pruned = shards
360 .group_of(row)
361 .is_some_and(|gid| self.pruned_groups.contains(&gid));
362 self.last_row = Some(row);
363 self.last_row_pruned = pruned;
364 pruned
365 }
366 };
367 if pruned {
368 continue;
369 }
370 if !self.table.is_visible(nbr.edge_id, self.ts) {
373 continue;
374 }
375 if !self.predicates.is_empty()
376 && !self.table.matches_pushdown_assume_visible(
377 nbr.edge_id,
378 self.ts,
379 &self.predicates,
380 )
381 {
382 self.rows_filtered += 1;
383 continue;
384 }
385 self.current_count += 1;
386 if self.outgoing {
387 return Some(self.table.edge_record_from_nbr_projected_assume_visible(
388 row,
389 nbr,
390 self.ts,
391 self.projection.as_deref(),
392 ));
393 }
394 return Some(self.table.edge_record_from_in_nbr_assume_visible(
395 row,
396 nbr,
397 self.ts,
398 self.projection.as_deref(),
399 ));
400 }
401 None
402 }
403}