Skip to main content

linkrs_storage/edge/
csr_trait.rs

1//! CSR Trait Definitions
2//!
3//! Unified trait interface for different CSR implementations.
4//! Supports runtime polymorphism for edge storage selection.
5//!
6//! Vertex lifecycle is owned above this layer: vertex tables tombstone
7//! vertex records and propagate survivors through edge remapping, so this
8//! trait exposes no vertex-scoped bulk delete. Incident-edge cleanup
9//! composes the row-scoped deletes below with the table-level reclaim and
10//! remap passes.
11//!
12//! Row sort contract: mutable rows sort by `(endpoint, rank, edge_id)` on
13//! the maintenance path only. Sorting moves slots, so every previously
14//! issued `EdgePosition` for the row goes stale like after a compaction or
15//! rebalance, and callers relocate through the edge-id key first. The
16//! primary block holds the sorted prefix while overflow chunks stay in
17//! insertion order as the unsorted suffix; new writes fill primary gaps or
18//! spill to the overflow tail and therefore mark the row unsorted again as
19//! observed by `is_row_sorted`. No sort watermark is persisted: order is
20//! cached in a memory-resident per-row flag and the live index is rebuilt
21//! on sort and on load. Threshold scans route internally on the cached
22//! flag: sorted primaries bisect the key window, unsorted rows and every
23//! overflow suffix scan linearly.
24
25use linkrs_core::StorageResult;
26
27use super::mutable_csr::EdgePosition;
28use super::{EdgeId, Nbr, Timestamp, VertexId};
29
30pub trait CsrBase: std::fmt::Debug + Send + Sync {
31    /// Concurrency contract: row stores carry no internal locks. Concurrent
32    /// reads are safe while no writer holds exclusive access, but any
33    /// concurrent mutation requires caller-side mutual exclusion. The
34    /// `Send + Sync` bounds express single-writer transfer across threads,
35    /// not lock-free concurrent writes. Multi-core scaling comes from
36    /// shard-level parallelism over `NodeGroup` shards above this trait;
37    /// the CSR lock model itself stays single-writer with no row locks.
38    fn vertex_capacity(&self) -> usize;
39
40    fn edge_count(&self) -> u64;
41
42    fn dump(&self) -> Vec<u8>;
43
44    fn load(&mut self, data: &[u8]) -> StorageResult<()>;
45
46    /// Append the same bytes as `dump` into `out` without an intermediate
47    /// owned buffer, so checkpoint writes borrow the live topology instead
48    /// of cloning it for serialization.
49    fn dump_into(&self, out: &mut Vec<u8>) {
50        out.extend_from_slice(&self.dump());
51    }
52}
53
54pub trait MutableCsrTrait: CsrBase {
55    /// Insert an edge.
56    ///
57    /// Topology and properties are decoupled: the CSR stores only the
58    /// topology (neighbor, edge_id, timestamps). Properties are stored
59    /// separately indexed by `EdgeId`.
60    ///
61    /// Physical uniqueness only, without timestamp ordering checks. Snapshot
62    /// visibility is decided by the version authority above this layer.
63    ///
64    /// Returns `Ok(())` on success, or an error explaining why insertion failed:
65    ///
66    /// - `MutableCsr`: checks for duplicate live (neighbor) keys across primary and overflow,
67    ///   writes to primary if space available, otherwise spills to overflow with auto-expansion.
68    ///   Returns `EdgeAlreadyExists` on duplicate.
69    /// - `SingleMutableCsr`: rejects a second live edge in an occupied slot
70    ///   with `Conflict`, matching the table-layer Single contract. A
71    ///   tombstoned slot accepts a rebuild at any timestamp. No silent
72    ///   overwrite exists.
73    fn insert_edge(
74        &mut self,
75        src_vid: u32,
76        dst: VertexId,
77        edge_id: EdgeId,
78        ts: Timestamp,
79    ) -> StorageResult<()>;
80
81    /// Delete an edge by edge_id.
82    ///
83    /// Returns:
84    /// - `Ok(true)` when the edge was deleted.
85    /// - `Ok(false)` when the edge does not exist or cannot be deleted at `ts`
86    ///   (e.g. `create_ts > ts`), or when it was already deleted at exactly
87    ///   `ts` (idempotent re-delete).
88    /// - `Err(StorageError::write_write_conflict)` when the edge already exists
89    ///   but was deleted at a **different** timestamp.
90    ///
91    /// - `MutableCsr`: uses `edge_id` to locate and delete the specific edge.
92    /// - `SingleMutableCsr`: the stored edge id must match exactly. No wildcard
93    ///   edge id is supported; callers pass the precise id or use endpoint
94    ///   addressing.
95    fn delete_edge(&mut self, src_vid: u32, edge_id: EdgeId, ts: Timestamp) -> StorageResult<bool>;
96
97    /// Delete all edges matching (src, dst) with full-match semantics.
98    ///
99    /// One call deletes every live match; no first-only variant exists.
100    /// Returns the deleted count so table rollback can reconcile by count and
101    /// tombstone metrics can observe multi-match rows.
102    ///
103    /// - `MutableCsr`: scans primary and overflow, deletes **all** matching edges.
104    /// - `SingleMutableCsr`: deletes the single edge if dst matches (0 or 1).
105    fn delete_edge_by_dst(&mut self, src_vid: u32, dst: VertexId, ts: Timestamp) -> usize;
106
107    /// Delete all edges matching (src, dst), reporting every stamped id.
108    ///
109    /// Same single-pass semantics as `delete_edge_by_dst`, plus one
110    /// `on_deleted` call per stamped edge so callers needing the ids (append
111    /// logs, audits) skip their own collection scan. The default routes
112    /// through `delete_edge_by_dst` and reports nothing; row stores override
113    /// it with an in-place reporting pass. A zero return covers both no match
114    /// and read-only refusal without invoking the callback; table write paths
115    /// reject frozen groups before reaching here so the two cases never merge
116    /// into one silent miss.
117    fn delete_edge_by_dst_reporting(
118        &mut self,
119        src_vid: u32,
120        dst: VertexId,
121        ts: Timestamp,
122        _on_deleted: &mut dyn FnMut(EdgeId),
123    ) -> usize {
124        self.delete_edge_by_dst(src_vid, dst, ts)
125    }
126
127    /// Delete all edges matching (src, dst), reporting each stamped id with
128    /// its row position.
129    ///
130    /// Same single-pass semantics as `delete_edge_by_dst_reporting`, plus the
131    /// position of each stamped entry so the caller can revert or re-delete
132    /// the exact slot without rescanning the row. Positions are row-local and
133    /// valid only until the next compaction, rebalance, repack or removal of
134    /// the row; every positional write revalidates the edge id. Stores
135    /// without positional addressing report `None` and the caller falls back
136    /// to the edge-id path. The default adapts the id-only pass.
137    fn delete_edge_by_dst_reporting_positioned(
138        &mut self,
139        src_vid: u32,
140        dst: VertexId,
141        ts: Timestamp,
142        on_deleted: &mut dyn FnMut(EdgeId, Option<EdgePosition>),
143    ) -> usize {
144        let mut count = 0usize;
145        let deleted = self.delete_edge_by_dst_reporting(src_vid, dst, ts, &mut |edge_id| {
146            on_deleted(edge_id, None);
147            count += 1;
148        });
149        let _ = count;
150        deleted
151    }
152
153    /// Locate one stored edge, returning its row position and copy.
154    ///
155    /// The default reports no position; row stores override it with a single
156    /// scan shared by delete and revert callers.
157    fn locate_edge(&self, _src_vid: u32, _edge_id: EdgeId) -> Option<(EdgePosition, Nbr)> {
158        None
159    }
160
161    /// Delete the edge at `position` when it still holds `expected` id.
162    ///
163    /// Stale positions are refused with `Ok(false)`. Position-less stores
164    /// fail closed with an error so a cross-variant position can never fall
165    /// back to an id scan silently; positional stores override it with a
166    /// direct slot write.
167    fn delete_edge_at_position(
168        &mut self,
169        src_vid: u32,
170        _position: EdgePosition,
171        expected: EdgeId,
172        ts: Timestamp,
173    ) -> StorageResult<bool> {
174        let _ = (src_vid, expected, ts);
175        Err(linkrs_core::StorageError::invalid_operation(
176            "row position is not supported by this CSR form; re-resolve by edge id".to_string(),
177        ))
178    }
179
180    /// Revert the deletion at `position` when it still holds `expected` id.
181    ///
182    /// Stale positions are refused with `false`. Position-less stores report
183    /// `false` so cross-form reuse is observable instead of silently
184    /// succeeding through another path; positional stores override it.
185    fn revert_delete_at_position(
186        &mut self,
187        src_vid: u32,
188        _position: EdgePosition,
189        expected: EdgeId,
190        ts: Timestamp,
191    ) -> bool {
192        let _ = (src_vid, _position, expected, ts);
193        false
194    }
195
196    /// Delete an edge by its offset position in the primary block.
197    ///
198    /// Offset indexes into the live row degree, never into reserved capacity.
199    /// Out-of-degree offsets fail without touching any slot. Conflict errors
200    /// are propagated instead of folded into a failure value.
201    fn delete_edge_by_offset(
202        &mut self,
203        src_vid: u32,
204        offset: i32,
205        ts: Timestamp,
206    ) -> StorageResult<bool>;
207
208    /// Revert a deleted edge by its offset position.
209    ///
210    /// - `MutableCsr`: offset indexes into the primary block degree.
211    /// - `SingleMutableCsr`: only offset == 0 is valid.
212    fn revert_delete_by_offset(&mut self, src_vid: u32, offset: i32, ts: Timestamp) -> bool;
213
214    /// Read-only view of one primary slot for offset identity checks.
215    fn nbr_at_offset(&self, _src_vid: u32, _offset: i32) -> Option<Nbr> {
216        None
217    }
218
219    /// Locate the first live edge by endpoint without consulting snapshots.
220    ///
221    /// Physical addressing only; tombstoned and gap slots are skipped.
222    /// Snapshot visibility still needs the version authority above this
223    /// layer, while tombstone access uses the full physical views.
224    fn get_edge_physical(&self, _src_vid: u32, _dst: VertexId) -> Option<Nbr> {
225        None
226    }
227
228    /// Every physically stored entry of one vertex without timestamp filtering.
229    ///
230    /// Test and offline use; production scans use `fill_physical_into` or
231    /// the visitor paths instead of this allocating accessor.
232    fn physical_edges_of(&self, _src_vid: u32) -> Vec<Nbr> {
233        Vec::new()
234    }
235
236    /// Fill a caller buffer with every physically stored entry of one vertex.
237    ///
238    /// Same content as the allocating accessor above, without the per-vertex
239    /// allocation. The default forwards to the allocating accessor; row
240    /// stores override it with a direct fill.
241    fn fill_physical_into(&self, src_vid: u32, out: &mut Vec<Nbr>) {
242        out.clear();
243        out.extend_from_slice(&self.physical_edges_of(src_vid));
244    }
245
246    /// Whether one vertex holds any physically stored entry.
247    fn has_physical_entries(&self, _vid: u32) -> bool {
248        false
249    }
250
251    /// Whether the primary row of one vertex holds `edge_id`.
252    fn primary_contains(&self, _src_vid: u32, _edge_id: EdgeId) -> bool {
253        false
254    }
255
256    /// Erase one just-inserted edge for insert rollback (no tombstone trace).
257    ///
258    /// Insert-rollback only: erases the slot as if the edge never existed.
259    /// Never use for MVCC deletes, which must go through `delete_edge` so
260    /// the tombstone stays visible to the reclaim machinery. Reclaims the
261    /// slot and updates the edge count. Only implemented by writable
262    /// strategies; other strategies default to no-op.
263    fn rollback_insert(&mut self, _src_vid: u32, _edge_id: EdgeId) -> bool {
264        false
265    }
266
267    /// Revert a deletion by edge id if it happened at or before `ts`.
268    ///
269    /// Restores the entry as live. Only implemented by `MutableCsr`; other
270    /// strategies default to no-op.
271    fn revert_delete_by_edge_id(
272        &mut self,
273        _src_vid: u32,
274        _edge_id: EdgeId,
275        _ts: Timestamp,
276    ) -> bool {
277        false
278    }
279
280    /// Get a specific edge by source and destination.
281    /// Test-only row-stamp filter; production reads go through the version authority.
282    fn get_edge(&self, src_vid: u32, dst: VertexId, ts: Timestamp) -> Option<Nbr>;
283
284    /// Get all valid edges of a vertex at the given timestamp.
285    /// Test-only row-stamp filter; production reads go through the version authority.
286    /// Test and offline use; production traversals use the visitor or
287    /// caller-buffer fill paths instead of this allocating accessor.
288    fn edges_of(&self, src_vid: u32, ts: Timestamp) -> Vec<Nbr>;
289
290    /// Reclaim one vertex in place, dropping entries eligible for collection
291    /// at `cutoff` and tightening live entries without touching other rows.
292    ///
293    /// Returns the number of removed entries. Reported removals flow through
294    /// `on_edge_removed` so tombstone promotion stays centralized.
295    /// Strategies without per-row fragmentation default to no-op.
296    ///
297    /// Mutable-row entry only: production reclaim of frozen rows must go
298    /// through the group-batched entries (one linear pass for any number of
299    /// rows) instead of one call per row. Frozen implementations reject the
300    /// single-row entry; every production trigger dispatches frozen groups
301    /// to the batched path before reaching here.
302    fn compact_vertex_with_reporting(
303        &mut self,
304        _vid: u32,
305        _cutoff: Timestamp,
306        _on_edge_removed: &mut dyn FnMut(EdgeId, Timestamp),
307    ) -> usize {
308        0
309    }
310
311    /// Count entries of one vertex that are reclaimable at `cutoff`.
312    ///
313    /// The write-path trigger consults this per-vertex count instead of a
314    /// whole-table ratio, so small writes never cause large rebuilds.
315    fn reclaimable_count(&self, _vid: u32, _cutoff: Timestamp) -> usize {
316        0
317    }
318
319    /// Whether one vertex holds anything reclaimable at `cutoff`.
320    fn vertex_needs_compact(&self, vid: u32, cutoff: Timestamp) -> bool {
321        self.reclaimable_count(vid, cutoff) > 0
322    }
323
324    /// Physical entry census of one vertex: `(live, dead, capacity)`.
325    ///
326    /// Backs the per-vertex fragmentation view; strategies without rows
327    /// report zeros.
328    fn vertex_census(&self, _vid: u32) -> (usize, usize, usize) {
329        (0, 0, 0)
330    }
331
332    /// Single-walk reclaim probe of one vertex: `(dead, reclaimable)`.
333    ///
334    /// Fuses the census and eligibility walks so maintenance passes pay one
335    /// row scan instead of two. The default derives both from the separate
336    /// primitives; row stores override it with one fused walk.
337    fn vertex_reclaim_probe(&self, vid: u32, cutoff: Timestamp) -> (usize, usize) {
338        let (_, dead, _) = self.vertex_census(vid);
339        (dead, self.reclaimable_count(vid, cutoff))
340    }
341
342    /// Reserved primary slots minus live primary entries of one row.
343    ///
344    /// Steady-state write gaps; strategies without rows report zero.
345    fn row_gap(&self, _vid: u32) -> usize {
346        0
347    }
348
349    /// Live primary entries per unit of reserved primary capacity.
350    ///
351    /// Strategies without rows report full density.
352    fn row_density(&self, _vid: u32) -> f32 {
353        1.0
354    }
355
356    /// Rebalance one row in place, pulling overflow entries into primary
357    /// gaps and repacking leftover overflow. Returns true when the row no
358    /// longer holds overflow. Strategies without rows report true.
359    fn rebalance_row(&mut self, _vid: u32) -> bool {
360        true
361    }
362
363    /// Return the approximate memory usage in bytes.
364    fn used_memory_size(&self) -> usize;
365}
366
367#[cfg(test)]
368mod tests {}