1use std::collections::HashMap;
10use std::mem::size_of;
11
12use nodedb_mem::EngineId;
13
14use super::index::CsrIndex;
15use crate::GraphError;
16
17const RKYV_MAGIC: &[u8; 6] = b"RKCS2\0";
19pub const CSR_FORMAT_VERSION: u8 = 2;
26
27#[derive(Debug, thiserror::Error)]
29#[non_exhaustive]
30pub enum CsrCheckpointError {
31 #[error("unsupported CSR checkpoint version {found}; expected {expected}")]
32 UnsupportedVersion { found: u8, expected: u8 },
33 #[error("CSR checkpoint rkyv deserialization failed")]
34 RkyvDeserialize,
35}
36
37#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
39struct CsrSnapshotRkyv {
40 nodes: Vec<String>,
41 labels: Vec<String>,
42 collections: Vec<String>,
43 out_offsets: Vec<u32>,
44 out_targets: Vec<u32>,
45 out_labels: Vec<u32>,
46 out_collections: Vec<u32>,
47 in_offsets: Vec<u32>,
48 in_targets: Vec<u32>,
49 in_labels: Vec<u32>,
50 in_collections: Vec<u32>,
51 buffer_out: Vec<Vec<(u32, u32)>>,
52 buffer_in: Vec<Vec<(u32, u32)>>,
53 buffer_out_collections: Vec<Vec<u32>>,
54 buffer_in_collections: Vec<Vec<u32>>,
55 deleted: Vec<(u32, u32, u32, u32)>,
60 has_weights: bool,
61 out_weights: Option<Vec<f64>>,
62 in_weights: Option<Vec<f64>>,
63 buffer_out_weights: Vec<Vec<f64>>,
64 buffer_in_weights: Vec<Vec<f64>>,
65}
66
67impl CsrIndex {
68 pub fn checkpoint_to_bytes(&self) -> Result<Vec<u8>, GraphError> {
75 let snapshot = CsrSnapshotRkyv {
76 nodes: self.id_to_node.clone(),
77 labels: self.id_to_label.clone(),
78 collections: self.id_to_collection.clone(),
79 out_offsets: self.out_offsets.clone(),
80 out_targets: self.out_targets.to_vec(),
81 out_labels: self.out_labels.to_vec(),
82 out_collections: self.out_collections.clone(),
83 in_offsets: self.in_offsets.clone(),
84 in_targets: self.in_targets.to_vec(),
85 in_labels: self.in_labels.to_vec(),
86 in_collections: self.in_collections.clone(),
87 buffer_out: self.buffer_out.clone(),
88 buffer_in: self.buffer_in.clone(),
89 buffer_out_collections: self.buffer_out_collections.clone(),
90 buffer_in_collections: self.buffer_in_collections.clone(),
91 deleted: self.deleted_edges.iter().copied().collect(),
92 has_weights: self.has_weights,
93 out_weights: self.out_weights.as_ref().map(|w| w.to_vec()),
94 in_weights: self.in_weights.as_ref().map(|w| w.to_vec()),
95 buffer_out_weights: self.buffer_out_weights.clone(),
96 buffer_in_weights: self.buffer_in_weights.clone(),
97 };
98 let rkyv_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&snapshot)
99 .expect("CSR rkyv serialization should not fail");
100 let buf_capacity = RKYV_MAGIC.len() + 1 + rkyv_bytes.len();
101 let _budget_guard = self
102 .governor
103 .as_ref()
104 .map(|g| g.reserve(EngineId::Graph, buf_capacity * size_of::<u8>()))
105 .transpose()?;
106 let mut buf = Vec::with_capacity(buf_capacity);
107 buf.extend_from_slice(RKYV_MAGIC);
108 buf.push(CSR_FORMAT_VERSION);
109 buf.extend_from_slice(&rkyv_bytes);
110 Ok(buf)
111 }
112
113 pub fn from_checkpoint(bytes: &[u8]) -> Result<Option<Self>, CsrCheckpointError> {
122 let header_len = RKYV_MAGIC.len() + 1; if bytes.len() > header_len && &bytes[..RKYV_MAGIC.len()] == RKYV_MAGIC {
124 let version = bytes[RKYV_MAGIC.len()];
125 if version != CSR_FORMAT_VERSION {
126 return Err(CsrCheckpointError::UnsupportedVersion {
127 found: version,
128 expected: CSR_FORMAT_VERSION,
129 });
130 }
131 return Ok(Self::from_rkyv_checkpoint(&bytes[header_len..]));
132 }
133 Ok(None)
134 }
135
136 fn from_rkyv_checkpoint(bytes: &[u8]) -> Option<Self> {
143 let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(bytes.len());
144 aligned.extend_from_slice(bytes);
145
146 #[cfg(target_endian = "little")]
147 {
148 Self::from_rkyv_zero_copy(aligned)
149 }
150 #[cfg(not(target_endian = "little"))]
151 {
152 let snap: CsrSnapshotRkyv =
153 rkyv::from_bytes::<CsrSnapshotRkyv, rkyv::rancor::Error>(&aligned).ok()?;
154 Some(Self::from_snapshot_fields(snap))
155 }
156 }
157
158 #[cfg(target_endian = "little")]
165 fn from_rkyv_zero_copy(aligned: rkyv::util::AlignedVec) -> Option<Self> {
166 use super::dense_array::DenseArray;
167
168 let backing = std::sync::Arc::new(aligned);
169
170 let archived =
172 rkyv::access::<rkyv::Archived<CsrSnapshotRkyv>, rkyv::rancor::Error>(&backing).ok()?;
173
174 let out_targets = unsafe {
176 let s = archived.out_targets.as_slice();
177 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
178 };
179 let out_labels = unsafe {
180 let s = archived.out_labels.as_slice();
181 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
182 };
183 let in_targets = unsafe {
184 let s = archived.in_targets.as_slice();
185 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
186 };
187 let in_labels = unsafe {
188 let s = archived.in_labels.as_slice();
189 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
190 };
191 let out_weights = archived.out_weights.as_ref().map(|w| unsafe {
192 let s = w.as_slice();
193 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<f64>(), s.len())
194 });
195 let in_weights = archived.in_weights.as_ref().map(|w| unsafe {
196 let s = w.as_slice();
197 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<f64>(), s.len())
198 });
199
200 let snap: CsrSnapshotRkyv =
202 rkyv::from_bytes::<CsrSnapshotRkyv, rkyv::rancor::Error>(&backing).ok()?;
203
204 let node_to_id: HashMap<String, u32> = snap
205 .nodes
206 .iter()
207 .enumerate()
208 .map(|(i, n)| (n.clone(), i as u32))
209 .collect();
210 let label_to_id: HashMap<String, u32> = snap
211 .labels
212 .iter()
213 .enumerate()
214 .map(|(i, l)| (l.clone(), i as u32))
215 .collect();
216 let node_count = snap.nodes.len();
217 let access_counts = (0..node_count).map(|_| std::cell::Cell::new(0)).collect();
218 let buffer_out_weights = if snap.buffer_out_weights.len() == node_count {
219 snap.buffer_out_weights
220 } else {
221 vec![Vec::new(); node_count]
222 };
223 let buffer_in_weights = if snap.buffer_in_weights.len() == node_count {
224 snap.buffer_in_weights
225 } else {
226 vec![Vec::new(); node_count]
227 };
228 let collection_to_id: HashMap<String, u32> = snap
229 .collections
230 .iter()
231 .enumerate()
232 .map(|(i, c)| (c.clone(), i as u32))
233 .collect();
234 let buffer_out_collections = if snap.buffer_out_collections.len() == node_count {
235 snap.buffer_out_collections
236 } else {
237 vec![Vec::new(); node_count]
238 };
239 let buffer_in_collections = if snap.buffer_in_collections.len() == node_count {
240 snap.buffer_in_collections
241 } else {
242 vec![Vec::new(); node_count]
243 };
244
245 Some(Self {
246 node_to_id,
247 id_to_node: snap.nodes,
248 label_to_id,
249 id_to_label: snap.labels,
250 collection_to_id,
251 id_to_collection: snap.collections,
252 out_offsets: snap.out_offsets,
253 out_targets,
254 out_labels,
255 out_collections: snap.out_collections,
256 out_weights,
257 in_offsets: snap.in_offsets,
258 in_targets,
259 in_labels,
260 in_collections: snap.in_collections,
261 in_weights,
262 buffer_out: snap.buffer_out,
263 buffer_in: snap.buffer_in,
264 buffer_out_weights,
265 buffer_in_weights,
266 buffer_out_collections,
267 buffer_in_collections,
268 deleted_edges: snap.deleted.into_iter().collect(),
269 has_weights: snap.has_weights,
270 node_label_bits: vec![0; node_count],
271 node_label_to_id: HashMap::new(),
272 node_label_names: Vec::new(),
273 node_surrogates: vec![0; node_count],
276 surrogate_to_local: HashMap::new(),
277 access_counts,
278 query_epoch: 0,
279 partition_tag: crate::csr::local_node_id::next_partition_tag(),
280 governor: None,
283 })
284 }
285
286 #[cfg(not(target_endian = "little"))]
288 fn from_snapshot_fields(snap: CsrSnapshotRkyv) -> Self {
289 let node_to_id: HashMap<String, u32> = snap
290 .nodes
291 .iter()
292 .enumerate()
293 .map(|(i, n)| (n.clone(), i as u32))
294 .collect();
295 let label_to_id: HashMap<String, u32> = snap
296 .labels
297 .iter()
298 .enumerate()
299 .map(|(i, l)| (l.clone(), i as u32))
300 .collect();
301
302 let node_count = snap.nodes.len();
303 let access_counts = (0..node_count).map(|_| std::cell::Cell::new(0)).collect();
304
305 let buffer_out_weights = if snap.buffer_out_weights.len() == node_count {
306 snap.buffer_out_weights
307 } else {
308 vec![Vec::new(); node_count]
309 };
310 let buffer_in_weights = if snap.buffer_in_weights.len() == node_count {
311 snap.buffer_in_weights
312 } else {
313 vec![Vec::new(); node_count]
314 };
315 let collection_to_id: HashMap<String, u32> = snap
316 .collections
317 .iter()
318 .enumerate()
319 .map(|(i, c)| (c.clone(), i as u32))
320 .collect();
321 let buffer_out_collections = if snap.buffer_out_collections.len() == node_count {
322 snap.buffer_out_collections
323 } else {
324 vec![Vec::new(); node_count]
325 };
326 let buffer_in_collections = if snap.buffer_in_collections.len() == node_count {
327 snap.buffer_in_collections
328 } else {
329 vec![Vec::new(); node_count]
330 };
331
332 Self {
333 node_to_id,
334 id_to_node: snap.nodes,
335 label_to_id,
336 id_to_label: snap.labels,
337 collection_to_id,
338 id_to_collection: snap.collections,
339 out_offsets: snap.out_offsets,
340 out_targets: snap.out_targets.into(),
341 out_labels: snap.out_labels.into(),
342 out_collections: snap.out_collections,
343 out_weights: snap.out_weights.map(Into::into),
344 in_offsets: snap.in_offsets,
345 in_targets: snap.in_targets.into(),
346 in_labels: snap.in_labels.into(),
347 in_collections: snap.in_collections,
348 in_weights: snap.in_weights.map(Into::into),
349 buffer_out: snap.buffer_out,
350 buffer_in: snap.buffer_in,
351 buffer_out_weights,
352 buffer_in_weights,
353 buffer_out_collections,
354 buffer_in_collections,
355 deleted_edges: snap.deleted.into_iter().collect(),
356 has_weights: snap.has_weights,
357 node_label_bits: vec![0; node_count],
358 node_label_to_id: HashMap::new(),
359 node_label_names: Vec::new(),
360 node_surrogates: vec![0; node_count],
363 surrogate_to_local: HashMap::new(),
364 access_counts,
365 query_epoch: 0,
366 partition_tag: crate::csr::local_node_id::next_partition_tag(),
367 governor: None,
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::csr::index::Direction;
377
378 #[test]
379 fn checkpoint_roundtrip_unweighted() {
380 let mut csr = CsrIndex::new();
381 csr.add_edge("a", "KNOWS", "b").unwrap();
382 csr.add_edge("b", "KNOWS", "c").unwrap();
383 csr.compact().expect("no governor, cannot fail");
384
385 let bytes = csr.checkpoint_to_bytes().expect("no governor, cannot fail");
386 let restored = CsrIndex::from_checkpoint(&bytes)
387 .expect("roundtrip")
388 .unwrap();
389 assert_eq!(restored.node_count(), 3);
390 assert_eq!(restored.edge_count(), 2);
391 assert!(!restored.has_weights());
392
393 let n = restored.neighbors("a", Some("KNOWS"), Direction::Out);
394 assert_eq!(n.len(), 1);
395 assert_eq!(n[0].1, "b");
396 }
397
398 #[test]
399 fn checkpoint_roundtrip_weighted() {
400 let mut csr = CsrIndex::new();
401 csr.add_edge_weighted("a", "R", "b", 2.5).unwrap();
402 csr.add_edge_weighted("b", "R", "c", 7.0).unwrap();
403 csr.add_edge("c", "R", "d").unwrap();
404 csr.compact().expect("no governor, cannot fail");
405
406 let bytes = csr.checkpoint_to_bytes().expect("no governor, cannot fail");
407 let restored = CsrIndex::from_checkpoint(&bytes)
408 .expect("roundtrip")
409 .unwrap();
410 assert!(restored.has_weights());
411 assert_eq!(restored.edge_weight("a", "R", "b"), Some(2.5));
412 assert_eq!(restored.edge_weight("b", "R", "c"), Some(7.0));
413 assert_eq!(restored.edge_weight("c", "R", "d"), Some(1.0));
414 }
415
416 #[test]
417 fn checkpoint_roundtrip_with_buffer() {
418 let mut csr = CsrIndex::new();
419 csr.add_edge("a", "L", "b").unwrap();
420 let bytes = csr.checkpoint_to_bytes().expect("no governor, cannot fail");
422 let restored = CsrIndex::from_checkpoint(&bytes)
423 .expect("roundtrip")
424 .unwrap();
425 assert_eq!(restored.edge_count(), 1);
426 }
427
428 #[test]
429 fn golden_header_layout() {
430 let mut csr = CsrIndex::new();
431 csr.add_edge("a", "KNOWS", "b").unwrap();
432 let bytes = csr.checkpoint_to_bytes().expect("no governor, cannot fail");
433 assert_eq!(&bytes[0..6], b"RKCS2\0");
435 assert_eq!(bytes[6], super::CSR_FORMAT_VERSION);
437 assert!(bytes.len() > 7);
439 }
440
441 #[test]
442 fn version_mismatch_returns_error() {
443 let mut csr = CsrIndex::new();
444 csr.add_edge("a", "KNOWS", "b").unwrap();
445 let mut bytes = csr.checkpoint_to_bytes().expect("no governor, cannot fail");
446 bytes[6] = 0;
448 match CsrIndex::from_checkpoint(&bytes) {
449 Err(CsrCheckpointError::UnsupportedVersion { found, expected }) => {
450 assert_eq!(found, 0);
451 assert_eq!(expected, super::CSR_FORMAT_VERSION);
452 }
453 Err(other) => panic!("unexpected error: {other}"),
454 Ok(_) => panic!("expected UnsupportedVersion error, got Ok"),
455 }
456 }
457}