forest/db/migration/
db_migration.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::path::PathBuf;
5
6use tracing::info;
7
8use crate::{
9    Config,
10    cli_shared::chain_path,
11    db::{
12        db_mode::{DbMode, get_latest_versioned_database},
13        migration::migration_map::create_migration_chain,
14    },
15    utils::version::FOREST_VERSION,
16};
17
18/// Governs the database migration process. This is the entry point for the migration process.
19pub struct DbMigration {
20    /// Forest configuration used.
21    config: Config,
22}
23
24impl DbMigration {
25    pub fn new(config: &Config) -> Self {
26        Self {
27            config: config.clone(),
28        }
29    }
30
31    pub fn chain_data_path(&self) -> PathBuf {
32        chain_path(&self.config)
33    }
34
35    /// Verifies if a migration is needed for the current Forest process.
36    /// Note that migration can possibly happen only if the DB is in `current` mode.
37    pub fn is_migration_required(&self) -> anyhow::Result<bool> {
38        // No chain data means that this is a fresh instance. No migration required.
39        if !self.chain_data_path().exists() {
40            return Ok(false);
41        }
42
43        // migration is required only if the DB is in `current` mode and the current db version is
44        // smaller than the current binary version
45        if let DbMode::Current = DbMode::read() {
46            let current_db = get_latest_versioned_database(&self.chain_data_path())?
47                .unwrap_or_else(|| FOREST_VERSION.clone());
48            Ok(current_db < *FOREST_VERSION)
49        } else {
50            Ok(false)
51        }
52    }
53
54    /// Performs a database migration if required. Note that this may take a long time to complete
55    /// and may need a lot of disk space (at least twice the size of the current database).
56    /// On a successful migration, the current database will be removed and the new database will
57    /// be used.
58    /// This method is tested via integration tests.
59    pub fn migrate(&self) -> anyhow::Result<()> {
60        if !self.is_migration_required()? {
61            info!("No database migration required");
62            return Ok(());
63        }
64
65        let latest_db_version = get_latest_versioned_database(&self.chain_data_path())?
66            .unwrap_or_else(|| FOREST_VERSION.clone());
67
68        let target_db_version = &FOREST_VERSION;
69
70        let migrations = create_migration_chain(&latest_db_version, target_db_version)?;
71
72        for migration in migrations {
73            info!(
74                "Migrating database from version {} to {}",
75                migration.from(),
76                migration.to()
77            );
78            let start = std::time::Instant::now();
79            migration.migrate(&self.chain_data_path(), &self.config)?;
80            info!(
81                "Successfully migrated from version {} to {}, took {}",
82                migration.from(),
83                migration.to(),
84                humantime::format_duration(std::time::Instant::now() - start),
85            );
86        }
87
88        Ok(())
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::db::db_mode::FOREST_DB_DEV_MODE;
96
97    #[test]
98    fn test_migration_not_required_no_chain_path() {
99        let temp_dir = tempfile::tempdir().unwrap();
100        let mut config = Config::default();
101        config.client.data_dir = temp_dir.path().join("azathoth");
102        let db_migration = DbMigration::new(&config);
103        assert!(!db_migration.is_migration_required().unwrap());
104    }
105
106    #[test]
107    fn test_migration_not_required_no_databases() {
108        let temp_dir = tempfile::tempdir().unwrap();
109        let mut config = Config::default();
110        temp_dir.path().clone_into(&mut config.client.data_dir);
111        let db_migration = DbMigration::new(&config);
112        assert!(!db_migration.is_migration_required().unwrap());
113    }
114
115    #[test]
116    fn test_migration_not_required_under_non_current_mode() {
117        let temp_dir = tempfile::tempdir().unwrap();
118        let mut config = Config::default();
119        temp_dir.path().clone_into(&mut config.client.data_dir);
120
121        let db_dir = temp_dir.path().join("mainnet/0.1.0");
122        std::fs::create_dir_all(&db_dir).unwrap();
123        let db_migration = DbMigration::new(&config);
124
125        unsafe { std::env::set_var(FOREST_DB_DEV_MODE, "latest") };
126        assert!(!db_migration.is_migration_required().unwrap());
127
128        std::fs::remove_dir(db_dir).unwrap();
129        std::fs::create_dir_all(temp_dir.path().join("mainnet/cthulhu")).unwrap();
130
131        unsafe { std::env::set_var(FOREST_DB_DEV_MODE, "cthulhu") };
132        assert!(!db_migration.is_migration_required().unwrap());
133    }
134
135    #[test]
136    fn test_migration_required_current_mode() {
137        let temp_dir = tempfile::tempdir().unwrap();
138        let mut config = Config::default();
139        temp_dir.path().clone_into(&mut config.client.data_dir);
140
141        let db_dir = temp_dir.path().join("mainnet/0.1.0");
142        std::fs::create_dir_all(db_dir).unwrap();
143        let db_migration = DbMigration::new(&config);
144
145        unsafe { std::env::set_var(FOREST_DB_DEV_MODE, "current") };
146        assert!(db_migration.is_migration_required().unwrap());
147        unsafe { std::env::remove_var(FOREST_DB_DEV_MODE) };
148        assert!(db_migration.is_migration_required().unwrap());
149    }
150}