1use std::collections::HashMap;
10use std::mem::size_of;
11
12use nodedb_mem::ScopedMemory;
13
14use super::index::CsrIndex;
15use crate::GraphError;
16
17const RKYV_MAGIC: &[u8; 6] = b"RKCS2\0";
19pub const CSR_FORMAT_VERSION: u8 = 3;
35
36#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum CsrCheckpointError {
40 #[error("unsupported CSR checkpoint version {found}; expected {expected}")]
41 UnsupportedVersion { found: u8, expected: u8 },
42 #[error("CSR checkpoint rkyv deserialization failed")]
43 RkyvDeserialize,
44}
45
46#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
48struct CsrSnapshotRkyv {
49 nodes: Vec<String>,
50 labels: Vec<String>,
51 collections: Vec<String>,
52 out_offsets: Vec<u32>,
53 out_targets: Vec<u32>,
54 out_labels: Vec<u32>,
55 out_collections: Vec<u32>,
56 in_offsets: Vec<u32>,
57 in_targets: Vec<u32>,
58 in_labels: Vec<u32>,
59 in_collections: Vec<u32>,
60 buffer_out: Vec<Vec<(u32, u32)>>,
61 buffer_in: Vec<Vec<(u32, u32)>>,
62 buffer_out_collections: Vec<Vec<u32>>,
63 buffer_in_collections: Vec<Vec<u32>>,
64 deleted: Vec<(u32, u32, u32, u32)>,
69 node_surrogates: Vec<u32>,
73 has_weights: bool,
74 out_weights: Option<Vec<f64>>,
75 in_weights: Option<Vec<f64>>,
76 buffer_out_weights: Vec<Vec<f64>>,
77 buffer_in_weights: Vec<Vec<f64>>,
78}
79
80impl CsrIndex {
81 pub fn checkpoint_to_bytes(&self) -> Result<Vec<u8>, GraphError> {
88 let snapshot = CsrSnapshotRkyv {
89 nodes: self.id_to_node.clone(),
90 labels: self.id_to_label.clone(),
91 collections: self.id_to_collection.clone(),
92 out_offsets: self.out_offsets.clone(),
93 out_targets: self.out_targets.to_vec(),
94 out_labels: self.out_labels.to_vec(),
95 out_collections: self.out_collections.clone(),
96 in_offsets: self.in_offsets.clone(),
97 in_targets: self.in_targets.to_vec(),
98 in_labels: self.in_labels.to_vec(),
99 in_collections: self.in_collections.clone(),
100 buffer_out: self.buffer_out.clone(),
101 buffer_in: self.buffer_in.clone(),
102 buffer_out_collections: self.buffer_out_collections.clone(),
103 buffer_in_collections: self.buffer_in_collections.clone(),
104 deleted: self.deleted_edges.iter().copied().collect(),
105 node_surrogates: self.node_surrogates.clone(),
106 has_weights: self.has_weights,
107 out_weights: self.out_weights.as_ref().map(|w| w.to_vec()),
108 in_weights: self.in_weights.as_ref().map(|w| w.to_vec()),
109 buffer_out_weights: self.buffer_out_weights.clone(),
110 buffer_in_weights: self.buffer_in_weights.clone(),
111 };
112 let rkyv_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&snapshot)
113 .expect("CSR rkyv serialization should not fail");
114 let buf_capacity = RKYV_MAGIC.len() + 1 + rkyv_bytes.len();
115 let _budget_guard = self.memory.reserve(buf_capacity * size_of::<u8>())?;
116 let mut buf = Vec::with_capacity(buf_capacity);
117 buf.extend_from_slice(RKYV_MAGIC);
118 buf.push(CSR_FORMAT_VERSION);
119 buf.extend_from_slice(&rkyv_bytes);
120 Ok(buf)
121 }
122
123 pub fn from_checkpoint(
132 bytes: &[u8],
133 memory: ScopedMemory,
134 ) -> Result<Option<Self>, CsrCheckpointError> {
135 let header_len = RKYV_MAGIC.len() + 1; if bytes.len() > header_len && &bytes[..RKYV_MAGIC.len()] == RKYV_MAGIC {
137 let version = bytes[RKYV_MAGIC.len()];
138 if version != CSR_FORMAT_VERSION {
139 return Err(CsrCheckpointError::UnsupportedVersion {
140 found: version,
141 expected: CSR_FORMAT_VERSION,
142 });
143 }
144 return Ok(Self::from_rkyv_checkpoint(&bytes[header_len..], memory));
145 }
146 Ok(None)
147 }
148
149 fn from_rkyv_checkpoint(bytes: &[u8], memory: ScopedMemory) -> Option<Self> {
156 let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(bytes.len());
157 aligned.extend_from_slice(bytes);
158
159 #[cfg(target_endian = "little")]
160 {
161 Self::from_rkyv_zero_copy(aligned, memory)
162 }
163 #[cfg(not(target_endian = "little"))]
164 {
165 let snap: CsrSnapshotRkyv =
166 rkyv::from_bytes::<CsrSnapshotRkyv, rkyv::rancor::Error>(&aligned).ok()?;
167 Some(Self::from_snapshot_fields(snap, memory))
168 }
169 }
170
171 #[cfg(target_endian = "little")]
178 fn from_rkyv_zero_copy(aligned: rkyv::util::AlignedVec, memory: ScopedMemory) -> Option<Self> {
179 use super::dense_array::DenseArray;
180
181 let backing = std::sync::Arc::new(aligned);
182
183 let archived =
185 rkyv::access::<rkyv::Archived<CsrSnapshotRkyv>, rkyv::rancor::Error>(&backing).ok()?;
186
187 let out_targets = unsafe {
189 let s = archived.out_targets.as_slice();
190 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
191 };
192 let out_labels = unsafe {
193 let s = archived.out_labels.as_slice();
194 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
195 };
196 let in_targets = unsafe {
197 let s = archived.in_targets.as_slice();
198 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
199 };
200 let in_labels = unsafe {
201 let s = archived.in_labels.as_slice();
202 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<u32>(), s.len())
203 };
204 let out_weights = archived.out_weights.as_ref().map(|w| unsafe {
205 let s = w.as_slice();
206 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<f64>(), s.len())
207 });
208 let in_weights = archived.in_weights.as_ref().map(|w| unsafe {
209 let s = w.as_slice();
210 DenseArray::zero_copy(backing.clone(), s.as_ptr().cast::<f64>(), s.len())
211 });
212
213 let snap: CsrSnapshotRkyv =
215 rkyv::from_bytes::<CsrSnapshotRkyv, rkyv::rancor::Error>(&backing).ok()?;
216
217 let node_to_id: HashMap<String, u32> = snap
218 .nodes
219 .iter()
220 .enumerate()
221 .map(|(i, n)| (n.clone(), i as u32))
222 .collect();
223 let label_to_id: HashMap<String, u32> = snap
224 .labels
225 .iter()
226 .enumerate()
227 .map(|(i, l)| (l.clone(), i as u32))
228 .collect();
229 let node_count = snap.nodes.len();
230 let access_counts = (0..node_count).map(|_| std::cell::Cell::new(0)).collect();
231 let buffer_out_weights = if snap.buffer_out_weights.len() == node_count {
232 snap.buffer_out_weights
233 } else {
234 vec![Vec::new(); node_count]
235 };
236 let buffer_in_weights = if snap.buffer_in_weights.len() == node_count {
237 snap.buffer_in_weights
238 } else {
239 vec![Vec::new(); node_count]
240 };
241 let collection_to_id: HashMap<String, u32> = snap
242 .collections
243 .iter()
244 .enumerate()
245 .map(|(i, c)| (c.clone(), i as u32))
246 .collect();
247 let buffer_out_collections = if snap.buffer_out_collections.len() == node_count {
248 snap.buffer_out_collections
249 } else {
250 vec![Vec::new(); node_count]
251 };
252 let buffer_in_collections = if snap.buffer_in_collections.len() == node_count {
253 snap.buffer_in_collections
254 } else {
255 vec![Vec::new(); node_count]
256 };
257 let (node_surrogates, surrogate_to_local) =
258 Self::restore_surrogates(snap.node_surrogates, node_count);
259
260 Some(Self {
261 node_to_id,
262 id_to_node: snap.nodes,
263 label_to_id,
264 id_to_label: snap.labels,
265 collection_to_id,
266 id_to_collection: snap.collections,
267 out_offsets: snap.out_offsets,
268 out_targets,
269 out_labels,
270 out_collections: snap.out_collections,
271 out_weights,
272 in_offsets: snap.in_offsets,
273 in_targets,
274 in_labels,
275 in_collections: snap.in_collections,
276 in_weights,
277 buffer_out: snap.buffer_out,
278 buffer_in: snap.buffer_in,
279 buffer_out_weights,
280 buffer_in_weights,
281 buffer_out_collections,
282 buffer_in_collections,
283 deleted_edges: snap.deleted.into_iter().collect(),
284 has_weights: snap.has_weights,
285 node_label_bits: vec![0; node_count],
286 node_label_to_id: HashMap::new(),
287 node_label_names: Vec::new(),
288 node_surrogates,
289 surrogate_to_local,
290 access_counts,
291 query_epoch: 0,
292 partition_tag: crate::csr::local_node_id::next_partition_tag(),
293 memory,
294 rebuild_journal: None,
295 })
296 }
297
298 fn restore_surrogates(persisted: Vec<u32>, node_count: usize) -> (Vec<u32>, HashMap<u32, u32>) {
304 if persisted.len() != node_count {
305 return (vec![0; node_count], HashMap::new());
306 }
307 let mut reverse = HashMap::with_capacity(persisted.len());
308 for (local, &raw) in persisted.iter().enumerate() {
309 if raw != 0 {
310 reverse.insert(raw, local as u32);
311 }
312 }
313 (persisted, reverse)
314 }
315
316 #[cfg(not(target_endian = "little"))]
318 fn from_snapshot_fields(snap: CsrSnapshotRkyv, memory: ScopedMemory) -> Self {
319 let node_to_id: HashMap<String, u32> = snap
320 .nodes
321 .iter()
322 .enumerate()
323 .map(|(i, n)| (n.clone(), i as u32))
324 .collect();
325 let label_to_id: HashMap<String, u32> = snap
326 .labels
327 .iter()
328 .enumerate()
329 .map(|(i, l)| (l.clone(), i as u32))
330 .collect();
331
332 let node_count = snap.nodes.len();
333 let access_counts = (0..node_count).map(|_| std::cell::Cell::new(0)).collect();
334
335 let buffer_out_weights = if snap.buffer_out_weights.len() == node_count {
336 snap.buffer_out_weights
337 } else {
338 vec![Vec::new(); node_count]
339 };
340 let buffer_in_weights = if snap.buffer_in_weights.len() == node_count {
341 snap.buffer_in_weights
342 } else {
343 vec![Vec::new(); node_count]
344 };
345 let collection_to_id: HashMap<String, u32> = snap
346 .collections
347 .iter()
348 .enumerate()
349 .map(|(i, c)| (c.clone(), i as u32))
350 .collect();
351 let buffer_out_collections = if snap.buffer_out_collections.len() == node_count {
352 snap.buffer_out_collections
353 } else {
354 vec![Vec::new(); node_count]
355 };
356 let buffer_in_collections = if snap.buffer_in_collections.len() == node_count {
357 snap.buffer_in_collections
358 } else {
359 vec![Vec::new(); node_count]
360 };
361 let (node_surrogates, surrogate_to_local) =
362 Self::restore_surrogates(snap.node_surrogates, node_count);
363
364 Self {
365 node_to_id,
366 id_to_node: snap.nodes,
367 label_to_id,
368 id_to_label: snap.labels,
369 collection_to_id,
370 id_to_collection: snap.collections,
371 out_offsets: snap.out_offsets,
372 out_targets: snap.out_targets.into(),
373 out_labels: snap.out_labels.into(),
374 out_collections: snap.out_collections,
375 out_weights: snap.out_weights.map(Into::into),
376 in_offsets: snap.in_offsets,
377 in_targets: snap.in_targets.into(),
378 in_labels: snap.in_labels.into(),
379 in_collections: snap.in_collections,
380 in_weights: snap.in_weights.map(Into::into),
381 buffer_out: snap.buffer_out,
382 buffer_in: snap.buffer_in,
383 buffer_out_weights,
384 buffer_in_weights,
385 buffer_out_collections,
386 buffer_in_collections,
387 deleted_edges: snap.deleted.into_iter().collect(),
388 has_weights: snap.has_weights,
389 node_label_bits: vec![0; node_count],
390 node_label_to_id: HashMap::new(),
391 node_label_names: Vec::new(),
392 node_surrogates,
393 surrogate_to_local,
394 access_counts,
395 query_epoch: 0,
396 partition_tag: crate::csr::local_node_id::next_partition_tag(),
397 memory,
398 rebuild_journal: None,
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::csr::index::Direction;
407 use crate::test_support::test_memory;
408
409 #[test]
410 fn checkpoint_roundtrip_unweighted() {
411 let mut csr = CsrIndex::new(test_memory());
412 csr.add_edge("a", "KNOWS", "b").unwrap();
413 csr.add_edge("b", "KNOWS", "c").unwrap();
414 csr.compact()
415 .expect("test governor ceiling covers this reservation");
416
417 let bytes = csr
418 .checkpoint_to_bytes()
419 .expect("test governor ceiling covers this reservation");
420 let restored = CsrIndex::from_checkpoint(&bytes, test_memory())
421 .expect("roundtrip")
422 .unwrap();
423 assert_eq!(restored.node_count(), 3);
424 assert_eq!(restored.edge_count(), 2);
425 assert!(!restored.has_weights());
426
427 let n = restored.neighbors("a", &["KNOWS"], Direction::Out);
428 assert_eq!(n.len(), 1);
429 assert_eq!(n[0].1, "b");
430 }
431
432 #[test]
433 fn checkpoint_roundtrip_weighted() {
434 let mut csr = CsrIndex::new(test_memory());
435 csr.add_edge_weighted("a", "R", "b", 2.5).unwrap();
436 csr.add_edge_weighted("b", "R", "c", 7.0).unwrap();
437 csr.add_edge("c", "R", "d").unwrap();
438 csr.compact()
439 .expect("test governor ceiling covers this reservation");
440
441 let bytes = csr
442 .checkpoint_to_bytes()
443 .expect("test governor ceiling covers this reservation");
444 let restored = CsrIndex::from_checkpoint(&bytes, test_memory())
445 .expect("roundtrip")
446 .unwrap();
447 assert!(restored.has_weights());
448 assert_eq!(restored.edge_weight("a", "R", "b"), Some(2.5));
449 assert_eq!(restored.edge_weight("b", "R", "c"), Some(7.0));
450 assert_eq!(restored.edge_weight("c", "R", "d"), Some(1.0));
451 }
452
453 #[test]
454 fn checkpoint_roundtrip_with_buffer() {
455 let mut csr = CsrIndex::new(test_memory());
456 csr.add_edge("a", "L", "b").unwrap();
457 let bytes = csr
459 .checkpoint_to_bytes()
460 .expect("test governor ceiling covers this reservation");
461 let restored = CsrIndex::from_checkpoint(&bytes, test_memory())
462 .expect("roundtrip")
463 .unwrap();
464 assert_eq!(restored.edge_count(), 1);
465 }
466
467 #[test]
468 fn golden_header_layout() {
469 let mut csr = CsrIndex::new(test_memory());
470 csr.add_edge("a", "KNOWS", "b").unwrap();
471 let bytes = csr
472 .checkpoint_to_bytes()
473 .expect("test governor ceiling covers this reservation");
474 assert_eq!(&bytes[0..6], b"RKCS2\0");
476 assert_eq!(bytes[6], super::CSR_FORMAT_VERSION);
478 assert!(bytes.len() > 7);
480 }
481
482 #[test]
483 fn version_mismatch_returns_error() {
484 let mut csr = CsrIndex::new(test_memory());
485 csr.add_edge("a", "KNOWS", "b").unwrap();
486 let mut bytes = csr
487 .checkpoint_to_bytes()
488 .expect("test governor ceiling covers this reservation");
489 bytes[6] = 0;
491 match CsrIndex::from_checkpoint(&bytes, test_memory()) {
492 Err(CsrCheckpointError::UnsupportedVersion { found, expected }) => {
493 assert_eq!(found, 0);
494 assert_eq!(expected, super::CSR_FORMAT_VERSION);
495 }
496 Err(other) => panic!("unexpected error: {other}"),
497 Ok(_) => panic!("expected UnsupportedVersion error, got Ok"),
498 }
499 }
500}