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}