1use std::path::{Path, PathBuf};
12use std::time::SystemTime;
13
14use linkrs_core::types::{CommitLsn, IndexGeneration};
15use serde::{Deserialize, Serialize};
16
17use crate::sqlite_outbox::OutboxSnapshot;
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct CheckpointManifest {
27 pub format_version: u32,
29 pub checkpoint_id: u64,
31 pub timestamp: u64,
33 pub storage_lsn: CommitLsn,
35 pub outbox_lsn: CommitLsn,
37 pub outbox_enabled: bool,
44 pub safe_lsn: CommitLsn,
47 pub storage_snapshot: StorageSnapshotRef,
49 pub outbox_snapshot: Option<OutboxSnapshotRef>,
51 pub index_manifests: Vec<IndexManifestRef>,
53 #[serde(default)]
57 pub max_commit_timestamp: u64,
58 #[serde(default)]
65 pub snapshot_timestamp: Option<u64>,
66 #[serde(default)]
68 pub schema_catalog_version: u64,
69 #[serde(default)]
71 pub sequence_version: u64,
72 pub manifest_checksum: u32,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct StorageSnapshotRef {
79 pub path: PathBuf,
81 pub size_bytes: u64,
83 pub checksum: u32,
85 pub files: Vec<StorageFileRef>,
87 pub checkpoint_seq: u64,
89 pub vertex_count: u64,
91 pub edge_count: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
97pub struct StorageFileRef {
98 pub path: PathBuf,
100 pub size_bytes: u64,
102 pub checksum: u32,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub struct OutboxSnapshotRef {
109 pub path: PathBuf,
111 pub size_bytes: u64,
113 pub checksum: u32,
115 pub materialized_lsn: CommitLsn,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121pub struct IndexManifestRef {
122 pub space_id: u64,
124 pub index_id: u64,
126 pub generation: IndexGeneration,
128 pub path: PathBuf,
130 pub size_bytes: u64,
132 pub checksum: u32,
134}
135
136impl CheckpointManifest {
137 pub const CURRENT_FORMAT_VERSION: u32 = 5;
139
140 pub fn storage_snapshot_from_directory(
144 path: impl AsRef<Path>,
145 checkpoint_seq: u64,
146 vertex_count: u64,
147 edge_count: u64,
148 ) -> Result<StorageSnapshotRef, String> {
149 let path = path.as_ref();
150 let files = collect_storage_files(path)?;
151 let size_bytes = files.iter().map(|file| file.size_bytes).sum();
152 let checksum = storage_files_checksum(&files)?;
153 Ok(StorageSnapshotRef {
154 path: path.to_path_buf(),
155 size_bytes,
156 checksum,
157 files,
158 checkpoint_seq,
159 vertex_count,
160 edge_count,
161 })
162 }
163
164 pub fn new(
170 checkpoint_id: u64,
171 storage_lsn: CommitLsn,
172 storage_snapshot: StorageSnapshotRef,
173 outbox_snapshot: Option<OutboxSnapshotRef>,
174 index_manifests: Vec<IndexManifestRef>,
175 ) -> Result<Self, String> {
176 Self::new_with_outbox_state(
177 checkpoint_id,
178 storage_lsn,
179 storage_snapshot,
180 outbox_snapshot,
181 index_manifests,
182 false,
183 )
184 }
185
186 pub fn new_with_outbox(
189 checkpoint_id: u64,
190 storage_lsn: CommitLsn,
191 storage_snapshot: StorageSnapshotRef,
192 outbox_snapshot: Option<OutboxSnapshotRef>,
193 index_manifests: Vec<IndexManifestRef>,
194 ) -> Result<Self, String> {
195 Self::new_with_outbox_state(
196 checkpoint_id,
197 storage_lsn,
198 storage_snapshot,
199 outbox_snapshot,
200 index_manifests,
201 true,
202 )
203 }
204
205 fn new_with_outbox_state(
206 checkpoint_id: u64,
207 storage_lsn: CommitLsn,
208 storage_snapshot: StorageSnapshotRef,
209 outbox_snapshot: Option<OutboxSnapshotRef>,
210 index_manifests: Vec<IndexManifestRef>,
211 outbox_enabled: bool,
212 ) -> Result<Self, String> {
213 let outbox_lsn = outbox_snapshot
214 .as_ref()
215 .map(|s| s.materialized_lsn)
216 .unwrap_or(CommitLsn::ZERO);
217
218 let safe_lsn = if outbox_enabled && outbox_snapshot.is_none() {
222 CommitLsn::ZERO
223 } else if outbox_snapshot.is_some() && outbox_lsn < storage_lsn {
224 outbox_lsn
225 } else {
226 storage_lsn
227 };
228
229 let mut manifest = Self {
230 format_version: Self::CURRENT_FORMAT_VERSION,
231 checkpoint_id,
232 timestamp: SystemTime::now()
233 .duration_since(SystemTime::UNIX_EPOCH)
234 .unwrap_or_default()
235 .as_secs(),
236 storage_lsn,
237 outbox_lsn,
238 outbox_enabled,
239 safe_lsn,
240 storage_snapshot,
241 outbox_snapshot,
242 index_manifests,
243 max_commit_timestamp: storage_lsn.get(),
244 snapshot_timestamp: None,
245 schema_catalog_version: 0,
246 sequence_version: 0,
247 manifest_checksum: 0,
248 };
249
250 manifest.manifest_checksum = manifest.compute_checksum()?;
251 Ok(manifest)
252 }
253
254 pub fn with_snapshot_timestamp(mut self, snapshot_ts: u64) -> Result<Self, String> {
262 self.max_commit_timestamp = snapshot_ts;
263 self.snapshot_timestamp = Some(snapshot_ts);
264 self.manifest_checksum = self.compute_checksum()?;
265 Ok(self)
266 }
267
268 fn compute_checksum(&self) -> Result<u32, String> {
270 let mut clone = self.clone();
271 clone.manifest_checksum = 0;
272 let bytes = postcard::to_allocvec(&clone)
273 .map_err(|error| format!("Failed to serialize checkpoint manifest: {error}"))?;
274 Ok(crc32fast::hash(&bytes))
275 }
276
277 pub fn verify_checksum(&self) -> Result<bool, String> {
279 let expected = self.manifest_checksum;
280 let mut clone = self.clone();
281 clone.manifest_checksum = 0;
282 let bytes = postcard::to_allocvec(&clone)
283 .map_err(|error| format!("Failed to serialize checkpoint manifest: {error}"))?;
284 let computed = crc32fast::hash(&bytes);
285 Ok(computed == expected)
286 }
287
288 pub fn validate(&self) -> Result<(), String> {
290 if self.format_version != Self::CURRENT_FORMAT_VERSION
291 && self.format_version != Self::CURRENT_FORMAT_VERSION - 1
292 {
293 return Err(format!(
294 "Unsupported manifest format version: {}",
295 self.format_version
296 ));
297 }
298
299 if !self.verify_checksum()? {
300 return Err("Manifest checksum verification failed".to_string());
301 }
302
303 let outbox_lsn = self
305 .outbox_snapshot
306 .as_ref()
307 .map(|s| s.materialized_lsn)
308 .unwrap_or(CommitLsn::ZERO);
309
310 let expected_safe = if self.outbox_enabled && self.outbox_snapshot.is_none() {
311 CommitLsn::ZERO
312 } else if self.outbox_snapshot.is_some() && outbox_lsn < self.storage_lsn {
313 outbox_lsn
314 } else {
315 self.storage_lsn
316 };
317
318 if self.safe_lsn != expected_safe {
319 return Err(format!(
320 "Invalid safe_lsn: expected {}, got {}",
321 expected_safe, self.safe_lsn
322 ));
323 }
324
325 if self.storage_snapshot.checkpoint_seq != self.checkpoint_id {
326 return Err(format!(
327 "Storage snapshot checkpoint sequence {} does not match manifest {}",
328 self.storage_snapshot.checkpoint_seq, self.checkpoint_id
329 ));
330 }
331
332 if self.outbox_snapshot.is_some() && self.outbox_lsn > self.storage_lsn {
333 return Err(format!(
334 "Outbox snapshot LSN {} is ahead of storage LSN {}",
335 self.outbox_lsn, self.storage_lsn
336 ));
337 }
338
339 if self.outbox_snapshot.is_none() && self.outbox_lsn != CommitLsn::ZERO {
341 return Err(
342 "Outbox LSN is non-zero but outbox snapshot reference is missing".to_string(),
343 );
344 }
345
346 verify_storage_reference(&self.storage_snapshot)?;
347 if let Some(snapshot) = &self.outbox_snapshot {
348 verify_file_reference(
349 &snapshot.path,
350 snapshot.size_bytes,
351 snapshot.checksum,
352 "outbox snapshot",
353 )?;
354 }
355 for manifest in &self.index_manifests {
356 verify_file_reference(
357 &manifest.path,
358 manifest.size_bytes,
359 manifest.checksum,
360 "index manifest",
361 )?;
362 }
363
364 Ok(())
365 }
366
367 pub fn load(path: impl AsRef<Path>) -> Result<Self, String> {
369 let bytes = std::fs::read(path.as_ref()).map_err(|error| error.to_string())?;
370 let manifest: Self = postcard::from_bytes(&bytes).map_err(|error| error.to_string())?;
371 manifest.validate()?;
372 Ok(manifest)
373 }
374
375 pub fn save_atomic(&self, destination: impl AsRef<Path>) -> Result<(), String> {
377 let destination = destination.as_ref();
378
379 if let Some(parent) = destination.parent() {
381 std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
382 }
383
384 let temporary = destination.with_extension("tmp");
386 let bytes = postcard::to_allocvec(self).map_err(|error| error.to_string())?;
387 std::fs::write(&temporary, &bytes).map_err(|error| error.to_string())?;
388
389 let file = std::fs::OpenOptions::new()
391 .read(true)
392 .open(&temporary)
393 .map_err(|error| error.to_string())?;
394 file.sync_all().map_err(|error| error.to_string())?;
395 drop(file);
396
397 if destination.exists() {
399 std::fs::remove_file(destination).map_err(|error| error.to_string())?;
400 }
401 std::fs::rename(&temporary, destination).map_err(|error| error.to_string())?;
402
403 if let Some(parent) = destination.parent() {
405 std::fs::File::open(parent)
406 .and_then(|dir| dir.sync_all())
407 .map_err(|error| error.to_string())?;
408 }
409
410 Ok(())
411 }
412
413 pub fn outbox_snapshot_from(outbox: &OutboxSnapshot) -> OutboxSnapshotRef {
415 OutboxSnapshotRef {
416 path: outbox.path.clone(),
417 size_bytes: outbox.size_bytes,
418 checksum: outbox.checksum,
419 materialized_lsn: outbox.materialized_lsn,
420 }
421 }
422}
423
424pub struct CheckpointManifestManager {
426 manifest_dir: PathBuf,
427}
428
429impl CheckpointManifestManager {
430 pub fn new(manifest_dir: impl AsRef<Path>) -> Self {
432 Self {
433 manifest_dir: manifest_dir.as_ref().to_path_buf(),
434 }
435 }
436
437 pub fn init(&self) -> Result<(), String> {
439 std::fs::create_dir_all(&self.manifest_dir).map_err(|error| error.to_string())?;
440
441 std::fs::File::open(&self.manifest_dir)
443 .and_then(|dir| dir.sync_all())
444 .map_err(|error| error.to_string())?;
445
446 Ok(())
447 }
448
449 pub fn manifest_path(&self, checkpoint_id: u64) -> PathBuf {
451 self.manifest_dir
452 .join(format!("manifest_{:020}.postcard", checkpoint_id))
453 }
454
455 pub fn publish(&self, manifest: &CheckpointManifest) -> Result<(), String> {
457 let path = self.manifest_path(manifest.checkpoint_id);
458 manifest.save_atomic(&path)?;
459 Ok(())
460 }
461
462 pub fn load_latest(&self) -> Result<Option<CheckpointManifest>, String> {
464 if !self.manifest_dir.exists() {
465 return Ok(None);
466 }
467
468 let mut manifests: Vec<(u64, PathBuf)> = std::fs::read_dir(&self.manifest_dir)
469 .map_err(|error| error.to_string())?
470 .filter_map(|entry| entry.ok())
471 .filter_map(|entry| {
472 let path = entry.path();
473 if !path.is_file() {
474 return None;
475 }
476 let name = path.file_name()?.to_str()?;
477 let id = name
478 .strip_prefix("manifest_")?
479 .strip_suffix(".postcard")?
480 .parse::<u64>()
481 .ok()?;
482 Some((id, path))
483 })
484 .collect();
485
486 manifests.sort_by_key(|(id, _)| std::cmp::Reverse(*id));
487
488 for (_, path) in manifests {
489 match CheckpointManifest::load(&path) {
490 Ok(manifest) => return Ok(Some(manifest)),
491 Err(error) => {
492 log::warn!(
493 "Skipping invalid checkpoint manifest {}: {}",
494 path.display(),
495 error
496 );
497 }
498 }
499 }
500 Ok(None)
501 }
502
503 pub fn load(&self, checkpoint_id: u64) -> Result<Option<CheckpointManifest>, String> {
505 let path = self.manifest_path(checkpoint_id);
506 if !path.exists() {
507 return Ok(None);
508 }
509 CheckpointManifest::load(path).map(Some)
510 }
511
512 pub fn list_manifests(&self) -> Result<Vec<u64>, String> {
514 if !self.manifest_dir.exists() {
515 return Ok(Vec::new());
516 }
517
518 let mut ids: Vec<u64> = std::fs::read_dir(&self.manifest_dir)
519 .map_err(|error| error.to_string())?
520 .filter_map(|entry| entry.ok())
521 .filter_map(|entry| {
522 let path = entry.path();
523 let name = path.file_name()?.to_str()?;
524 name.strip_prefix("manifest_")?
525 .strip_suffix(".postcard")?
526 .parse::<u64>()
527 .ok()
528 })
529 .collect();
530
531 ids.sort();
532 Ok(ids)
533 }
534
535 pub fn latest_safe_lsn(&self) -> Result<CommitLsn, String> {
537 self.load_latest()?
538 .map(|m| m.safe_lsn)
539 .ok_or_else(|| "No published manifest found".to_string())
540 }
541
542 pub fn cleanup_old(&self, keep_newer_than: u64) -> Result<usize, String> {
544 let mut removed = 0;
545 for id in self.list_manifests()? {
546 if id < keep_newer_than {
547 let path = self.manifest_path(id);
548 std::fs::remove_file(path).map_err(|error| error.to_string())?;
549 removed += 1;
550 }
551 }
552
553 if removed > 0 {
555 std::fs::File::open(&self.manifest_dir)
556 .and_then(|dir| dir.sync_all())
557 .map_err(|error| error.to_string())?;
558 }
559
560 Ok(removed)
561 }
562}
563
564fn verify_file_reference(
565 path: &Path,
566 expected_size: u64,
567 expected_checksum: u32,
568 kind: &str,
569) -> Result<(), String> {
570 let bytes = std::fs::read(path)
571 .map_err(|error| format!("Failed to read {kind} {}: {error}", path.display()))?;
572 if bytes.len() as u64 != expected_size {
573 return Err(format!("{kind} size mismatch: {}", path.display()));
574 }
575 if crc32fast::hash(&bytes) != expected_checksum {
576 return Err(format!("{kind} checksum mismatch: {}", path.display()));
577 }
578 Ok(())
579}
580
581fn collect_storage_files(root: &Path) -> Result<Vec<StorageFileRef>, String> {
582 fn visit(root: &Path, directory: &Path, files: &mut Vec<StorageFileRef>) -> Result<(), String> {
583 for entry in std::fs::read_dir(directory).map_err(|error| error.to_string())? {
584 let path = entry.map_err(|error| error.to_string())?.path();
585 if path.is_dir() {
586 visit(root, &path, files)?;
587 continue;
588 }
589 if !path.is_file() {
590 continue;
591 }
592 if path
599 .file_name()
600 .and_then(|name| name.to_str())
601 .is_some_and(|name| name == "edge_wal.bin")
602 {
603 continue;
604 }
605 let bytes = std::fs::read(&path).map_err(|error| error.to_string())?;
606 let relative = path
607 .strip_prefix(root)
608 .map_err(|error| error.to_string())?
609 .to_path_buf();
610 files.push(StorageFileRef {
611 path: relative,
612 size_bytes: bytes.len() as u64,
613 checksum: crc32fast::hash(&bytes),
614 });
615 }
616 Ok(())
617 }
618
619 if !root.is_dir() {
620 return Err(format!(
621 "Storage snapshot directory does not exist: {}",
622 root.display()
623 ));
624 }
625 let mut files = Vec::new();
626 visit(root, root, &mut files)?;
627 files.sort_by(|left, right| left.path.cmp(&right.path));
628 Ok(files)
629}
630
631fn storage_files_checksum(files: &[StorageFileRef]) -> Result<u32, String> {
632 let bytes = postcard::to_allocvec(files).map_err(|error| error.to_string())?;
633 Ok(crc32fast::hash(&bytes))
634}
635
636fn verify_storage_reference(reference: &StorageSnapshotRef) -> Result<(), String> {
637 let actual = collect_storage_files(&reference.path)?;
638 if actual != reference.files {
639 return Err(format!(
640 "Storage snapshot file list mismatch: {}",
641 reference.path.display()
642 ));
643 }
644 let size_bytes: u64 = actual.iter().map(|file| file.size_bytes).sum();
645 if size_bytes != reference.size_bytes {
646 return Err(format!(
647 "Storage snapshot size mismatch: {}",
648 reference.path.display()
649 ));
650 }
651 let checksum = storage_files_checksum(&actual)?;
652 if checksum != reference.checksum {
653 return Err(format!(
654 "Storage snapshot checksum mismatch: {}",
655 reference.path.display()
656 ));
657 }
658
659 for file in &actual {
660 if file.path.is_absolute()
661 || file
662 .path
663 .components()
664 .any(|component| component == std::path::Component::ParentDir)
665 {
666 return Err(format!(
667 "Invalid storage snapshot file path: {}",
668 file.path.display()
669 ));
670 }
671 }
672 Ok(())
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use tempfile::TempDir;
679
680 fn create_test_storage_snapshot_ref(temp_dir: &Path) -> StorageSnapshotRef {
681 let snapshot_path = temp_dir.join("storage_snapshot");
682 std::fs::create_dir_all(&snapshot_path).unwrap();
683 std::fs::write(snapshot_path.join("checkpoint.meta"), b"checkpoint").unwrap();
684 CheckpointManifest::storage_snapshot_from_directory(&snapshot_path, 1, 100, 50).unwrap()
685 }
686
687 fn create_test_outbox_snapshot_ref(temp_dir: &Path) -> OutboxSnapshotRef {
688 let snapshot_path = temp_dir.join("outbox_snapshot.sqlite");
689 std::fs::write(&snapshot_path, b"test").unwrap();
690 OutboxSnapshotRef {
691 path: snapshot_path,
692 size_bytes: 4,
693 checksum: crc32fast::hash(b"test"),
694 materialized_lsn: CommitLsn::new(100),
695 }
696 }
697
698 #[test]
699 fn test_manifest_creation_and_validation() {
700 let temp_dir = TempDir::new().unwrap();
701 let storage_ref = create_test_storage_snapshot_ref(temp_dir.path());
702 let outbox_ref = create_test_outbox_snapshot_ref(temp_dir.path());
703
704 let manifest = CheckpointManifest::new(
705 1,
706 CommitLsn::new(100),
707 storage_ref,
708 Some(outbox_ref),
709 Vec::new(),
710 )
711 .expect("checkpoint manifest should be created");
712
713 assert!(manifest.validate().is_ok());
714 assert_eq!(manifest.safe_lsn, CommitLsn::new(100));
715 assert_eq!(manifest.storage_lsn, CommitLsn::new(100));
716 assert_eq!(manifest.outbox_lsn, CommitLsn::new(100));
717 }
718
719 #[test]
720 fn test_safe_lsn_is_minimum() {
721 let temp_dir = TempDir::new().unwrap();
722 let storage_ref = create_test_storage_snapshot_ref(temp_dir.path());
723
724 let manifest1 = CheckpointManifest::new(
727 1,
728 CommitLsn::new(50),
729 storage_ref.clone(),
730 Some(OutboxSnapshotRef {
731 path: temp_dir.path().join("outbox.sqlite"),
732 size_bytes: 4,
733 checksum: 0,
734 materialized_lsn: CommitLsn::new(50),
735 }),
736 Vec::new(),
737 )
738 .expect("checkpoint manifest should be created");
739 assert_eq!(manifest1.safe_lsn, CommitLsn::new(50));
740
741 let manifest2 = CheckpointManifest::new(
743 2,
744 CommitLsn::new(100),
745 storage_ref,
746 Some(OutboxSnapshotRef {
747 path: temp_dir.path().join("outbox2.sqlite"),
748 size_bytes: 4,
749 checksum: 0,
750 materialized_lsn: CommitLsn::new(20),
751 }),
752 Vec::new(),
753 )
754 .expect("checkpoint manifest should be created");
755 assert_eq!(manifest2.safe_lsn, CommitLsn::new(20));
756 }
757
758 #[test]
759 fn enabled_outbox_without_snapshot_keeps_wal_reclaim_boundary_at_zero() {
760 let temp_dir = TempDir::new().unwrap();
761 let manifest = CheckpointManifest::new_with_outbox(
762 1,
763 CommitLsn::new(100),
764 create_test_storage_snapshot_ref(temp_dir.path()),
765 None,
766 Vec::new(),
767 )
768 .expect("checkpoint manifest should be created");
769
770 assert_eq!(manifest.safe_lsn, CommitLsn::ZERO);
771 assert!(manifest.validate().is_ok());
772 }
773
774 #[test]
775 fn test_manifest_save_and_load() {
776 let temp_dir = TempDir::new().unwrap();
777 let storage_ref = create_test_storage_snapshot_ref(temp_dir.path());
778 let outbox_ref = create_test_outbox_snapshot_ref(temp_dir.path());
779
780 let manifest = CheckpointManifest::new(
781 1,
782 CommitLsn::new(100),
783 storage_ref,
784 Some(outbox_ref),
785 Vec::new(),
786 )
787 .expect("checkpoint manifest should be created");
788
789 let manifest_path = temp_dir.path().join("test_manifest.postcard");
790 manifest.save_atomic(&manifest_path).unwrap();
791
792 let loaded = CheckpointManifest::load(&manifest_path).unwrap();
793 assert_eq!(loaded.checkpoint_id, manifest.checkpoint_id);
794 assert_eq!(loaded.safe_lsn, manifest.safe_lsn);
795 }
796
797 fn create_storage_snapshot_ref(temp_dir: &Path, checkpoint_seq: u64) -> StorageSnapshotRef {
798 let snapshot_path = temp_dir.join(format!("storage_snapshot_{}", checkpoint_seq));
799 std::fs::create_dir_all(&snapshot_path).unwrap();
800 std::fs::write(snapshot_path.join("checkpoint.meta"), b"checkpoint").unwrap();
801 CheckpointManifest::storage_snapshot_from_directory(&snapshot_path, checkpoint_seq, 100, 50)
802 .unwrap()
803 }
804
805 #[test]
806 fn test_manifest_manager() {
807 let temp_dir = TempDir::new().unwrap();
808 let manager = CheckpointManifestManager::new(temp_dir.path());
809 manager.init().unwrap();
810
811 let storage_ref1 = create_storage_snapshot_ref(temp_dir.path(), 1);
812 let storage_ref2 = create_storage_snapshot_ref(temp_dir.path(), 2);
813 let outbox_ref = create_test_outbox_snapshot_ref(temp_dir.path());
814
815 let manifest1 = CheckpointManifest::new(
816 1,
817 CommitLsn::new(100),
818 storage_ref1,
819 Some(outbox_ref.clone()),
820 Vec::new(),
821 )
822 .expect("checkpoint manifest should be created");
823 let manifest2 = CheckpointManifest::new(
824 2,
825 CommitLsn::new(100),
826 storage_ref2,
827 Some(outbox_ref),
828 Vec::new(),
829 )
830 .expect("checkpoint manifest should be created");
831
832 manager.publish(&manifest1).unwrap();
833 manager.publish(&manifest2).unwrap();
834
835 let latest = manager.load_latest().unwrap().unwrap();
836 assert_eq!(latest.checkpoint_id, 2);
837 assert_eq!(latest.safe_lsn, CommitLsn::new(100));
838
839 let safe_lsn = manager.latest_safe_lsn().unwrap();
840 assert_eq!(safe_lsn, CommitLsn::new(100));
841
842 let manifests = manager.list_manifests().unwrap();
843 assert_eq!(manifests, vec![1, 2]);
844 }
845
846 #[test]
847 fn test_checksum_verification() {
848 let temp_dir = TempDir::new().unwrap();
849 let storage_ref = create_test_storage_snapshot_ref(temp_dir.path());
850
851 let mut manifest =
852 CheckpointManifest::new(1, CommitLsn::new(100), storage_ref, None, Vec::new())
853 .expect("checkpoint manifest should be created");
854
855 assert!(manifest.verify_checksum().expect("checksum should compute"));
856
857 manifest.checkpoint_id = 999;
859 assert!(!manifest.verify_checksum().expect("checksum should compute"));
860 }
861
862 #[test]
863 fn test_snapshot_timestamp_fix_and_roundtrip() {
864 let temp_dir = TempDir::new().unwrap();
865 let storage_ref = create_test_storage_snapshot_ref(temp_dir.path());
866
867 let legacy = CheckpointManifest::new(
869 1,
870 CommitLsn::new(100),
871 storage_ref.clone(),
872 None,
873 Vec::new(),
874 )
875 .expect("checkpoint manifest should be created");
876 assert_eq!(legacy.snapshot_timestamp, None);
877
878 let stamped =
882 CheckpointManifest::new(1, CommitLsn::new(100), storage_ref, None, Vec::new())
883 .expect("checkpoint manifest should be created")
884 .with_snapshot_timestamp(40)
885 .expect("snapshot stamp should apply");
886 assert_eq!(stamped.max_commit_timestamp, 40);
887 assert_eq!(stamped.snapshot_timestamp, Some(40));
888 assert!(stamped.validate().is_ok());
889
890 let path = temp_dir.path().join("stamped.postcard");
891 stamped.save_atomic(&path).unwrap();
892 let loaded = CheckpointManifest::load(&path).unwrap();
893 assert_eq!(loaded.snapshot_timestamp, Some(40));
894 assert_eq!(loaded.max_commit_timestamp, 40);
895 }
896}