Skip to main content

linkrs_storage/edge/immutable_csr/
pack.rs

1use linkrs_core::types::Timestamp;
2use linkrs_core::StorageError;
3
4use super::super::{
5    BundledCsr, ColdStamps, HotNbr, MutableCsr, Nbr, PureTopologyCsr, SingleMutableCsr,
6    INVALID_EDGE_ID,
7};
8use super::ImmutableCsr;
9use bitvec::vec::BitVec;
10
11pub(crate) fn frozen_error() -> StorageError {
12    StorageError::invalid_operation(
13        "frozen CSR group rejects writes: unfreeze the group before writing".to_string(),
14    )
15}
16
17/// Frozen row order: point-query key first, then version order, then a
18/// stable total-order tiebreak. Total, so packing sorts deterministically.
19pub(crate) fn frozen_row_key(nbr: &Nbr) -> (u32, i64, u64) {
20    (nbr.endpoint, nbr.rank, nbr.edge_id.0)
21}
22
23/// Sort one packed row into frozen order and drop reserved-slot gap
24/// sentinels, which carry no edge. Tombstones sort by the same key and stay:
25/// queries filter them by timestamp inside the key range.
26fn sort_packed_row(row: &mut Vec<Nbr>) {
27    row.retain(|nbr| nbr.edge_id != INVALID_EDGE_ID);
28    row.sort_by_key(frozen_row_key);
29}
30
31/// `(endpoint, rank)` key range inside one sorted frozen row, as
32/// `(start, end)` offsets relative to the row slice. Empty when the row
33/// holds no entry with the key.
34pub(crate) fn frozen_key_range(hot: &[HotNbr], endpoint: u32, rank: i64) -> (usize, usize) {
35    let lo = hot.partition_point(|h| (h.endpoint, h.rank) < (endpoint, rank));
36    let hi = hot.partition_point(|h| (h.endpoint, h.rank) <= (endpoint, rank));
37    (lo, hi)
38}
39
40impl ImmutableCsr {
41    /// Empty table with no rows.
42    pub fn new() -> Self {
43        Self {
44            hot_entries: Vec::new(),
45            cold_entries: Vec::new(),
46            degrees: Vec::new(),
47            offsets: Vec::new(),
48            edge_count: 0,
49            values: Vec::new(),
50            valid: BitVec::new(),
51        }
52    }
53
54    /// Drop every entry, keeping no rows.
55    pub fn clear(&mut self) {
56        self.hot_entries.clear();
57        self.cold_entries.clear();
58        self.degrees.clear();
59        self.offsets.clear();
60        self.edge_count = 0;
61        self.values.clear();
62        self.valid.clear();
63    }
64
65    /// Row count of the packed table.
66    pub fn vertex_capacity(&self) -> usize {
67        self.degrees.len()
68    }
69
70    /// Packed hot halves for snapshot-file writers.
71    pub(crate) fn packed_hot(&self) -> &[HotNbr] {
72        &self.hot_entries
73    }
74
75    /// Packed cold halves for snapshot-file writers.
76    pub(crate) fn packed_cold(&self) -> &[ColdStamps] {
77        &self.cold_entries
78    }
79
80    /// Packed row degrees for snapshot-file writers.
81    pub(crate) fn packed_degrees(&self) -> &[u32] {
82        &self.degrees
83    }
84
85    /// Live edge count of the packed table.
86    pub fn edge_count(&self) -> u64 {
87        self.edge_count
88    }
89
90    /// Pack every physical entry of a mutable CSR, primary rows and overflow
91    /// chains merged per row, then sorted into frozen row order.
92    ///
93    /// Content-preserving up to logical equivalence: tombstones travel
94    /// verbatim, reserved-slot gap sentinels are dropped, and rows are sorted
95    /// by `(endpoint, rank, edge_id)`. Timestamp-filtered reads
96    /// observe the same logical entries as the source table.
97    pub fn pack_from_mutable(csr: &MutableCsr) -> Self {
98        Self::pack_from_rows(
99            csr.vertex_capacity(),
100            csr.edge_count() as usize,
101            |local, row| {
102                csr.fill_physical_into(local, row);
103            },
104        )
105    }
106
107    /// Pack every physical entry of a single-edge CSR, sorted the same way.
108    ///
109    /// Rows hold at most one entry; the packed form is uniform with the
110    /// multi-edge pack so one frozen type serves both strategies.
111    pub fn pack_single_from(csr: &SingleMutableCsr) -> Self {
112        Self::pack_from_rows(
113            csr.vertex_capacity(),
114            csr.edge_count() as usize,
115            |local, row| {
116                csr.fill_physical_into(local, row);
117            },
118        )
119    }
120
121    /// Pack a pure-topology group directly into the frozen layout.
122    ///
123    /// Same sorted content as routing through a temporary mutable table,
124    /// without the full copy and without fabricating timestamp replicas.
125    /// Pure rows carry no timestamps and no tombstones: every physically
126    /// stored entry is live, so the pack walks the topology only.
127    pub fn pack_from_pure(csr: &PureTopologyCsr) -> Self {
128        Self::pack_from_rows(
129            csr.vertex_capacity(),
130            csr.edge_count() as usize,
131            |local, row| {
132                row.clear();
133                csr.visit_physical(local, |nbr| {
134                    row.push(nbr);
135                    true
136                });
137            },
138        )
139    }
140
141    /// Pack a bundled group together with its inline values.
142    ///
143    /// Same sorted content as the topology-only pack, with the value column
144    /// carried slot-parallel in row order: a NULL slot stores a zero word
145    /// with a cleared validity bit, mirroring the bundled layout. Freezing a
146    /// valued bundled group therefore preserves its properties; unfreezing
147    /// replays them back through the regular valued insert entry.
148    pub fn pack_from_bundled(csr: &BundledCsr) -> Self {
149        Self::pack_valued_rows(
150            csr.vertex_capacity(),
151            csr.edge_count() as usize,
152            |local, row, vals| {
153                row.clear();
154                vals.clear();
155                csr.visit_physical_with_values(local, |nbr, value| {
156                    row.push(nbr);
157                    vals.push(value);
158                    true
159                });
160            },
161        )
162    }
163
164    /// Shared pack over a row-fill closure: extract, sort, append, count.
165    ///
166    /// Single packing entry so the multi-edge and single-edge packs cannot
167    /// drift apart; only the row source differs.
168    fn pack_from_rows(
169        rows: usize,
170        edge_hint: usize,
171        mut fill_row: impl FnMut(u32, &mut Vec<Nbr>),
172    ) -> Self {
173        let mut hot_entries = Vec::with_capacity(edge_hint);
174        let mut cold_entries = Vec::with_capacity(edge_hint);
175        let mut degrees = Vec::with_capacity(rows);
176        let mut row_buf = Vec::new();
177        let mut live = 0u64;
178        for local in 0..rows {
179            fill_row(local as u32, &mut row_buf);
180            sort_packed_row(&mut row_buf);
181            degrees.push(row_buf.len() as u32);
182            for nbr in &row_buf {
183                if nbr.edge_id != INVALID_EDGE_ID && nbr.delete_ts == Timestamp::MAX {
184                    live += 1;
185                }
186                hot_entries.push(nbr.hot());
187                cold_entries.push(nbr.cold());
188            }
189        }
190        let mut packed = Self {
191            hot_entries,
192            cold_entries,
193            degrees,
194            offsets: Vec::with_capacity(rows),
195            edge_count: live,
196            values: Vec::new(),
197            valid: BitVec::new(),
198        };
199        packed.rebuild_offsets();
200        packed
201    }
202
203    /// Shared pack over a row-fill closure carrying inline values.
204    ///
205    /// Valued counterpart of [`Self::pack_from_rows`]: each row's entries
206    /// sort together with their values by the same frozen key, so the value
207    /// column stays slot-parallel to the packed halves after the reorder.
208    /// Gap sentinels are dropped with their slots; tombstones travel with
209    /// their retained words like the bundled layout.
210    fn pack_valued_rows(
211        rows: usize,
212        edge_hint: usize,
213        mut fill_row: impl FnMut(u32, &mut Vec<Nbr>, &mut Vec<Option<u64>>),
214    ) -> Self {
215        let mut hot_entries = Vec::with_capacity(edge_hint);
216        let mut cold_entries = Vec::with_capacity(edge_hint);
217        let mut values = Vec::with_capacity(edge_hint);
218        let mut valid = BitVec::new();
219        let mut degrees = Vec::with_capacity(rows);
220        let mut row_buf = Vec::new();
221        let mut val_buf = Vec::new();
222        let mut pairs = Vec::new();
223        let mut live = 0u64;
224        for local in 0..rows {
225            fill_row(local as u32, &mut row_buf, &mut val_buf);
226            pairs.clear();
227            pairs.extend(row_buf.drain(..).zip(val_buf.drain(..)));
228            pairs.retain(|(nbr, _)| nbr.edge_id != INVALID_EDGE_ID);
229            pairs.sort_by_key(|(nbr, _)| frozen_row_key(nbr));
230            degrees.push(pairs.len() as u32);
231            for (nbr, value) in &pairs {
232                if nbr.edge_id != INVALID_EDGE_ID && nbr.delete_ts == Timestamp::MAX {
233                    live += 1;
234                }
235                hot_entries.push(nbr.hot());
236                cold_entries.push(nbr.cold());
237                values.push(value.unwrap_or(0));
238                valid.push(value.is_some());
239            }
240        }
241        let mut packed = Self {
242            hot_entries,
243            cold_entries,
244            degrees,
245            offsets: Vec::with_capacity(rows),
246            edge_count: live,
247            values,
248            valid,
249        };
250        packed.rebuild_offsets();
251        packed
252    }
253
254    pub(crate) fn rebuild_offsets(&mut self) {
255        self.offsets.clear();
256        self.offsets.reserve(self.degrees.len());
257        let mut base = 0u32;
258        for degree in &self.degrees {
259            self.offsets.push(base);
260            base = base.saturating_add(*degree);
261        }
262    }
263}