Skip to main content

crdt_migrate/
engine.rs

1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::fmt;
5
6/// A single migration step that transforms data from one version to the next.
7///
8/// Migrations form a linear chain: v1→v2, v2→v3, etc.
9/// Each step must be **deterministic and pure** — two devices running the
10/// same migration on the same data must produce identical results.
11pub trait MigrationStep: Send + Sync {
12    /// Source version.
13    fn source_version(&self) -> u32;
14    /// Target version.
15    fn target_version(&self) -> u32;
16    /// Transform serialized data from source to target version.
17    fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, MigrationError>;
18}
19
20/// Error during migration.
21#[derive(Debug, Clone, PartialEq)]
22pub enum MigrationError {
23    /// No migration path exists between the source and target versions.
24    NoPath { from: u32, to: u32 },
25    /// A migration step failed.
26    StepFailed { from: u32, to: u32, reason: String },
27    /// The version chain has a gap (e.g., v1→v3 without v2).
28    GapInChain { missing: u32 },
29    /// The source version is newer than the current version (forward compat).
30    FutureVersion { found: u32, current: u32 },
31    /// Deserialization failed.
32    Deserialization(String),
33    /// Serialization failed.
34    Serialization(String),
35}
36
37impl fmt::Display for MigrationError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::NoPath { from, to } => {
41                write!(f, "no migration path from v{from} to v{to}")
42            }
43            Self::StepFailed { from, to, reason } => {
44                write!(f, "migration v{from}→v{to} failed: {reason}")
45            }
46            Self::GapInChain { missing } => {
47                write!(f, "missing migration step for v{missing}")
48            }
49            Self::FutureVersion { found, current } => {
50                write!(f, "data version v{found} is newer than current v{current}")
51            }
52            Self::Deserialization(msg) => write!(f, "deserialization error: {msg}"),
53            Self::Serialization(msg) => write!(f, "serialization error: {msg}"),
54        }
55    }
56}
57
58/// Configuration for the migration engine.
59#[derive(Debug, Clone)]
60pub struct MigrationConfig {
61    /// If true, re-write migrated data back to storage after reading.
62    /// Recommended for SQLite/redb. Not recommended for flash storage.
63    pub write_back_on_read: bool,
64    /// If true, migrate all data eagerly on startup instead of lazily.
65    pub eager_migration: bool,
66}
67
68impl Default for MigrationConfig {
69    fn default() -> Self {
70        Self {
71            write_back_on_read: true,
72            eager_migration: false,
73        }
74    }
75}
76
77/// The migration engine that runs a chain of migration steps.
78///
79/// Steps are registered in order and form a linear chain.
80/// When data at version N needs to reach version M (where N < M),
81/// the engine runs steps N→N+1, N+1→N+2, ..., M-1→M in sequence.
82///
83/// # Example
84///
85/// ```
86/// use crdt_migrate::{MigrationEngine, MigrationStep, MigrationError};
87///
88/// struct AddFieldMigration;
89///
90/// impl MigrationStep for AddFieldMigration {
91///     fn source_version(&self) -> u32 { 1 }
92///     fn target_version(&self) -> u32 { 2 }
93///     fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, MigrationError> {
94///         // In real code: deserialize v1, create v2 with new fields, serialize
95///         let mut result = data.to_vec();
96///         result.extend_from_slice(b"|humidity=none");
97///         Ok(result)
98///     }
99/// }
100///
101/// let mut engine = MigrationEngine::new(2); // current version = 2
102/// engine.register(Box::new(AddFieldMigration));
103///
104/// let v1_data = b"temp=22.5";
105/// let v2_data = engine.migrate_to_current(v1_data, 1).unwrap();
106/// assert_eq!(v2_data, b"temp=22.5|humidity=none");
107/// ```
108pub struct MigrationEngine {
109    current_version: u32,
110    steps: Vec<Box<dyn MigrationStep>>,
111}
112
113impl MigrationEngine {
114    /// Create a new engine targeting `current_version`.
115    pub fn new(current_version: u32) -> Self {
116        Self {
117            current_version,
118            steps: Vec::new(),
119        }
120    }
121
122    /// Register a migration step.
123    pub fn register(&mut self, step: Box<dyn MigrationStep>) {
124        self.steps.push(step);
125        // Keep sorted by from_version for efficient lookup
126        self.steps.sort_by_key(|s| s.source_version());
127    }
128
129    /// The current (target) schema version.
130    pub fn current_version(&self) -> u32 {
131        self.current_version
132    }
133
134    /// Check if data needs migration.
135    pub fn needs_migration(&self, data_version: u32) -> bool {
136        data_version != self.current_version
137    }
138
139    /// Migrate data from `from_version` to `current_version`.
140    ///
141    /// Runs the chain of steps sequentially. Each step receives the
142    /// output of the previous step.
143    pub fn migrate_to_current(
144        &self,
145        data: &[u8],
146        from_version: u32,
147    ) -> Result<Vec<u8>, MigrationError> {
148        if from_version == self.current_version {
149            return Ok(data.to_vec());
150        }
151
152        if from_version > self.current_version {
153            return Err(MigrationError::FutureVersion {
154                found: from_version,
155                current: self.current_version,
156            });
157        }
158
159        let mut current_data = data.to_vec();
160        let mut version = from_version;
161
162        while version < self.current_version {
163            let step = self
164                .steps
165                .iter()
166                .find(|s| s.source_version() == version)
167                .ok_or(MigrationError::GapInChain { missing: version })?;
168
169            current_data = step
170                .migrate(&current_data)
171                .map_err(|e| MigrationError::StepFailed {
172                    from: version,
173                    to: step.target_version(),
174                    reason: e.to_string(),
175                })?;
176
177            version = step.target_version();
178        }
179
180        Ok(current_data)
181    }
182
183    /// Validate that the migration chain is complete from `min_version` to `current_version`.
184    pub fn validate_chain(&self, min_version: u32) -> Result<(), MigrationError> {
185        let mut version = min_version;
186        while version < self.current_version {
187            if !self.steps.iter().any(|s| s.source_version() == version) {
188                return Err(MigrationError::GapInChain { missing: version });
189            }
190            version += 1;
191        }
192        Ok(())
193    }
194
195    /// List all registered migration steps as (from, to) pairs.
196    pub fn registered_steps(&self) -> Vec<(u32, u32)> {
197        self.steps
198            .iter()
199            .map(|s| (s.source_version(), s.target_version()))
200            .collect()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    struct SimpleStep {
209        from: u32,
210        to: u32,
211        suffix: &'static [u8],
212    }
213
214    impl MigrationStep for SimpleStep {
215        fn source_version(&self) -> u32 {
216            self.from
217        }
218        fn target_version(&self) -> u32 {
219            self.to
220        }
221        fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, MigrationError> {
222            let mut result = data.to_vec();
223            result.extend_from_slice(self.suffix);
224            Ok(result)
225        }
226    }
227
228    #[test]
229    fn no_migration_needed() {
230        let engine = MigrationEngine::new(1);
231        let data = b"hello";
232        let result = engine.migrate_to_current(data, 1).unwrap();
233        assert_eq!(result, b"hello");
234    }
235
236    #[test]
237    fn single_step_migration() {
238        let mut engine = MigrationEngine::new(2);
239        engine.register(Box::new(SimpleStep {
240            from: 1,
241            to: 2,
242            suffix: b"+v2",
243        }));
244
245        let result = engine.migrate_to_current(b"data", 1).unwrap();
246        assert_eq!(result, b"data+v2");
247    }
248
249    #[test]
250    fn multi_step_chain() {
251        let mut engine = MigrationEngine::new(4);
252        engine.register(Box::new(SimpleStep {
253            from: 1,
254            to: 2,
255            suffix: b"+v2",
256        }));
257        engine.register(Box::new(SimpleStep {
258            from: 2,
259            to: 3,
260            suffix: b"+v3",
261        }));
262        engine.register(Box::new(SimpleStep {
263            from: 3,
264            to: 4,
265            suffix: b"+v4",
266        }));
267
268        // Full chain: v1 → v4
269        let result = engine.migrate_to_current(b"v1", 1).unwrap();
270        assert_eq!(result, b"v1+v2+v3+v4");
271
272        // Partial chain: v2 → v4
273        let result = engine.migrate_to_current(b"v2", 2).unwrap();
274        assert_eq!(result, b"v2+v3+v4");
275
276        // Single step: v3 → v4
277        let result = engine.migrate_to_current(b"v3", 3).unwrap();
278        assert_eq!(result, b"v3+v4");
279    }
280
281    #[test]
282    fn future_version_error() {
283        let engine = MigrationEngine::new(2);
284        let err = engine.migrate_to_current(b"data", 5).unwrap_err();
285        assert_eq!(
286            err,
287            MigrationError::FutureVersion {
288                found: 5,
289                current: 2
290            }
291        );
292    }
293
294    #[test]
295    fn gap_in_chain_error() {
296        let mut engine = MigrationEngine::new(3);
297        engine.register(Box::new(SimpleStep {
298            from: 1,
299            to: 2,
300            suffix: b"+v2",
301        }));
302        // Missing v2→v3 step
303
304        let err = engine.migrate_to_current(b"data", 1).unwrap_err();
305        assert_eq!(err, MigrationError::GapInChain { missing: 2 });
306    }
307
308    #[test]
309    fn validate_chain_ok() {
310        let mut engine = MigrationEngine::new(3);
311        engine.register(Box::new(SimpleStep {
312            from: 1,
313            to: 2,
314            suffix: b"",
315        }));
316        engine.register(Box::new(SimpleStep {
317            from: 2,
318            to: 3,
319            suffix: b"",
320        }));
321
322        assert!(engine.validate_chain(1).is_ok());
323    }
324
325    #[test]
326    fn validate_chain_gap() {
327        let mut engine = MigrationEngine::new(3);
328        engine.register(Box::new(SimpleStep {
329            from: 1,
330            to: 2,
331            suffix: b"",
332        }));
333        // Missing v2→v3
334
335        let err = engine.validate_chain(1).unwrap_err();
336        assert_eq!(err, MigrationError::GapInChain { missing: 2 });
337    }
338
339    #[test]
340    fn needs_migration() {
341        let engine = MigrationEngine::new(3);
342        assert!(engine.needs_migration(1));
343        assert!(engine.needs_migration(2));
344        assert!(!engine.needs_migration(3));
345    }
346
347    #[test]
348    fn registered_steps_list() {
349        let mut engine = MigrationEngine::new(3);
350        engine.register(Box::new(SimpleStep {
351            from: 2,
352            to: 3,
353            suffix: b"",
354        }));
355        engine.register(Box::new(SimpleStep {
356            from: 1,
357            to: 2,
358            suffix: b"",
359        }));
360
361        let steps = engine.registered_steps();
362        assert_eq!(steps, vec![(1, 2), (2, 3)]); // sorted by from
363    }
364}