forest/db/migration/
migration_map.rs

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