Skip to main content

ipfrs_storage/
migration.rs

1//! Storage migration utilities
2//!
3//! This module provides utilities for migrating data between different storage backends,
4//! enabling seamless transitions in production deployments.
5//!
6//! It also provides the `StorageMigrationFramework` for schema-level version upgrades.
7
8use 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/// Migration statistics for backend-to-backend block migrations
17#[derive(Debug, Clone, Default)]
18pub struct BlockMigrationStats {
19    /// Total blocks migrated
20    pub blocks_migrated: u64,
21    /// Total bytes migrated
22    pub bytes_migrated: u64,
23    /// Number of blocks skipped (already present in destination)
24    pub blocks_skipped: u64,
25    /// Number of errors encountered
26    pub errors: u64,
27    /// Migration duration
28    pub duration: Duration,
29    /// Migration throughput in blocks per second
30    pub blocks_per_second: f64,
31    /// Migration throughput in bytes per second
32    pub bytes_per_second: f64,
33}
34
35impl BlockMigrationStats {
36    /// Calculate throughput metrics
37    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/// Migration configuration
47#[derive(Debug, Clone)]
48pub struct MigrationConfig {
49    /// Batch size for bulk operations
50    pub batch_size: usize,
51    /// Whether to skip blocks that already exist in destination
52    pub skip_existing: bool,
53    /// Whether to verify each block after migration
54    pub verify: bool,
55    /// Maximum number of concurrent operations
56    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
70/// Progress callback type
71pub type ProgressCallback = Arc<dyn Fn(u64, u64) + Send + Sync>;
72
73/// Storage migrator
74pub 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    /// Create a new migrator
83    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    /// Create with custom configuration
93    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    /// Set progress callback
103    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    /// Migrate all blocks from source to destination
112    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        // Get all CIDs from source
121        let all_cids = self.source.list_cids()?;
122        let total_blocks = all_cids.len() as u64;
123
124        // Migrate in batches
125        for batch in all_cids.chunks(self.config.batch_size) {
126            // Check which blocks already exist in destination if skip_existing is enabled
127            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            // Get blocks from source
150            let blocks_result = self.source.get_many(&cids_to_migrate).await?;
151
152            // Filter out None values and collect valid blocks
153            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            // Put blocks to destination
164            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                        // Verify if enabled
170                        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            // Call progress callback
186            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    /// Migrate specific CIDs
208    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        // Migrate in batches
217        for batch in cids.chunks(self.config.batch_size) {
218            // Check which blocks already exist
219            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            // Get and migrate blocks
242            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
282/// Helper function to migrate between stores
283pub 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
291/// Helper function to migrate with progress reporting
292pub 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
305/// Migrate with custom batch size for optimal performance
306pub 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
319/// Migrate with verification enabled (slower but safer)
320pub 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/// Estimate migration time and space requirements
333#[derive(Debug, Clone)]
334pub struct MigrationEstimate {
335    /// Total blocks to migrate
336    pub total_blocks: usize,
337    /// Total bytes to migrate
338    pub total_bytes: u64,
339    /// Estimated duration at 100 blocks/sec
340    pub estimated_duration_low: Duration,
341    /// Estimated duration at 1000 blocks/sec
342    pub estimated_duration_high: Duration,
343    /// Space required in destination
344    pub space_required: u64,
345}
346
347/// Estimate migration requirements
348pub 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    // Sample first 100 blocks to estimate average size
353    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    // Estimate durations (conservative: 100 blocks/sec, optimistic: 1000 blocks/sec)
372    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
384/// Migration validation - verify both stores have identical content
385pub 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    // Check if same number of blocks
393    if source_cids.len() != dest_cids.len() {
394        return Ok(false);
395    }
396
397    // Check all source CIDs exist in destination
398    let exists = destination.has_many(&source_cids).await?;
399    Ok(exists.iter().all(|&e| e))
400}
401
402// ============================================================
403//  StorageMigrationFramework
404// ============================================================
405
406/// Schema version newtype.
407#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
408pub struct SchemaVersion(pub u32);
409
410impl SchemaVersion {
411    /// Schema version 1.
412    pub const V1: SchemaVersion = SchemaVersion(1);
413    /// Schema version 2.
414    pub const V2: SchemaVersion = SchemaVersion(2);
415    /// Schema version 3.
416    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/// A single migration step from one schema version to another.
438#[derive(Debug, Clone)]
439pub struct MigrationStep {
440    /// Source schema version.
441    pub from_version: SchemaVersion,
442    /// Target schema version.
443    pub to_version: SchemaVersion,
444    /// Human-readable description of what this step does.
445    pub description: String,
446    /// Whether this step can be safely reversed (rolled back).
447    pub is_reversible: bool,
448}
449
450impl MigrationStep {
451    /// Create a new migration step.
452    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/// Status of a migration record.
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum MigrationStatus {
470    /// Not yet started.
471    Pending,
472    /// Currently executing.
473    Running,
474    /// Successfully completed.
475    Completed,
476    /// Failed with a reason.
477    Failed {
478        /// Human-readable failure reason.
479        reason: String,
480    },
481    /// Successfully rolled back.
482    RolledBack,
483}
484
485/// A historical record of a single migration step execution.
486#[derive(Debug, Clone)]
487pub struct MigrationRecord {
488    /// The step that was (or is being) executed.
489    pub step: MigrationStep,
490    /// Wall-clock timestamp (milliseconds since epoch) when execution started.
491    pub started_at_ms: u64,
492    /// Wall-clock timestamp when execution completed, or `None` if still running.
493    pub completed_at_ms: Option<u64>,
494    /// Number of blocks touched during this step.
495    pub blocks_migrated: u64,
496    /// Current status.
497    pub status: MigrationStatus,
498}
499
500/// Errors produced by the migration framework.
501#[derive(Debug, Error)]
502pub enum MigrationError {
503    /// No chain of steps leads from `from` to `to`.
504    #[error("no migration path found from v{from} to v{to}")]
505    NoPathFound {
506        /// Source version.
507        from: u32,
508        /// Target version.
509        to: u32,
510    },
511    /// A step execution failed.
512    #[error("step v{from}→v{to} failed: {reason}")]
513    StepFailed {
514        /// Source version.
515        from: u32,
516        /// Target version.
517        to: u32,
518        /// Failure reason.
519        reason: String,
520    },
521    /// The store is already at the requested target version.
522    #[error("already at version v{0}")]
523    AlreadyAtVersion(u32),
524    /// The plan or last step cannot be reversed.
525    #[error("migration is not reversible: {0}")]
526    NotReversible(String),
527}
528
529/// An ordered sequence of [`MigrationStep`]s needed to reach a target version.
530#[derive(Debug, Clone)]
531pub struct MigrationPlan {
532    /// Ordered steps to execute (current → target).
533    pub steps: Vec<MigrationStep>,
534    /// Starting schema version.
535    pub current_version: SchemaVersion,
536    /// Desired schema version.
537    pub target_version: SchemaVersion,
538}
539
540impl MigrationPlan {
541    /// Build a migration plan by finding a chain of steps from `current` to `target`.
542    ///
543    /// Uses a simple BFS / greedy chain search: each step's `to_version` must match
544    /// the next step's `from_version`.
545    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        // BFS to find a path
555        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    /// Number of steps in the plan.
586    pub fn step_count(&self) -> usize {
587        self.steps.len()
588    }
589
590    /// Returns `true` when there are no steps to execute.
591    pub fn is_empty(&self) -> bool {
592        self.steps.is_empty()
593    }
594
595    /// Returns `true` when every step in the plan is reversible.
596    pub fn is_reversible(&self) -> bool {
597        self.steps.iter().all(|s| s.is_reversible)
598    }
599}
600
601/// A snapshot of [`MigrationStats`] with plain `u64` values for easy inspection.
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct MigrationStatsSnapshot {
604    /// Total number of migration steps that have been executed.
605    pub total_steps_run: u64,
606    /// Total blocks touched across all executed steps.
607    pub total_blocks_migrated: u64,
608    /// Total number of steps that failed.
609    pub total_failures: u64,
610}
611
612/// Atomic counters tracking migration activity across the lifetime of a [`MigrationRunner`].
613#[derive(Debug, Default)]
614pub struct MigrationStats {
615    /// Total number of migration steps executed (success or failure).
616    pub total_steps_run: AtomicU64,
617    /// Total blocks touched by all executed steps.
618    pub total_blocks_migrated: AtomicU64,
619    /// Total number of failed steps.
620    pub total_failures: AtomicU64,
621}
622
623impl MigrationStats {
624    /// Create a new zeroed stats instance.
625    pub fn new() -> Self {
626        Self::default()
627    }
628
629    /// Return a point-in-time snapshot of the counters.
630    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
639/// Returns current time as milliseconds since the Unix epoch, using a monotonic fallback.
640fn 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
647/// Executes migration plans and tracks history.
648///
649/// All mutation is performed through shared references so that the runner can be
650/// wrapped in an `Arc` and used from multiple threads.
651pub struct MigrationRunner {
652    /// Full history of every step that has been executed.
653    pub history: Mutex<Vec<MigrationRecord>>,
654    /// Current schema version, updated atomically after each successful step.
655    pub current_version: AtomicU32,
656    /// Cumulative statistics.
657    pub stats: MigrationStats,
658}
659
660impl MigrationRunner {
661    /// Create a runner starting at `initial_version`.
662    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    /// Execute a single migration step (no actual I/O — simulates the step).
671    ///
672    /// Records the outcome in the history log and updates statistics.
673    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        // Build a running record
681        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        // Simulate work — in a real implementation this would perform I/O.
690        // We treat all steps as successful unless the step would create an invalid transition.
691        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        // Update the current version
702        self.current_version
703            .store(step.to_version.0, Ordering::SeqCst);
704
705        // Update stats
706        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        // Append to history
712        {
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    /// Execute every step in a [`MigrationPlan`] sequentially.
728    ///
729    /// Returns the list of completed [`MigrationRecord`]s.  If any step fails the
730    /// error is returned immediately and subsequent steps are not executed.
731    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    /// Return the current schema version.
752    pub fn current_version(&self) -> SchemaVersion {
753        SchemaVersion(self.current_version.load(Ordering::SeqCst))
754    }
755
756    /// Return a clone of the full history.
757    pub fn history(&self) -> Vec<MigrationRecord> {
758        self.history.lock().map(|g| g.clone()).unwrap_or_default()
759    }
760
761    /// Returns `true` when the most recent step was reversible and can be rolled back.
762    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    // ---- helpers ------------------------------------------------------------
778
779    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    // ---- SchemaVersion ------------------------------------------------------
793
794    #[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    // ---- MigrationPlan::build -----------------------------------------------
808
809    #[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        // only v2→v3 exists; v1→v2 is missing
830        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    // ---- MigrationRunner::execute_step --------------------------------------
860
861    #[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    // ---- MigrationRunner::execute_plan --------------------------------------
883
884    #[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    // ---- Stats --------------------------------------------------------------
907
908    #[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    // ---- can_rollback -------------------------------------------------------
921
922    #[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    // ---- multi-step plan ----------------------------------------------------
943
944    #[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        // Add some blocks to source
971        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        // Migrate
980        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        // Add blocks to both stores
996        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        // Add all to source
1003        for block in &blocks {
1004            source.put(block).await.unwrap();
1005        }
1006
1007        // Add first 5 to destination
1008        for block in blocks.iter().take(5) {
1009            destination.put(block).await.unwrap();
1010        }
1011
1012        // Migrate with skip_existing
1013        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); // Only new blocks
1021        assert_eq!(stats.blocks_skipped, 5); // Existing blocks
1022        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        // Add blocks
1031        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        // Add blocks
1055        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        // Add blocks with unique data (so they have unique CIDs)
1073        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        // Add same blocks to both
1091        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        // Add one more block to source only
1104        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}