Skip to main content

forest/db/migration/
migration_map.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{
5    path::{Path, PathBuf},
6    str::FromStr,
7    sync::{Arc, LazyLock},
8};
9
10use crate::Config;
11use crate::db::migration::v0_22_1::Migration0_22_0_0_22_1;
12use crate::db::migration::v0_26_0::Migration0_25_3_0_26_0;
13use crate::db::migration::v0_31_0::Migration0_30_5_0_31_0;
14use anyhow::Context as _;
15use anyhow::bail;
16use itertools::Itertools;
17use multimap::MultiMap;
18use semver::Version;
19use tracing::debug;
20
21use super::void_migration::MigrationVoid;
22
23/// Migration trait. It is expected that the [`MigrationOperation::migrate`] method will pick up the relevant database
24/// existing under `chain_data_path` and create a new migration database in the same directory.
25pub(super) trait MigrationOperation {
26    fn new(from: Version, to: Version) -> Self
27    where
28        Self: Sized;
29    /// From version
30    fn from(&self) -> &Version;
31    /// To version
32    fn to(&self) -> &Version;
33    /// Performs pre-migration checks. This is the place to check if the database is in a valid
34    /// state and if the migration can be performed. Note that some of the higher-level checks
35    /// (like checking if the database exists) are performed by the [`Migration`].
36    fn pre_checks(&self, chain_data_path: &Path) -> anyhow::Result<()> {
37        let old_db = self.old_db_path(chain_data_path);
38        anyhow::ensure!(
39            old_db.is_dir(),
40            "source database {} does not exist",
41            old_db.display()
42        );
43        let new_db = self.new_db_path(chain_data_path);
44        anyhow::ensure!(
45            !new_db.exists(),
46            "target database {} already exists",
47            new_db.display()
48        );
49        let temp_db = self.temporary_db_path(chain_data_path);
50        if temp_db.exists() {
51            tracing::info!("Removing old temporary database {}", temp_db.display());
52            std::fs::remove_dir_all(&temp_db)?;
53        }
54        Ok(())
55    }
56    /// Performs the actual migration. All the logic should be implemented here.
57    /// Ideally, the migration should use as little of the Forest codebase as possible to avoid
58    /// potential issues with the migration code itself and having to update it in the future.
59    /// Returns the path to the migrated database (which is not yet validated)
60    fn migrate_core(&self, chain_data_path: &Path, config: &Config) -> anyhow::Result<PathBuf>;
61    fn migrate(&self, chain_data_path: &Path, config: &Config) -> anyhow::Result<()> {
62        self.pre_checks(chain_data_path)?;
63        let migrated_db = self.migrate_core(chain_data_path, config)?;
64        self.post_checks(chain_data_path)?;
65
66        let new_db = self.new_db_path(chain_data_path);
67        debug!(
68            "Renaming database {} to {}",
69            migrated_db.display(),
70            new_db.display()
71        );
72        std::fs::rename(migrated_db, new_db)?;
73
74        let old_db = self.old_db_path(chain_data_path);
75        debug!("Deleting database {}", old_db.display());
76        std::fs::remove_dir_all(old_db)?;
77
78        Ok(())
79    }
80    /// Performs post-migration checks. This is the place to check if the migration database is
81    /// ready to be used by Forest and renamed into a versioned database.
82    fn post_checks(&self, chain_data_path: &Path) -> anyhow::Result<()> {
83        let temp_db_path = self.temporary_db_path(chain_data_path);
84        anyhow::ensure!(
85            temp_db_path.exists(),
86            "temp db {} does not exist",
87            temp_db_path.display()
88        );
89        Ok(())
90    }
91}
92
93pub trait MigrationOperationExt {
94    fn old_db_path(&self, chain_data_path: &Path) -> PathBuf;
95
96    fn new_db_path(&self, chain_data_path: &Path) -> PathBuf;
97
98    fn temporary_db_name(&self) -> String;
99
100    fn temporary_db_path(&self, chain_data_path: &Path) -> PathBuf;
101}
102
103impl<T: ?Sized + MigrationOperation> MigrationOperationExt for T {
104    fn old_db_path(&self, chain_data_path: &Path) -> PathBuf {
105        chain_data_path.join(self.from().to_string())
106    }
107
108    fn new_db_path(&self, chain_data_path: &Path) -> PathBuf {
109        chain_data_path.join(self.to().to_string())
110    }
111
112    fn temporary_db_name(&self) -> String {
113        format!("migration_{}_{}", self.from(), self.to()).replace('.', "_")
114    }
115
116    fn temporary_db_path(&self, chain_data_path: &Path) -> PathBuf {
117        chain_data_path.join(self.temporary_db_name())
118    }
119}
120
121/// Migrations map. The key is the starting version and the value is the tuple of the target version
122/// and the [`MigrationOperation`] implementation.
123///
124/// In the future we might want to drop legacy migrations (e.g., to clean-up the database
125/// dependency that may get several breaking changes).
126// If need be, we should introduce "jump" migrations here, e.g. 0.12.0 -> 0.12.2, 0.12.2 -> 0.12.3, etc.
127// This would allow us to skip migrations in case of bugs or just for performance reasons.
128type Migrator = Arc<dyn MigrationOperation + Send + Sync>;
129type MigrationsMap = MultiMap<Version, (Version, Migrator)>;
130
131/// A utility macro to make the migrations easier to declare.
132/// The usage is:
133/// `<FROM version> -> <TO version> @ <Migrator object>`
134macro_rules! create_migrations {
135    ($($from:literal -> $to:literal @ $migration:tt),* $(,)?) => {
136pub(super) static MIGRATIONS: LazyLock<MigrationsMap> = LazyLock::new(|| {
137    MigrationsMap::from_iter(
138        [
139            $((
140            Version::from_str($from).unwrap(),
141            (
142                Version::from_str($to).unwrap(),
143                Arc::new($migration::new(
144                        $from.parse().expect("invalid <from> version"),
145                        $to.parse().expect("invalid <to> version")))
146                as _,
147            )),
148            )*
149        ]
150        .iter()
151        .cloned(),
152    )
153});
154}}
155
156create_migrations!(
157    "0.22.0" -> "0.22.1" @ Migration0_22_0_0_22_1,
158    "0.25.3" -> "0.26.0" @ Migration0_25_3_0_26_0,
159    "0.30.5" -> "0.31.0" @ Migration0_30_5_0_31_0,
160);
161
162/// Creates a migration chain from `start` to `goal`. The chain is chosen to be the shortest
163/// possible. If there are multiple shortest paths, any of them is chosen. This method will use
164/// the pre-defined migrations map.
165pub(super) fn create_migration_chain(
166    start: &Version,
167    goal: &Version,
168) -> anyhow::Result<Vec<Arc<dyn MigrationOperation + Send + Sync>>> {
169    create_migration_chain_from_migrations(start, goal, &MIGRATIONS, |from, to| {
170        Arc::new(MigrationVoid::new(from.clone(), to.clone()))
171    })
172}
173
174/// Same as [`create_migration_chain`], but uses any provided migrations map.
175fn create_migration_chain_from_migrations(
176    start: &Version,
177    goal: &Version,
178    migrations_map: &MigrationsMap,
179    void_migration: impl Fn(&Version, &Version) -> Arc<dyn MigrationOperation + Send + Sync>,
180) -> anyhow::Result<Vec<Arc<dyn MigrationOperation + Send + Sync>>> {
181    let sorted_from_versions = migrations_map.keys().sorted().collect_vec();
182    let result = pathfinding::directed::bfs::bfs(
183        start,
184        |from| {
185            if let Some(migrations) = migrations_map.get_vec(from) {
186                migrations.iter().map(|(to, _)| to).cloned().collect()
187            } else if let Some(&next) =
188                sorted_from_versions.get(sorted_from_versions.partition_point(|&i| i <= from))
189            {
190                // Jump straight to the next smallest from version in the migration map
191                vec![next.clone()]
192            } else if goal > from {
193                // Or to the goal
194                vec![goal.clone()]
195            } else {
196                // Or fail for downgrading
197                vec![]
198            }
199        },
200        |to| to == goal,
201    )
202    .with_context(|| format!("No migration path found from version {start} to {goal}"))?
203    .iter()
204    .tuple_windows()
205    .map(|(from, to)| {
206        migrations_map
207            .get_vec(from)
208            .map(|v| {
209                v.iter()
210                    .find_map(|(version, migration)| {
211                        if version == to {
212                            Some(migration.clone())
213                        } else {
214                            None
215                        }
216                    })
217                    .expect("Migration must exist")
218            })
219            .unwrap_or_else(|| void_migration(from, to))
220    })
221    .collect_vec();
222
223    if result.is_empty() {
224        bail!(
225            "No migration path found from version {start} to {goal}",
226            start = start,
227            goal = goal
228        );
229    }
230
231    Ok(result)
232}
233
234#[cfg(test)]
235mod tests {
236    use std::fs;
237
238    use tempfile::TempDir;
239
240    use super::*;
241    use crate::utils::version::FOREST_VERSION;
242
243    #[test]
244    fn test_possible_to_migrate_to_current_version() {
245        // This test ensures that it is possible to migrate from the oldest supported version to the current
246        // version.
247        let earliest_version = MIGRATIONS
248            .iter_all()
249            .map(|(from, _)| from)
250            .min()
251            .expect("At least one migration must exist");
252        let current_version = &FOREST_VERSION;
253
254        let migrations = create_migration_chain(earliest_version, current_version).unwrap();
255        assert!(!migrations.is_empty());
256    }
257
258    #[test]
259    fn test_ensure_migration_possible_from_anywhere_to_latest() {
260        // This test ensures that it is possible to find migration chain from any version to the
261        // current version.
262        let current_version = &FOREST_VERSION;
263
264        for (from, _) in MIGRATIONS.iter_all() {
265            let migrations = create_migration_chain(from, current_version).unwrap();
266            assert!(!migrations.is_empty());
267        }
268    }
269
270    #[test]
271    fn test_ensure_migration_not_possible_if_higher_than_latest() {
272        // This test ensures that it is not possible to migrate from a version higher than the
273        // current version.
274        let current_version = &FOREST_VERSION;
275
276        let higher_version = Version::new(
277            current_version.major,
278            current_version.minor,
279            current_version.patch + 1,
280        );
281        let migrations = create_migration_chain(&higher_version, current_version);
282        assert!(migrations.is_err());
283    }
284
285    #[test]
286    fn test_migration_down_not_possible() {
287        // This test ensures that it is not possible to migrate down from the latest version.
288        // This is not a strict requirement and we may want to allow this in the future.
289        let current_version = &*FOREST_VERSION;
290
291        for (from, _) in MIGRATIONS.iter_all() {
292            let migrations = create_migration_chain(current_version, from);
293            assert!(migrations.is_err());
294        }
295    }
296
297    #[derive(Debug, Clone)]
298    struct EmptyMigration {
299        from: Version,
300        to: Version,
301    }
302
303    impl MigrationOperation for EmptyMigration {
304        fn pre_checks(&self, _chain_data_path: &Path) -> anyhow::Result<()> {
305            Ok(())
306        }
307
308        fn migrate_core(
309            &self,
310            _chain_data_path: &Path,
311            _config: &Config,
312        ) -> anyhow::Result<PathBuf> {
313            Ok("".into())
314        }
315
316        fn post_checks(&self, _chain_data_path: &Path) -> anyhow::Result<()> {
317            Ok(())
318        }
319
320        fn new(from: Version, to: Version) -> Self
321        where
322            Self: Sized,
323        {
324            Self { from, to }
325        }
326
327        fn from(&self) -> &Version {
328            &self.from
329        }
330
331        fn to(&self) -> &Version {
332            &self.to
333        }
334    }
335
336    fn map_empty_migration(
337        (from, to): (Version, Version),
338    ) -> (
339        Version,
340        (Version, Arc<dyn MigrationOperation + Send + Sync>),
341    ) {
342        (
343            from.clone(),
344            (to.clone(), Arc::new(EmptyMigration::new(from, to)) as _),
345        )
346    }
347
348    #[test]
349    fn test_migration_should_use_shortest_path() {
350        let migrations = MigrationsMap::from_iter(
351            [
352                (Version::new(0, 1, 0), Version::new(0, 2, 0)),
353                (Version::new(0, 2, 0), Version::new(0, 3, 0)),
354                (Version::new(0, 1, 0), Version::new(0, 3, 0)),
355            ]
356            .into_iter()
357            .map(map_empty_migration),
358        );
359
360        let migrations = create_migration_chain_from_migrations(
361            &Version::new(0, 1, 0),
362            &Version::new(0, 3, 0),
363            &migrations,
364            |_, _| unimplemented!("void migration"),
365        )
366        .unwrap();
367
368        // The shortest path is 0.1.0 to 0.3.0 (without going through 0.2.0)
369        assert_eq!(1, migrations.len());
370        assert_eq!(&Version::new(0, 1, 0), migrations[0].from());
371        assert_eq!(&Version::new(0, 3, 0), migrations[0].to());
372    }
373
374    #[test]
375    fn test_migration_complex_path() {
376        let migrations = MigrationsMap::from_iter(
377            [
378                (Version::new(0, 1, 0), Version::new(0, 2, 0)),
379                (Version::new(0, 2, 0), Version::new(0, 3, 0)),
380                (Version::new(0, 1, 0), Version::new(0, 3, 0)),
381                (Version::new(0, 3, 0), Version::new(0, 3, 1)),
382            ]
383            .into_iter()
384            .map(map_empty_migration),
385        );
386
387        let migrations = create_migration_chain_from_migrations(
388            &Version::new(0, 1, 0),
389            &Version::new(0, 3, 1),
390            &migrations,
391            |_, _| unimplemented!("void migration"),
392        )
393        .unwrap();
394
395        // The shortest path is 0.1.0 -> 0.3.0 -> 0.3.1
396        assert_eq!(2, migrations.len());
397        assert_eq!(&Version::new(0, 1, 0), migrations[0].from());
398        assert_eq!(&Version::new(0, 3, 0), migrations[0].to());
399        assert_eq!(&Version::new(0, 3, 0), migrations[1].from());
400        assert_eq!(&Version::new(0, 3, 1), migrations[1].to());
401    }
402
403    #[test]
404    fn test_void_migration() {
405        let migrations = MigrationsMap::from_iter(
406            [
407                (Version::new(0, 12, 1), Version::new(0, 13, 0)),
408                (Version::new(0, 15, 2), Version::new(0, 16, 0)),
409            ]
410            .into_iter()
411            .map(map_empty_migration),
412        );
413
414        let start = Version::new(0, 12, 0);
415        let goal = Version::new(1, 0, 0);
416        let migrations =
417            create_migration_chain_from_migrations(&start, &goal, &migrations, |from, to| {
418                Arc::new(EmptyMigration::new(from.clone(), to.clone()))
419            })
420            .unwrap();
421
422        // The shortest path is 0.12.0 -> 0.12.1 -> 0.13.0 -> 0.15.2 -> 0.16.0 -> 1.0.0
423        assert_eq!(5, migrations.len());
424        for (a, b) in migrations.iter().zip(migrations.iter().skip(1)) {
425            assert_eq!(a.to(), b.from());
426        }
427        assert_eq!(&start, migrations[0].from());
428        assert_eq!(&Version::new(0, 12, 1), migrations[1].from());
429        assert_eq!(&Version::new(0, 13, 0), migrations[2].from());
430        assert_eq!(&Version::new(0, 15, 2), migrations[3].from());
431        assert_eq!(&Version::new(0, 16, 0), migrations[4].from());
432        assert_eq!(&goal, migrations[4].to());
433    }
434
435    #[test]
436    fn test_same_distance_paths_should_yield_any() {
437        let migrations = MigrationsMap::from_iter(
438            [
439                (Version::new(0, 1, 0), Version::new(0, 2, 0)),
440                (Version::new(0, 2, 0), Version::new(0, 4, 0)),
441                (Version::new(0, 1, 0), Version::new(0, 3, 0)),
442                (Version::new(0, 3, 0), Version::new(0, 4, 0)),
443            ]
444            .into_iter()
445            .map(map_empty_migration),
446        );
447
448        let migrations = create_migration_chain_from_migrations(
449            &Version::new(0, 1, 0),
450            &Version::new(0, 4, 0),
451            &migrations,
452            |_, _| unimplemented!("void migration"),
453        )
454        .unwrap();
455
456        // there are two possible shortest paths:
457        // 0.1.0 -> 0.2.0 -> 0.4.0
458        // 0.1.0 -> 0.3.0 -> 0.4.0
459        // Both of them are correct and should be accepted.
460        assert_eq!(2, migrations.len());
461        if migrations[0].to() == &Version::new(0, 2, 0) {
462            assert_eq!(&Version::new(0, 1, 0), migrations[0].from());
463            assert_eq!(&Version::new(0, 2, 0), migrations[0].to());
464            assert_eq!(&Version::new(0, 2, 0), migrations[1].from());
465            assert_eq!(&Version::new(0, 4, 0), migrations[1].to());
466        } else {
467            assert_eq!(&Version::new(0, 1, 0), migrations[0].from());
468            assert_eq!(&Version::new(0, 3, 0), migrations[0].to());
469            assert_eq!(&Version::new(0, 3, 0), migrations[1].from());
470            assert_eq!(&Version::new(0, 4, 0), migrations[1].to());
471        }
472    }
473
474    struct SimpleMigration0_1_0_0_2_0 {
475        from: Version,
476        to: Version,
477    }
478
479    impl MigrationOperation for SimpleMigration0_1_0_0_2_0 {
480        fn migrate_core(
481            &self,
482            chain_data_path: &Path,
483            _config: &Config,
484        ) -> anyhow::Result<PathBuf> {
485            let temp_db_path = self.temporary_db_path(chain_data_path);
486            fs::create_dir(&temp_db_path).unwrap();
487            Ok(temp_db_path)
488        }
489
490        fn new(from: Version, to: Version) -> Self
491        where
492            Self: Sized,
493        {
494            Self { from, to }
495        }
496
497        fn from(&self) -> &Version {
498            &self.from
499        }
500
501        fn to(&self) -> &Version {
502            &self.to
503        }
504    }
505
506    #[test]
507    fn test_migration_map_migration() {
508        let from = Version::new(0, 1, 0);
509        let to = Version::new(0, 2, 0);
510        let migration = Arc::new(SimpleMigration0_1_0_0_2_0::new(from, to));
511
512        let temp_dir = TempDir::new().unwrap();
513
514        assert!(migration.pre_checks(temp_dir.path()).is_err());
515        fs::create_dir(temp_dir.path().join("0.1.0")).unwrap();
516        assert!(migration.pre_checks(temp_dir.path()).is_ok());
517
518        migration
519            .migrate(temp_dir.path(), &Config::default())
520            .unwrap();
521        assert!(temp_dir.path().join("0.2.0").exists());
522
523        assert!(migration.post_checks(temp_dir.path()).is_err());
524        fs::create_dir(temp_dir.path().join("migration_0_1_0_0_2_0")).unwrap();
525        assert!(migration.post_checks(temp_dir.path()).is_ok());
526    }
527}