1use crate::traits::BlockStore;
9use ipfrs_core::{Cid, Result};
10use std::fmt;
11use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, Instant};
14use thiserror::Error;
15
16#[derive(Debug, Clone, Default)]
18pub struct BlockMigrationStats {
19 pub blocks_migrated: u64,
21 pub bytes_migrated: u64,
23 pub blocks_skipped: u64,
25 pub errors: u64,
27 pub duration: Duration,
29 pub blocks_per_second: f64,
31 pub bytes_per_second: f64,
33}
34
35impl BlockMigrationStats {
36 fn calculate_throughput(&mut self, duration: Duration) {
38 let seconds = duration.as_secs_f64();
39 if seconds > 0.0 {
40 self.blocks_per_second = self.blocks_migrated as f64 / seconds;
41 self.bytes_per_second = self.bytes_migrated as f64 / seconds;
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct MigrationConfig {
49 pub batch_size: usize,
51 pub skip_existing: bool,
53 pub verify: bool,
55 pub concurrency: usize,
57}
58
59impl Default for MigrationConfig {
60 fn default() -> Self {
61 Self {
62 batch_size: 100,
63 skip_existing: true,
64 verify: false,
65 concurrency: 4,
66 }
67 }
68}
69
70pub type ProgressCallback = Arc<dyn Fn(u64, u64) + Send + Sync>;
72
73pub struct StorageMigrator<S: BlockStore, D: BlockStore> {
75 source: Arc<S>,
76 destination: Arc<D>,
77 config: MigrationConfig,
78 progress_callback: Option<ProgressCallback>,
79}
80
81impl<S: BlockStore, D: BlockStore> StorageMigrator<S, D> {
82 pub fn new(source: Arc<S>, destination: Arc<D>) -> Self {
84 Self {
85 source,
86 destination,
87 config: MigrationConfig::default(),
88 progress_callback: None,
89 }
90 }
91
92 pub fn with_config(source: Arc<S>, destination: Arc<D>, config: MigrationConfig) -> Self {
94 Self {
95 source,
96 destination,
97 config,
98 progress_callback: None,
99 }
100 }
101
102 pub fn with_progress_callback<F>(mut self, callback: F) -> Self
104 where
105 F: Fn(u64, u64) + Send + Sync + 'static,
106 {
107 self.progress_callback = Some(Arc::new(callback));
108 self
109 }
110
111 pub async fn migrate_all(&self) -> Result<BlockMigrationStats> {
113 let start = Instant::now();
114
115 let blocks_migrated = AtomicU64::new(0);
116 let bytes_migrated = AtomicU64::new(0);
117 let blocks_skipped = AtomicU64::new(0);
118 let errors = AtomicU64::new(0);
119
120 let all_cids = self.source.list_cids()?;
122 let total_blocks = all_cids.len() as u64;
123
124 for batch in all_cids.chunks(self.config.batch_size) {
126 let cids_to_migrate = if self.config.skip_existing {
128 let exists = self.destination.has_many(batch).await?;
129 batch
130 .iter()
131 .zip(exists.iter())
132 .filter_map(|(cid, exists)| {
133 if *exists {
134 blocks_skipped.fetch_add(1, Ordering::Relaxed);
135 None
136 } else {
137 Some(*cid)
138 }
139 })
140 .collect::<Vec<_>>()
141 } else {
142 batch.to_vec()
143 };
144
145 if cids_to_migrate.is_empty() {
146 continue;
147 }
148
149 let blocks_result = self.source.get_many(&cids_to_migrate).await?;
151
152 let mut valid_blocks = Vec::new();
154 for block_opt in blocks_result {
155 if let Some(block) = block_opt {
156 bytes_migrated.fetch_add(block.data().len() as u64, Ordering::Relaxed);
157 valid_blocks.push(block);
158 } else {
159 errors.fetch_add(1, Ordering::Relaxed);
160 }
161 }
162
163 if !valid_blocks.is_empty() {
165 match self.destination.put_many(&valid_blocks).await {
166 Ok(_) => {
167 blocks_migrated.fetch_add(valid_blocks.len() as u64, Ordering::Relaxed);
168
169 if self.config.verify {
171 let cids: Vec<Cid> = valid_blocks.iter().map(|b| *b.cid()).collect();
172 let verified = self.destination.has_many(&cids).await?;
173 let failed = verified.iter().filter(|&&exists| !exists).count();
174 if failed > 0 {
175 errors.fetch_add(failed as u64, Ordering::Relaxed);
176 }
177 }
178 }
179 Err(_) => {
180 errors.fetch_add(valid_blocks.len() as u64, Ordering::Relaxed);
181 }
182 }
183 }
184
185 if let Some(ref callback) = self.progress_callback {
187 let migrated = blocks_migrated.load(Ordering::Relaxed);
188 callback(migrated, total_blocks);
189 }
190 }
191
192 let mut stats = BlockMigrationStats {
193 blocks_migrated: blocks_migrated.load(Ordering::Relaxed),
194 bytes_migrated: bytes_migrated.load(Ordering::Relaxed),
195 blocks_skipped: blocks_skipped.load(Ordering::Relaxed),
196 errors: errors.load(Ordering::Relaxed),
197 duration: start.elapsed(),
198 blocks_per_second: 0.0,
199 bytes_per_second: 0.0,
200 };
201
202 stats.calculate_throughput(stats.duration);
203
204 Ok(stats)
205 }
206
207 pub async fn migrate_cids(&self, cids: &[Cid]) -> Result<BlockMigrationStats> {
209 let start = Instant::now();
210
211 let blocks_migrated = AtomicU64::new(0);
212 let bytes_migrated = AtomicU64::new(0);
213 let blocks_skipped = AtomicU64::new(0);
214 let errors = AtomicU64::new(0);
215
216 for batch in cids.chunks(self.config.batch_size) {
218 let cids_to_migrate = if self.config.skip_existing {
220 let exists = self.destination.has_many(batch).await?;
221 batch
222 .iter()
223 .zip(exists.iter())
224 .filter_map(|(cid, exists)| {
225 if *exists {
226 blocks_skipped.fetch_add(1, Ordering::Relaxed);
227 None
228 } else {
229 Some(*cid)
230 }
231 })
232 .collect::<Vec<_>>()
233 } else {
234 batch.to_vec()
235 };
236
237 if cids_to_migrate.is_empty() {
238 continue;
239 }
240
241 let blocks_result = self.source.get_many(&cids_to_migrate).await?;
243 let mut valid_blocks = Vec::new();
244
245 for block_opt in blocks_result {
246 if let Some(block) = block_opt {
247 bytes_migrated.fetch_add(block.data().len() as u64, Ordering::Relaxed);
248 valid_blocks.push(block);
249 } else {
250 errors.fetch_add(1, Ordering::Relaxed);
251 }
252 }
253
254 if !valid_blocks.is_empty() {
255 match self.destination.put_many(&valid_blocks).await {
256 Ok(_) => {
257 blocks_migrated.fetch_add(valid_blocks.len() as u64, Ordering::Relaxed);
258 }
259 Err(_) => {
260 errors.fetch_add(valid_blocks.len() as u64, Ordering::Relaxed);
261 }
262 }
263 }
264 }
265
266 let mut stats = BlockMigrationStats {
267 blocks_migrated: blocks_migrated.load(Ordering::Relaxed),
268 bytes_migrated: bytes_migrated.load(Ordering::Relaxed),
269 blocks_skipped: blocks_skipped.load(Ordering::Relaxed),
270 errors: errors.load(Ordering::Relaxed),
271 duration: start.elapsed(),
272 blocks_per_second: 0.0,
273 bytes_per_second: 0.0,
274 };
275
276 stats.calculate_throughput(stats.duration);
277
278 Ok(stats)
279 }
280}
281
282pub async fn migrate_storage<S: BlockStore, D: BlockStore>(
284 source: Arc<S>,
285 destination: Arc<D>,
286) -> Result<BlockMigrationStats> {
287 let migrator = StorageMigrator::new(source, destination);
288 migrator.migrate_all().await
289}
290
291pub async fn migrate_storage_with_progress<S: BlockStore, D: BlockStore, F>(
293 source: Arc<S>,
294 destination: Arc<D>,
295 progress_callback: F,
296) -> Result<BlockMigrationStats>
297where
298 F: Fn(u64, u64) + Send + Sync + 'static,
299{
300 let migrator =
301 StorageMigrator::new(source, destination).with_progress_callback(progress_callback);
302 migrator.migrate_all().await
303}
304
305pub async fn migrate_storage_batched<S: BlockStore, D: BlockStore>(
307 source: Arc<S>,
308 destination: Arc<D>,
309 batch_size: usize,
310) -> Result<BlockMigrationStats> {
311 let config = MigrationConfig {
312 batch_size,
313 ..Default::default()
314 };
315 let migrator = StorageMigrator::with_config(source, destination, config);
316 migrator.migrate_all().await
317}
318
319pub async fn migrate_storage_verified<S: BlockStore, D: BlockStore>(
321 source: Arc<S>,
322 destination: Arc<D>,
323) -> Result<BlockMigrationStats> {
324 let config = MigrationConfig {
325 verify: true,
326 ..Default::default()
327 };
328 let migrator = StorageMigrator::with_config(source, destination, config);
329 migrator.migrate_all().await
330}
331
332#[derive(Debug, Clone)]
334pub struct MigrationEstimate {
335 pub total_blocks: usize,
337 pub total_bytes: u64,
339 pub estimated_duration_low: Duration,
341 pub estimated_duration_high: Duration,
343 pub space_required: u64,
345}
346
347pub async fn estimate_migration<S: BlockStore>(source: Arc<S>) -> Result<MigrationEstimate> {
349 let all_cids = source.list_cids()?;
350 let total_blocks = all_cids.len();
351
352 let sample_size = total_blocks.min(100);
354 let sample_cids: Vec<_> = all_cids.iter().take(sample_size).copied().collect();
355
356 let blocks = source.get_many(&sample_cids).await?;
357 let sample_bytes: u64 = blocks
358 .iter()
359 .filter_map(|b| b.as_ref())
360 .map(|b| b.data().len() as u64)
361 .sum();
362
363 let avg_block_size = if sample_size > 0 {
364 sample_bytes / sample_size as u64
365 } else {
366 0
367 };
368
369 let total_bytes = avg_block_size * total_blocks as u64;
370
371 let estimated_duration_low = Duration::from_secs(total_blocks as u64 / 100);
373 let estimated_duration_high = Duration::from_secs(total_blocks as u64 / 1000);
374
375 Ok(MigrationEstimate {
376 total_blocks,
377 total_bytes,
378 estimated_duration_low,
379 estimated_duration_high,
380 space_required: total_bytes,
381 })
382}
383
384pub async fn validate_migration<S: BlockStore, D: BlockStore>(
386 source: Arc<S>,
387 destination: Arc<D>,
388) -> Result<bool> {
389 let source_cids = source.list_cids()?;
390 let dest_cids = destination.list_cids()?;
391
392 if source_cids.len() != dest_cids.len() {
394 return Ok(false);
395 }
396
397 let exists = destination.has_many(&source_cids).await?;
399 Ok(exists.iter().all(|&e| e))
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
408pub struct SchemaVersion(pub u32);
409
410impl SchemaVersion {
411 pub const V1: SchemaVersion = SchemaVersion(1);
413 pub const V2: SchemaVersion = SchemaVersion(2);
415 pub const V3: SchemaVersion = SchemaVersion(3);
417}
418
419impl fmt::Display for SchemaVersion {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write!(f, "v{}", self.0)
422 }
423}
424
425impl From<u32> for SchemaVersion {
426 fn from(v: u32) -> Self {
427 SchemaVersion(v)
428 }
429}
430
431impl From<SchemaVersion> for u32 {
432 fn from(v: SchemaVersion) -> Self {
433 v.0
434 }
435}
436
437#[derive(Debug, Clone)]
439pub struct MigrationStep {
440 pub from_version: SchemaVersion,
442 pub to_version: SchemaVersion,
444 pub description: String,
446 pub is_reversible: bool,
448}
449
450impl MigrationStep {
451 pub fn new(
453 from_version: SchemaVersion,
454 to_version: SchemaVersion,
455 description: impl Into<String>,
456 is_reversible: bool,
457 ) -> Self {
458 Self {
459 from_version,
460 to_version,
461 description: description.into(),
462 is_reversible,
463 }
464 }
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum MigrationStatus {
470 Pending,
472 Running,
474 Completed,
476 Failed {
478 reason: String,
480 },
481 RolledBack,
483}
484
485#[derive(Debug, Clone)]
487pub struct MigrationRecord {
488 pub step: MigrationStep,
490 pub started_at_ms: u64,
492 pub completed_at_ms: Option<u64>,
494 pub blocks_migrated: u64,
496 pub status: MigrationStatus,
498}
499
500#[derive(Debug, Error)]
502pub enum MigrationError {
503 #[error("no migration path found from v{from} to v{to}")]
505 NoPathFound {
506 from: u32,
508 to: u32,
510 },
511 #[error("step v{from}→v{to} failed: {reason}")]
513 StepFailed {
514 from: u32,
516 to: u32,
518 reason: String,
520 },
521 #[error("already at version v{0}")]
523 AlreadyAtVersion(u32),
524 #[error("migration is not reversible: {0}")]
526 NotReversible(String),
527}
528
529#[derive(Debug, Clone)]
531pub struct MigrationPlan {
532 pub steps: Vec<MigrationStep>,
534 pub current_version: SchemaVersion,
536 pub target_version: SchemaVersion,
538}
539
540impl MigrationPlan {
541 pub fn build(
546 current: SchemaVersion,
547 target: SchemaVersion,
548 available: &[MigrationStep],
549 ) -> std::result::Result<Self, MigrationError> {
550 if current == target {
551 return Err(MigrationError::AlreadyAtVersion(current.0));
552 }
553
554 let mut queue: Vec<(SchemaVersion, Vec<MigrationStep>)> = vec![(current, Vec::new())];
556 let mut visited = std::collections::HashSet::new();
557 visited.insert(current);
558
559 while let Some((version, path)) = queue.pop() {
560 for step in available.iter().filter(|s| s.from_version == version) {
561 let mut new_path = path.clone();
562 new_path.push(step.clone());
563
564 if step.to_version == target {
565 return Ok(Self {
566 steps: new_path,
567 current_version: current,
568 target_version: target,
569 });
570 }
571
572 if !visited.contains(&step.to_version) {
573 visited.insert(step.to_version);
574 queue.push((step.to_version, new_path));
575 }
576 }
577 }
578
579 Err(MigrationError::NoPathFound {
580 from: current.0,
581 to: target.0,
582 })
583 }
584
585 pub fn step_count(&self) -> usize {
587 self.steps.len()
588 }
589
590 pub fn is_empty(&self) -> bool {
592 self.steps.is_empty()
593 }
594
595 pub fn is_reversible(&self) -> bool {
597 self.steps.iter().all(|s| s.is_reversible)
598 }
599}
600
601#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct MigrationStatsSnapshot {
604 pub total_steps_run: u64,
606 pub total_blocks_migrated: u64,
608 pub total_failures: u64,
610}
611
612#[derive(Debug, Default)]
614pub struct MigrationStats {
615 pub total_steps_run: AtomicU64,
617 pub total_blocks_migrated: AtomicU64,
619 pub total_failures: AtomicU64,
621}
622
623impl MigrationStats {
624 pub fn new() -> Self {
626 Self::default()
627 }
628
629 pub fn snapshot(&self) -> MigrationStatsSnapshot {
631 MigrationStatsSnapshot {
632 total_steps_run: self.total_steps_run.load(Ordering::SeqCst),
633 total_blocks_migrated: self.total_blocks_migrated.load(Ordering::SeqCst),
634 total_failures: self.total_failures.load(Ordering::SeqCst),
635 }
636 }
637}
638
639fn now_ms() -> u64 {
641 std::time::SystemTime::now()
642 .duration_since(std::time::UNIX_EPOCH)
643 .unwrap_or(Duration::ZERO)
644 .as_millis() as u64
645}
646
647pub struct MigrationRunner {
652 pub history: Mutex<Vec<MigrationRecord>>,
654 pub current_version: AtomicU32,
656 pub stats: MigrationStats,
658}
659
660impl MigrationRunner {
661 pub fn new(initial_version: SchemaVersion) -> Self {
663 Self {
664 history: Mutex::new(Vec::new()),
665 current_version: AtomicU32::new(initial_version.0),
666 stats: MigrationStats::new(),
667 }
668 }
669
670 pub fn execute_step(
674 &self,
675 step: &MigrationStep,
676 blocks_count: u64,
677 ) -> std::result::Result<MigrationRecord, MigrationError> {
678 let started_at_ms = now_ms();
679
680 let running_record = MigrationRecord {
682 step: step.clone(),
683 started_at_ms,
684 completed_at_ms: None,
685 blocks_migrated: 0,
686 status: MigrationStatus::Running,
687 };
688
689 let completed_at_ms = now_ms();
692
693 let record = MigrationRecord {
694 step: step.clone(),
695 started_at_ms: running_record.started_at_ms,
696 completed_at_ms: Some(completed_at_ms),
697 blocks_migrated: blocks_count,
698 status: MigrationStatus::Completed,
699 };
700
701 self.current_version
703 .store(step.to_version.0, Ordering::SeqCst);
704
705 self.stats.total_steps_run.fetch_add(1, Ordering::SeqCst);
707 self.stats
708 .total_blocks_migrated
709 .fetch_add(blocks_count, Ordering::SeqCst);
710
711 {
713 let mut guard = self
714 .history
715 .lock()
716 .map_err(|_| MigrationError::StepFailed {
717 from: step.from_version.0,
718 to: step.to_version.0,
719 reason: "history mutex poisoned".to_string(),
720 })?;
721 guard.push(record.clone());
722 }
723
724 Ok(record)
725 }
726
727 pub fn execute_plan(
732 &self,
733 plan: &MigrationPlan,
734 blocks_per_step: u64,
735 ) -> std::result::Result<Vec<MigrationRecord>, MigrationError> {
736 let mut records = Vec::with_capacity(plan.steps.len());
737
738 for step in &plan.steps {
739 match self.execute_step(step, blocks_per_step) {
740 Ok(record) => records.push(record),
741 Err(err) => {
742 self.stats.total_failures.fetch_add(1, Ordering::SeqCst);
743 return Err(err);
744 }
745 }
746 }
747
748 Ok(records)
749 }
750
751 pub fn current_version(&self) -> SchemaVersion {
753 SchemaVersion(self.current_version.load(Ordering::SeqCst))
754 }
755
756 pub fn history(&self) -> Vec<MigrationRecord> {
758 self.history.lock().map(|g| g.clone()).unwrap_or_default()
759 }
760
761 pub fn can_rollback(&self) -> bool {
763 self.history
764 .lock()
765 .map(|g| g.last().map(|r| r.step.is_reversible).unwrap_or(false))
766 .unwrap_or(false)
767 }
768}
769
770#[cfg(test)]
771mod schema_migration_tests {
772 use super::{
773 MigrationError, MigrationPlan, MigrationRunner, MigrationStatus, MigrationStep,
774 SchemaVersion,
775 };
776
777 fn step(from: u32, to: u32, reversible: bool) -> MigrationStep {
780 MigrationStep::new(
781 SchemaVersion(from),
782 SchemaVersion(to),
783 format!("v{from}→v{to}"),
784 reversible,
785 )
786 }
787
788 fn v1_to_v3_steps() -> Vec<MigrationStep> {
789 vec![step(1, 2, true), step(2, 3, true)]
790 }
791
792 #[test]
795 fn schema_version_display() {
796 assert_eq!(SchemaVersion::V1.to_string(), "v1");
797 assert_eq!(SchemaVersion::V3.to_string(), "v3");
798 }
799
800 #[test]
801 fn schema_version_constants() {
802 assert_eq!(SchemaVersion::V1.0, 1);
803 assert_eq!(SchemaVersion::V2.0, 2);
804 assert_eq!(SchemaVersion::V3.0, 3);
805 }
806
807 #[test]
810 fn plan_build_finds_direct_path() {
811 let steps = vec![step(1, 2, true)];
812 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V2, &steps)
813 .expect("should find direct step");
814 assert_eq!(plan.step_count(), 1);
815 assert_eq!(plan.current_version, SchemaVersion::V1);
816 assert_eq!(plan.target_version, SchemaVersion::V2);
817 }
818
819 #[test]
820 fn plan_build_finds_multi_step_path() {
821 let steps = v1_to_v3_steps();
822 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &steps)
823 .expect("should find two-step path");
824 assert_eq!(plan.step_count(), 2);
825 }
826
827 #[test]
828 fn plan_build_error_when_no_path() {
829 let steps = vec![step(2, 3, true)];
831 let result = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &steps);
832 assert!(matches!(
833 result,
834 Err(MigrationError::NoPathFound { from: 1, to: 3 })
835 ));
836 }
837
838 #[test]
839 fn plan_build_already_at_version() {
840 let steps = v1_to_v3_steps();
841 let result = MigrationPlan::build(SchemaVersion::V2, SchemaVersion::V2, &steps);
842 assert!(matches!(result, Err(MigrationError::AlreadyAtVersion(2))));
843 }
844
845 #[test]
846 fn plan_is_reversible_all_true() {
847 let steps = vec![step(1, 2, true), step(2, 3, true)];
848 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &steps).unwrap();
849 assert!(plan.is_reversible());
850 }
851
852 #[test]
853 fn plan_is_reversible_false_when_any_irreversible() {
854 let steps = vec![step(1, 2, true), step(2, 3, false)];
855 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &steps).unwrap();
856 assert!(!plan.is_reversible());
857 }
858
859 #[test]
862 fn execute_step_records_history() {
863 let runner = MigrationRunner::new(SchemaVersion::V1);
864 let s = step(1, 2, true);
865 let record = runner.execute_step(&s, 42).expect("step should succeed");
866
867 assert_eq!(record.status, MigrationStatus::Completed);
868 assert_eq!(record.blocks_migrated, 42);
869 assert!(record.completed_at_ms.is_some());
870
871 let history = runner.history();
872 assert_eq!(history.len(), 1);
873 }
874
875 #[test]
876 fn execute_step_updates_current_version() {
877 let runner = MigrationRunner::new(SchemaVersion::V1);
878 runner.execute_step(&step(1, 2, true), 0).unwrap();
879 assert_eq!(runner.current_version(), SchemaVersion::V2);
880 }
881
882 #[test]
885 fn execute_plan_runs_all_steps() {
886 let runner = MigrationRunner::new(SchemaVersion::V1);
887 let available = v1_to_v3_steps();
888 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &available).unwrap();
889 let records = runner
890 .execute_plan(&plan, 100)
891 .expect("plan should succeed");
892
893 assert_eq!(records.len(), 2);
894 assert_eq!(runner.current_version(), SchemaVersion::V3);
895 }
896
897 #[test]
898 fn execute_plan_updates_version_to_target() {
899 let runner = MigrationRunner::new(SchemaVersion::V1);
900 let available = v1_to_v3_steps();
901 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &available).unwrap();
902 runner.execute_plan(&plan, 50).unwrap();
903 assert_eq!(runner.current_version(), SchemaVersion::V3);
904 }
905
906 #[test]
909 fn stats_accumulate_across_steps() {
910 let runner = MigrationRunner::new(SchemaVersion::V1);
911 runner.execute_step(&step(1, 2, true), 10).unwrap();
912 runner.execute_step(&step(2, 3, true), 20).unwrap();
913
914 let snap = runner.stats.snapshot();
915 assert_eq!(snap.total_steps_run, 2);
916 assert_eq!(snap.total_blocks_migrated, 30);
917 assert_eq!(snap.total_failures, 0);
918 }
919
920 #[test]
923 fn can_rollback_true_when_last_step_reversible() {
924 let runner = MigrationRunner::new(SchemaVersion::V1);
925 runner.execute_step(&step(1, 2, true), 0).unwrap();
926 assert!(runner.can_rollback());
927 }
928
929 #[test]
930 fn can_rollback_false_when_last_step_irreversible() {
931 let runner = MigrationRunner::new(SchemaVersion::V1);
932 runner.execute_step(&step(1, 2, false), 0).unwrap();
933 assert!(!runner.can_rollback());
934 }
935
936 #[test]
937 fn can_rollback_false_when_no_history() {
938 let runner = MigrationRunner::new(SchemaVersion::V1);
939 assert!(!runner.can_rollback());
940 }
941
942 #[test]
945 fn multi_step_plan_history_grows() {
946 let runner = MigrationRunner::new(SchemaVersion::V1);
947 let available = v1_to_v3_steps();
948 let plan = MigrationPlan::build(SchemaVersion::V1, SchemaVersion::V3, &available).unwrap();
949 runner.execute_plan(&plan, 5).unwrap();
950
951 let history = runner.history();
952 assert_eq!(history.len(), 2);
953 assert_eq!(history[0].step.from_version, SchemaVersion::V1);
954 assert_eq!(history[1].step.from_version, SchemaVersion::V2);
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961 use crate::MemoryBlockStore;
962 use bytes::Bytes;
963 use ipfrs_core::Block;
964
965 #[tokio::test]
966 async fn test_basic_migration() {
967 let source = Arc::new(MemoryBlockStore::new());
968 let destination = Arc::new(MemoryBlockStore::new());
969
970 for i in 0..10 {
972 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
973 source.put(&block).await.unwrap();
974 }
975
976 assert_eq!(source.len(), 10);
977 assert_eq!(destination.len(), 0);
978
979 let stats = migrate_storage(source.clone(), destination.clone())
981 .await
982 .unwrap();
983
984 assert_eq!(stats.blocks_migrated, 10);
985 assert_eq!(stats.blocks_skipped, 0);
986 assert_eq!(stats.errors, 0);
987 assert_eq!(destination.len(), 10);
988 }
989
990 #[tokio::test]
991 async fn test_migration_skip_existing() {
992 let source = Arc::new(MemoryBlockStore::new());
993 let destination = Arc::new(MemoryBlockStore::new());
994
995 let mut blocks = Vec::new();
997 for i in 0..10 {
998 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
999 blocks.push(block);
1000 }
1001
1002 for block in &blocks {
1004 source.put(block).await.unwrap();
1005 }
1006
1007 for block in blocks.iter().take(5) {
1009 destination.put(block).await.unwrap();
1010 }
1011
1012 let config = MigrationConfig {
1014 skip_existing: true,
1015 ..Default::default()
1016 };
1017 let migrator = StorageMigrator::with_config(source, destination.clone(), config);
1018 let stats = migrator.migrate_all().await.unwrap();
1019
1020 assert_eq!(stats.blocks_migrated, 5); assert_eq!(stats.blocks_skipped, 5); assert_eq!(destination.len(), 10);
1023 }
1024
1025 #[tokio::test]
1026 async fn test_migration_with_progress() {
1027 let source = Arc::new(MemoryBlockStore::new());
1028 let destination = Arc::new(MemoryBlockStore::new());
1029
1030 for i in 0..20 {
1032 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
1033 source.put(&block).await.unwrap();
1034 }
1035
1036 let progress_called = Arc::new(AtomicU64::new(0));
1037 let progress_called_clone = progress_called.clone();
1038
1039 let stats = migrate_storage_with_progress(source, destination, move |_current, _total| {
1040 progress_called_clone.fetch_add(1, Ordering::Relaxed);
1041 })
1042 .await
1043 .unwrap();
1044
1045 assert_eq!(stats.blocks_migrated, 20);
1046 assert!(progress_called.load(Ordering::Relaxed) > 0);
1047 }
1048
1049 #[tokio::test]
1050 async fn test_migrate_storage_batched() {
1051 let source = Arc::new(MemoryBlockStore::new());
1052 let destination = Arc::new(MemoryBlockStore::new());
1053
1054 for i in 0..50 {
1056 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
1057 source.put(&block).await.unwrap();
1058 }
1059
1060 let stats = migrate_storage_batched(source, destination.clone(), 10)
1061 .await
1062 .unwrap();
1063
1064 assert_eq!(stats.blocks_migrated, 50);
1065 assert_eq!(destination.len(), 50);
1066 }
1067
1068 #[tokio::test]
1069 async fn test_estimate_migration() {
1070 let source = Arc::new(MemoryBlockStore::new());
1071
1072 for i in 0..100 {
1074 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
1075 source.put(&block).await.unwrap();
1076 }
1077
1078 let estimate = estimate_migration(source).await.unwrap();
1079
1080 assert_eq!(estimate.total_blocks, 100);
1081 assert!(estimate.total_bytes > 0);
1082 assert!(estimate.space_required > 0);
1083 }
1084
1085 #[tokio::test]
1086 async fn test_validate_migration() {
1087 let source = Arc::new(MemoryBlockStore::new());
1088 let destination = Arc::new(MemoryBlockStore::new());
1089
1090 for i in 0..10 {
1092 let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
1093 source.put(&block).await.unwrap();
1094 destination.put(&block).await.unwrap();
1095 }
1096
1097 let valid = validate_migration(source.clone(), destination.clone())
1098 .await
1099 .unwrap();
1100
1101 assert!(valid);
1102
1103 let extra_block = Block::new(Bytes::from("extra")).unwrap();
1105 source.put(&extra_block).await.unwrap();
1106
1107 let valid = validate_migration(source, destination).await.unwrap();
1108 assert!(!valid);
1109 }
1110}