Skip to main content

linkrs_storage/edge/node_group/
trait_impl.rs

1//! CSR trait adapters for the shard set.
2//!
3//! `MutableCsrTrait` routes every row-addressed mutation and read to the
4//! group owning the bound endpoint, mirroring dirt and append-log recording
5//! of the single-edge path. `CsrBase` keeps whole-direction dump/load as a
6//! fail-closed guard: persistence moves through the per-group incremental
7//! protocol only.
8
9use linkrs_core::types::{EdgeId, EdgeStrategy, Timestamp, VertexId};
10use linkrs_core::{StorageError, StorageResult};
11
12use super::super::{csr_shared::decode_endpoint_pair, CsrBase, EdgePosition, MutableCsrTrait, Nbr};
13use super::{local_vid, CsrShardSet};
14
15impl CsrBase for CsrShardSet {
16    /// Materialized rows only: existing groups times group size.
17    ///
18    /// Memory-proportional by design, so sparse tables stay proportional to
19    /// materialized groups. Not an address upper bound: holes are excluded,
20    /// use `address_span_rows` when a true bound is needed.
21    fn vertex_capacity(&self) -> usize {
22        self.shards.len() * self.group_size()
23    }
24
25    fn edge_count(&self) -> u64 {
26        self.shards
27            .values()
28            .map(|shard| shard.variant.edge_count())
29            .sum()
30    }
31
32    /// Whole-direction dump is rejected by design: persistence moves through
33    /// the per-group incremental protocol only, so a whole-direction payload
34    /// would silently bypass group dirt, append sidecars and shard manifests.
35    /// Any call here is a caller bug and fails loudly instead of returning
36    /// an empty payload that a reader could mistake for an empty table.
37    fn dump(&self) -> Vec<u8> {
38        panic!("whole-direction shard dump removed: use per-group incremental protocol");
39    }
40
41    fn dump_into(&self, _out: &mut Vec<u8>) {
42        panic!("whole-direction shard dump removed: use per-group incremental protocol");
43    }
44
45    /// Whole-direction load is rejected by design, mirroring the dump guard
46    /// above. Group payloads load through the per-group path instead.
47    fn load(&mut self, _data: &[u8]) -> StorageResult<()> {
48        Err(StorageError::deserialize_error(
49            "whole-direction shard dump removed: use per-group incremental protocol".to_string(),
50        ))
51    }
52}
53
54fn no_edges_error() -> StorageError {
55    StorageError::invalid_operation(super::super::NO_EDGES_STORED_MSG.to_string())
56}
57
58impl MutableCsrTrait for CsrShardSet {
59    fn insert_edge(
60        &mut self,
61        src_vid: u32,
62        dst: VertexId,
63        edge_id: EdgeId,
64        ts: Timestamp,
65    ) -> StorageResult<()> {
66        let gid = self.ensure_group_for(src_vid)?;
67        let local = local_vid(src_vid, self.group_bits);
68        self.shards
69            .get_mut(&gid)
70            .ok_or_else(|| {
71                StorageError::invalid_operation(format!("missing group {} on insert", gid))
72            })?
73            .variant
74            .insert_edge(local, dst, edge_id, ts)?;
75        let (decoded_endpoint, decoded_rank) = decode_endpoint_pair(dst).ok_or_else(|| {
76            StorageError::invalid_input(format!("Malformed edge endpoint key: {}", dst))
77        })?;
78        let nbr = Nbr::with_create_ts(decoded_endpoint, decoded_rank, edge_id, ts);
79        self.mark_region_insert(gid, local);
80        self.record_append_insert(gid, local, nbr);
81        Ok(())
82    }
83
84    fn delete_edge(&mut self, src_vid: u32, edge_id: EdgeId, ts: Timestamp) -> StorageResult<bool> {
85        // Missing groups read as empty, matching the routed read contract:
86        // a sparse hole holds no row, so the edge is simply absent. Only a
87        // no-edge table rejects the call, mirroring the placeholder variant.
88        let Some((gid, local)) = self.route(src_vid) else {
89            if self.strategy == EdgeStrategy::None {
90                return Err(no_edges_error());
91            }
92            return Ok(false);
93        };
94        let deleted = self
95            .shards
96            .get_mut(&gid)
97            .ok_or_else(no_edges_error)?
98            .variant
99            .delete_edge(local, edge_id, ts)?;
100        if deleted {
101            self.mark_region_delete(gid, local);
102            self.record_append_delete(gid, local, edge_id, ts);
103            if let Some(shard) = self.shards.get_mut(&gid) {
104                shard.reclaim_hint = true;
105                shard.dead_entries = shard.dead_entries.saturating_add(1);
106            }
107        }
108        Ok(deleted)
109    }
110
111    fn delete_edge_by_dst(&mut self, src_vid: u32, dst: VertexId, ts: Timestamp) -> usize {
112        let mut noop = |_: EdgeId| {};
113        self.delete_edge_by_dst_reporting(src_vid, dst, ts, &mut noop)
114    }
115
116    fn delete_edge_by_dst_reporting(
117        &mut self,
118        src_vid: u32,
119        dst: VertexId,
120        ts: Timestamp,
121        on_deleted: &mut dyn FnMut(EdgeId),
122    ) -> usize {
123        // Counting deletes stay silent on missing groups and read-only
124        // variants: a zero return means either no match or no write. Audited
125        // production deletes go through the table layer, which rejects frozen
126        // groups explicitly so a refused write never reads as a miss.
127        let Some((gid, local)) = self.route(src_vid) else {
128            return 0;
129        };
130        // Single pass: the variant stamps matches and reports their ids
131        // through the callback. Ids are parked locally so the append log is
132        // fed after the shard borrow ends, with no separate collection scan
133        // over the row first.
134        let mut doomed: Vec<EdgeId> = Vec::new();
135        let deleted = self
136            .shards
137            .get_mut(&gid)
138            .map(|shard| {
139                shard.variant.delete_edge_by_dst_reporting(
140                    local,
141                    dst,
142                    ts,
143                    &mut |edge_id: EdgeId| {
144                        on_deleted(edge_id);
145                        doomed.push(edge_id);
146                    },
147                )
148            })
149            .unwrap_or(0);
150        if deleted > 0 {
151            self.mark_region_delete(gid, local);
152            for edge_id in doomed {
153                self.record_append_delete(gid, local, edge_id, ts);
154            }
155            if let Some(shard) = self.shards.get_mut(&gid) {
156                shard.reclaim_hint = true;
157                shard.dead_entries = shard.dead_entries.saturating_add(deleted);
158            }
159        }
160        deleted
161    }
162
163    fn delete_edge_by_dst_reporting_positioned(
164        &mut self,
165        src_vid: u32,
166        dst: VertexId,
167        ts: Timestamp,
168        on_deleted: &mut dyn FnMut(EdgeId, Option<EdgePosition>),
169    ) -> usize {
170        let Some((gid, local)) = self.route(src_vid) else {
171            return 0;
172        };
173        let mut doomed: Vec<(EdgeId, Option<EdgePosition>)> = Vec::new();
174        let deleted = self
175            .shards
176            .get_mut(&gid)
177            .map(|shard| {
178                shard.variant.delete_edge_by_dst_reporting_positioned(
179                    local,
180                    dst,
181                    ts,
182                    &mut |edge_id: EdgeId, position: Option<EdgePosition>| {
183                        on_deleted(edge_id, position);
184                        doomed.push((edge_id, position));
185                    },
186                )
187            })
188            .unwrap_or(0);
189        if deleted > 0 {
190            self.mark_region_delete(gid, local);
191            for (edge_id, _) in doomed {
192                self.record_append_delete(gid, local, edge_id, ts);
193            }
194            if let Some(shard) = self.shards.get_mut(&gid) {
195                shard.reclaim_hint = true;
196                shard.dead_entries = shard.dead_entries.saturating_add(deleted);
197            }
198        }
199        deleted
200    }
201
202    fn locate_edge(&self, src_vid: u32, edge_id: EdgeId) -> Option<(EdgePosition, Nbr)> {
203        let (gid, local) = self.route(src_vid)?;
204        let shard = self.shards.get(&gid)?;
205        if let Some(mapped) = &shard.mapped {
206            return mapped.locate_edge(local, edge_id);
207        }
208        shard.variant.locate_edge(local, edge_id)
209    }
210
211    fn delete_edge_at_position(
212        &mut self,
213        src_vid: u32,
214        position: EdgePosition,
215        expected: EdgeId,
216        ts: Timestamp,
217    ) -> StorageResult<bool> {
218        // Same missing-group rule as `delete_edge`: sparse holes miss with
219        // `Ok(false)`, no-edge tables fail closed like the placeholder form.
220        let Some((gid, local)) = self.route(src_vid) else {
221            if self.strategy == EdgeStrategy::None {
222                return Err(no_edges_error());
223            }
224            return Ok(false);
225        };
226        let deleted = self
227            .shards
228            .get_mut(&gid)
229            .ok_or_else(no_edges_error)?
230            .variant
231            .delete_edge_at_position(local, position, expected, ts)?;
232        if deleted {
233            self.mark_region_delete(gid, local);
234            self.record_append_delete(gid, local, expected, ts);
235            if let Some(shard) = self.shards.get_mut(&gid) {
236                shard.reclaim_hint = true;
237                shard.dead_entries = shard.dead_entries.saturating_add(1);
238            }
239        }
240        Ok(deleted)
241    }
242
243    fn revert_delete_at_position(
244        &mut self,
245        src_vid: u32,
246        position: EdgePosition,
247        expected: EdgeId,
248        ts: Timestamp,
249    ) -> bool {
250        let Some((gid, local)) = self.route(src_vid) else {
251            return false;
252        };
253        let reverted = self
254            .shards
255            .get_mut(&gid)
256            .map(|shard| {
257                shard
258                    .variant
259                    .revert_delete_at_position(local, position, expected, ts)
260            })
261            .unwrap_or(false);
262        if reverted {
263            if let Some(shard) = self.shards.get_mut(&gid) {
264                shard.dead_entries = shard.dead_entries.saturating_sub(1);
265                if shard.dead_entries == 0 {
266                    shard.reclaim_hint = false;
267                }
268            }
269        }
270        reverted
271    }
272
273    fn delete_edge_by_offset(
274        &mut self,
275        src_vid: u32,
276        offset: i32,
277        ts: Timestamp,
278    ) -> StorageResult<bool> {
279        // Same missing-group rule as `delete_edge`: sparse holes miss with
280        // `Ok(false)`, no-edge tables fail closed like the placeholder form.
281        let Some((gid, local)) = self.route(src_vid) else {
282            if self.strategy == EdgeStrategy::None {
283                return Err(no_edges_error());
284            }
285            return Ok(false);
286        };
287        let before = self
288            .shards
289            .get(&gid)
290            .and_then(|shard| shard.variant.nbr_at_offset(local, offset));
291        let deleted = self
292            .shards
293            .get_mut(&gid)
294            .ok_or_else(no_edges_error)?
295            .variant
296            .delete_edge_by_offset(local, offset, ts)?;
297        if deleted {
298            self.mark_region_delete(gid, local);
299            if let Some(nbr) = before {
300                self.record_append_delete(gid, local, nbr.edge_id, ts);
301            }
302            if let Some(shard) = self.shards.get_mut(&gid) {
303                shard.reclaim_hint = true;
304                shard.dead_entries = shard.dead_entries.saturating_add(1);
305            }
306        }
307        Ok(deleted)
308    }
309
310    fn nbr_at_offset(&self, src_vid: u32, offset: i32) -> Option<Nbr> {
311        let (gid, local) = self.route(src_vid)?;
312        let shard = self.shards.get(&gid)?;
313        if let Some(mapped) = &shard.mapped {
314            return mapped.nbr_at_offset(local, offset);
315        }
316        shard.variant.nbr_at_offset(local, offset)
317    }
318
319    fn get_edge_physical(&self, src_vid: u32, dst: VertexId) -> Option<Nbr> {
320        let (gid, local) = self.route(src_vid)?;
321        let shard = self.shards.get(&gid)?;
322        if let Some(mapped) = &shard.mapped {
323            return mapped.get_edge_physical(local, dst);
324        }
325        shard.variant.get_edge_physical(local, dst)
326    }
327
328    fn physical_edges_of(&self, src_vid: u32) -> Vec<Nbr> {
329        let Some((gid, local)) = self.route(src_vid) else {
330            return Vec::new();
331        };
332        self.shards
333            .get(&gid)
334            .map(|shard| {
335                if let Some(mapped) = &shard.mapped {
336                    return mapped.physical_edges_of(local);
337                }
338                shard.variant.physical_edges_of(local)
339            })
340            .unwrap_or_default()
341    }
342
343    fn fill_physical_into(&self, src_vid: u32, out: &mut Vec<Nbr>) {
344        CsrShardSet::fill_physical_into(self, src_vid, out)
345    }
346
347    fn has_physical_entries(&self, vid: u32) -> bool {
348        let Some((gid, local)) = self.route(vid) else {
349            return false;
350        };
351        self.shards.get(&gid).is_some_and(|shard| {
352            if let Some(mapped) = &shard.mapped {
353                return mapped.has_physical_entries(local);
354            }
355            shard.variant.has_physical_entries(local)
356        })
357    }
358
359    fn primary_contains(&self, src_vid: u32, edge_id: EdgeId) -> bool {
360        let Some((gid, local)) = self.route(src_vid) else {
361            return false;
362        };
363        self.shards.get(&gid).is_some_and(|shard| {
364            if let Some(mapped) = &shard.mapped {
365                return mapped.primary_contains(local, edge_id);
366            }
367            shard.variant.primary_contains(local, edge_id)
368        })
369    }
370
371    fn revert_delete_by_offset(&mut self, src_vid: u32, offset: i32, ts: Timestamp) -> bool {
372        let Some((gid, local)) = self.route(src_vid) else {
373            return false;
374        };
375        let reverted = self
376            .shards
377            .get_mut(&gid)
378            .map(|shard| shard.variant.revert_delete_by_offset(local, offset, ts))
379            .unwrap_or(false);
380        if reverted {
381            self.mark_region_delete(gid, local);
382            if let Some(shard) = self.shards.get_mut(&gid) {
383                shard.dead_entries = shard.dead_entries.saturating_sub(1);
384                if shard.dead_entries == 0 {
385                    shard.reclaim_hint = false;
386                }
387            }
388        }
389        reverted
390    }
391
392    fn rollback_insert(&mut self, src_vid: u32, edge_id: EdgeId) -> bool {
393        let Some((gid, local)) = self.route(src_vid) else {
394            return false;
395        };
396        let removed = self
397            .shards
398            .get_mut(&gid)
399            .map(|shard| shard.variant.rollback_insert(local, edge_id))
400            .unwrap_or(false);
401        if removed {
402            self.mark_region_delete(gid, local);
403        }
404        removed
405    }
406
407    fn revert_delete_by_edge_id(&mut self, src_vid: u32, edge_id: EdgeId, ts: Timestamp) -> bool {
408        let Some((gid, local)) = self.route(src_vid) else {
409            return false;
410        };
411        let reverted = self
412            .shards
413            .get_mut(&gid)
414            .map(|shard| shard.variant.revert_delete_by_edge_id(local, edge_id, ts))
415            .unwrap_or(false);
416        if reverted {
417            self.mark_region_delete(gid, local);
418            if let Some(shard) = self.shards.get_mut(&gid) {
419                shard.dead_entries = shard.dead_entries.saturating_sub(1);
420                if shard.dead_entries == 0 {
421                    shard.reclaim_hint = false;
422                }
423            }
424        }
425        reverted
426    }
427
428    fn get_edge(&self, src_vid: u32, dst: VertexId, ts: Timestamp) -> Option<Nbr> {
429        let (gid, local) = self.route(src_vid)?;
430        let shard = self.shards.get(&gid)?;
431        if let Some(mapped) = &shard.mapped {
432            return mapped.get_edge(local, dst, ts);
433        }
434        shard.variant.get_edge(local, dst, ts)
435    }
436
437    fn edges_of(&self, src_vid: u32, ts: Timestamp) -> Vec<Nbr> {
438        let Some((gid, local)) = self.route(src_vid) else {
439            return Vec::new();
440        };
441        self.shards
442            .get(&gid)
443            .map(|shard| {
444                if let Some(mapped) = &shard.mapped {
445                    return mapped.edges_of(local, ts);
446                }
447                shard.variant.edges_of(local, ts)
448            })
449            .unwrap_or_default()
450    }
451
452    fn compact_vertex_with_reporting(
453        &mut self,
454        vid: u32,
455        cutoff: Timestamp,
456        on_edge_removed: &mut dyn FnMut(EdgeId, Timestamp),
457    ) -> usize {
458        let Some((gid, local)) = self.route(vid) else {
459            return 0;
460        };
461        let removed = self
462            .shards
463            .get_mut(&gid)
464            .map(|shard| {
465                shard
466                    .variant
467                    .compact_vertex_with_reporting(local, cutoff, on_edge_removed)
468            })
469            .unwrap_or(0);
470        if removed > 0 {
471            self.mark_region_delete(gid, local);
472            if let Some(shard) = self.shards.get_mut(&gid) {
473                shard.dead_entries = shard.dead_entries.saturating_sub(removed);
474                if shard.dead_entries == 0 {
475                    shard.reclaim_hint = false;
476                }
477            }
478        }
479        removed
480    }
481
482    fn reclaimable_count(&self, vid: u32, cutoff: Timestamp) -> usize {
483        let Some((gid, local)) = self.route(vid) else {
484            return 0;
485        };
486        self.shards
487            .get(&gid)
488            .map(|shard| shard.variant.reclaimable_count(local, cutoff))
489            .unwrap_or(0)
490    }
491
492    fn vertex_needs_compact(&self, vid: u32, cutoff: Timestamp) -> bool {
493        let Some((gid, local)) = self.route(vid) else {
494            return false;
495        };
496        self.shards
497            .get(&gid)
498            .is_some_and(|shard| shard.variant.vertex_needs_compact(local, cutoff))
499    }
500
501    fn vertex_census(&self, vid: u32) -> (usize, usize, usize) {
502        let Some((gid, local)) = self.route(vid) else {
503            return (0, 0, 0);
504        };
505        self.shards
506            .get(&gid)
507            .map(|shard| shard.variant.vertex_census(local))
508            .unwrap_or((0, 0, 0))
509    }
510
511    fn vertex_reclaim_probe(&self, vid: u32, cutoff: Timestamp) -> (usize, usize) {
512        let Some((gid, local)) = self.route(vid) else {
513            return (0, 0);
514        };
515        self.shards
516            .get(&gid)
517            .map(|shard| shard.variant.vertex_reclaim_probe(local, cutoff))
518            .unwrap_or((0, 0))
519    }
520
521    fn row_gap(&self, vid: u32) -> usize {
522        let Some((gid, local)) = self.route(vid) else {
523            return 0;
524        };
525        self.shards
526            .get(&gid)
527            .map(|shard| shard.variant.row_gap(local))
528            .unwrap_or(0)
529    }
530
531    fn row_density(&self, vid: u32) -> f32 {
532        let Some((gid, local)) = self.route(vid) else {
533            return 1.0;
534        };
535        self.shards
536            .get(&gid)
537            .map(|shard| shard.variant.row_density(local))
538            .unwrap_or(1.0)
539    }
540
541    fn rebalance_row(&mut self, vid: u32) -> bool {
542        let Some((gid, local)) = self.route(vid) else {
543            return true;
544        };
545        self.shards
546            .get_mut(&gid)
547            .map(|shard| shard.variant.rebalance_row(local))
548            .unwrap_or(true)
549    }
550
551    fn used_memory_size(&self) -> usize {
552        CsrShardSet::used_memory_size(self)
553    }
554}