Skip to main content

linkrs_storage/edge/
slot.rs

1//! CSR slot primitives: the hot/cold neighbor split and its assembly.
2
3use linkrs_core::types::INVALID_EDGE_ID;
4use linkrs_core::types::{EdgeId, Timestamp, VertexId};
5
6/// Hot topology half of a CSR slot: everything a traversal needs.
7///
8/// Packed `(endpoint, rank, edge_id)` without timestamp replicas. Scans that
9/// resolve visibility through the version authority (by `edge_id`) walk this
10/// half only, so the cold timestamp lines stay out of the cache. Assembled
11/// back into [`Nbr`] at API boundaries through [`Nbr::from_parts`].
12///
13/// Rank keeps full 64-bit width: it is the caller-controlled multigraph
14/// multiplicity key, shared with endpoint key packing, WAL redo records and
15/// the query layer, so the store must hold any legal input value exactly.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct HotNbr {
18    pub endpoint: u32,
19    pub rank: i64,
20    pub edge_id: EdgeId,
21}
22
23/// Cold timestamp half of a CSR slot: physical MVCC replica.
24///
25/// Only `delete_ts` is stored inline (`Timestamp::MAX` means alive).
26/// `create_ts` lives in the `EdgeTimestamps` authority and is consulted
27/// on-demand for visibility. This keeps the cold half at 8 bytes. Query
28/// paths must never decide visibility from this field alone; the version
29/// authority owns that decision. Touched only by writes, deletes, rollback,
30/// compaction and persistence assembly, never by topology scans.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct ColdStamps {
33    pub delete_ts: Timestamp,
34}
35
36impl HotNbr {
37    /// Gap-fill sentinel matching [`Nbr::dead_gap`]: unassignable edge id.
38    pub fn dead_gap() -> Self {
39        Self {
40            endpoint: 0,
41            rank: 0,
42            edge_id: INVALID_EDGE_ID,
43        }
44    }
45}
46
47impl ColdStamps {
48    /// Gap-fill sentinel matching [`Nbr::dead_gap`]: empty stamp window.
49    pub fn dead_gap() -> Self {
50        Self { delete_ts: 0 }
51    }
52
53    /// Whether the slot holds no deletion stamp.
54    #[inline]
55    pub fn is_live(&self) -> bool {
56        self.delete_ts == Timestamp::MAX
57    }
58}
59
60/// Compact CSR edge entry.
61///
62/// Logical assembly of a [`HotNbr`] topology half and a [`ColdStamps`]
63/// timestamp half. Storage layers persist the halves separately and
64/// reassemble this record at API boundaries; the exact in-memory size
65/// follows the measured struct size.
66///
67/// The `endpoint` is the internal vertex ID of the neighbor. The `rank` is the
68/// edge multiplicity index (typically 0 for simple edges).
69///
70/// `delete_ts` is the deletion timestamp (`Timestamp::MAX` means alive),
71/// maintained as physical state for row-level reclaim decisions.
72/// `create_ts` lives in the `EdgeTimestamps` authority and is consulted
73/// on-demand for MVCC visibility; it is not stored inline.
74///
75/// Topology and properties are decoupled: the CSR entry carries only the
76/// topology (endpoint, rank, edge_id, timestamps). Edge properties are
77/// stored in a separate columnar store indexed by `EdgeId` — no
78/// `prop_offset` indirection is stored per edge.
79///
80/// Use [`Nbr::to_vertex_id`] to reconstruct the full `VertexId` at the API boundary.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct Nbr {
83    pub endpoint: u32,
84    pub rank: i64,
85    pub edge_id: EdgeId,
86    pub delete_ts: Timestamp,
87}
88
89impl Nbr {
90    /// Create a new alive edge (delete_ts = MAX means "not deleted").
91    pub fn new(endpoint: u32, rank: i64, edge_id: EdgeId) -> Self {
92        Self {
93            endpoint,
94            rank,
95            edge_id,
96            delete_ts: Timestamp::MAX,
97        }
98    }
99
100    /// Create with explicit create timestamp (stored in the authority, not inline).
101    ///
102    /// The returned `Nbr` carries only the topology and `delete_ts`; the
103    /// caller must record `create_ts` in the `EdgeTimestamps` authority.
104    pub fn with_create_ts(
105        endpoint: u32,
106        rank: i64,
107        edge_id: EdgeId,
108        _create_ts: Timestamp,
109    ) -> Self {
110        Self {
111            endpoint,
112            rank,
113            edge_id,
114            delete_ts: Timestamp::MAX,
115        }
116    }
117
118    /// Create with explicit delete timestamp.
119    pub fn with_timestamps(
120        endpoint: u32,
121        rank: i64,
122        edge_id: EdgeId,
123        delete_ts: Timestamp,
124    ) -> Self {
125        Self {
126            endpoint,
127            rank,
128            edge_id,
129            delete_ts,
130        }
131    }
132
133    /// Gap-fill sentinel for rebuilt rows: never alive at any timestamp.
134    ///
135    /// Uses the unassignable edge id with an empty delete window,
136    /// so an overrun scan reports absence instead of a ghost live edge. This
137    /// is the only sanctioned filler for reserved primary slots; every CSR
138    /// shape shares it.
139    pub fn dead_gap() -> Self {
140        Self {
141            endpoint: 0,
142            rank: 0,
143            edge_id: INVALID_EDGE_ID,
144            delete_ts: 0,
145        }
146    }
147
148    /// Check if this edge is physically reclaimable at the given cutoff.
149    ///
150    /// Physical reclaim probe only: compares the projected `delete_ts` replica
151    /// without the authority `create_ts`. Query visibility must go through the
152    /// version authority
153    /// ([`crate::mvcc_visibility::Visibility::is_edge_visible`]); using this
154    /// probe for queries would fork a second visibility decision that drifts
155    /// from the authority. Visible to the edge subtree only so storage layers
156    /// outside edge storage cannot mistake it for a visibility check.
157    #[inline]
158    pub(in crate::edge) fn is_alive_at(&self, ts: Timestamp) -> bool {
159        ts < self.delete_ts
160    }
161
162    /// Split the record into its hot topology half.
163    #[inline]
164    pub fn hot(&self) -> HotNbr {
165        HotNbr {
166            endpoint: self.endpoint,
167            rank: self.rank,
168            edge_id: self.edge_id,
169        }
170    }
171
172    /// Split the record into its cold timestamp half.
173    #[inline]
174    pub fn cold(&self) -> ColdStamps {
175        ColdStamps {
176            delete_ts: self.delete_ts,
177        }
178    }
179
180    /// Reassemble a record from its halves.
181    #[inline]
182    pub fn from_parts(hot: HotNbr, cold: ColdStamps) -> Self {
183        Self {
184            endpoint: hot.endpoint,
185            rank: hot.rank,
186            edge_id: hot.edge_id,
187            delete_ts: cold.delete_ts,
188        }
189    }
190
191    /// Reconstruct the full `VertexId` from the packed `(endpoint, rank)` pair.
192    ///
193    /// The result is a 16-byte VertexId encoding `(endpoint as i64, rank)` in
194    /// big-endian, matching the format produced by
195    /// `EdgeTable::edge_endpoint_key`.
196    #[inline]
197    pub fn to_vertex_id(&self) -> VertexId {
198        VertexId::edge_endpoint_key(self.endpoint, self.rank)
199    }
200}