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