geekorm_core/migrations/
mod.rs

1//! # Migrations
2//!
3//! This module contains the migration logic for the database.
4
5pub mod validate;
6
7use crate::backends::TableInfo;
8use crate::builder::models::QueryType;
9use crate::error::MigrationError;
10use crate::{Database, GeekConnection, Query, Table, Values};
11
12use self::validate::Validator;
13
14/// Migration state
15///
16/// Represents the state of the database schema
17#[derive(Debug)]
18pub enum MigrationState {
19    /// The database is initialized but no tables have been created
20    Initialized,
21    /// The database is up to date
22    UpToDate,
23    /// The database is out of date
24    OutOfDate(String),
25}
26
27pub(crate) type DatabaseTables = Vec<(String, Vec<TableInfo>)>;
28
29/// Migration trait
30pub trait Migration
31where
32    Self: Sync + Send,
33{
34    /// Get the version of the migration
35    fn version() -> &'static str
36    where
37        Self: Sized;
38    /// Get the create query
39    fn create_query() -> &'static str
40    where
41        Self: Sized;
42    /// Get the upgrade query
43    fn upgrade_query() -> &'static str
44    where
45        Self: Sized,
46    {
47        ""
48    }
49    /// Get the rollback query
50    fn rollback_query() -> &'static str
51    where
52        Self: Sized,
53    {
54        ""
55    }
56
57    /// Get the previous database if it exists
58    fn previous() -> Option<Box<dyn Migration>>
59    where
60        Self: Sized,
61    {
62        None
63    }
64
65    /// Get the database schema
66    fn database(&self) -> &Database;
67
68    /// This function is called to validate the database schema
69    /// by comparing the live database to the migration database
70    #[allow(async_fn_in_trait, unused_variables)]
71    async fn validate_database<'a, C>(
72        &self,
73        connection: &'a C,
74        database: &Database,
75    ) -> Result<MigrationState, crate::Error>
76    where
77        Self: Sized,
78        C: GeekConnection<Connection = C> + 'a,
79    {
80        // Get all the data from live database
81        let database_tables = C::table_names(connection).await?;
82
83        // If the database is empty, then it is initialized
84        if database_tables.is_empty() {
85            return Ok(MigrationState::Initialized);
86        }
87
88        let mut database_table_columns: DatabaseTables = Vec::new();
89        for table in database_tables {
90            let dbcolumns = C::pragma_info(connection, table.as_str()).await?;
91            database_table_columns.push((table, dbcolumns));
92        }
93
94        let mut migrations: Vec<Box<dyn Migration>> = Vec::new();
95        #[cfg(feature = "log")]
96        {
97            log::debug!("Validating database schema");
98        }
99
100        let state = Self::validate(&mut migrations, database, &database_table_columns)?;
101
102        for migration in migrations {
103            #[cfg(feature = "log")]
104            {
105                let v = Self::version();
106                log::info!("Upgrading database to version {}", v);
107            }
108            Self::upgrade(connection).await?;
109        }
110        if matches!(state, MigrationState::OutOfDate(_)) {
111            #[cfg(feature = "log")]
112            {
113                log::info!("Upgrading database to version {}", Self::version());
114            }
115            Self::upgrade(connection).await?;
116        }
117
118        Ok(MigrationState::UpToDate)
119    }
120
121    /// Validate the database schema is correct
122    #[allow(unused_variables)]
123    fn validate(
124        migrations: &mut Vec<Box<dyn Migration>>,
125        migration_database: &Database,
126        live_database: &DatabaseTables,
127    ) -> Result<MigrationState, crate::Error>
128    where
129        Self: Sized,
130    {
131        let mut validator = Validator {
132            errors: Vec::new(),
133            quick: true,
134        };
135        let result =
136            validate::validate_database(live_database, migration_database, &mut validator)?;
137
138        match result {
139            MigrationState::OutOfDate(reason) => {
140                #[cfg(feature = "log")]
141                {
142                    log::info!("Database is out of date: {}", reason);
143                }
144                if let Some(prev) = Self::previous() {
145                    migrations.push(prev);
146                }
147
148                Ok(MigrationState::OutOfDate(reason))
149            }
150            _ => Ok(MigrationState::UpToDate),
151        }
152    }
153
154    /// Create the database if it does not exist
155    ///
156    /// Assumes the database is already created but the tables are not
157    #[allow(async_fn_in_trait)]
158    async fn create<'a, C>(connection: &'a C) -> Result<(), crate::Error>
159    where
160        Self: Sized,
161        C: GeekConnection<Connection = C> + 'a,
162    {
163        let query = Self::create_query().to_string();
164
165        C::batch(
166            connection,
167            Query::new(
168                QueryType::Create,
169                query,
170                Values::new(),
171                Values::new(),
172                Vec::new(),
173                Table::default(),
174            ),
175        )
176        .await
177    }
178
179    /// Migrate the previos database to the current version
180    #[allow(async_fn_in_trait)]
181    async fn upgrade<'a, C>(connection: &'a C) -> Result<(), crate::Error>
182    where
183        Self: Sized,
184        C: GeekConnection<Connection = C> + 'a,
185    {
186        let query = Self::upgrade_query().to_string();
187        if query.is_empty() {
188            #[cfg(feature = "log")]
189            {
190                log::warn!("No upgrade query found");
191            }
192            return Err(crate::Error::MigrationError(MigrationError::UpgradeError(
193                "No upgrade is avalible".to_string(),
194            )));
195        }
196        #[cfg(feature = "log")]
197        {
198            log::debug!("Executing upgrade query: {}", query);
199        }
200        C::batch(
201            connection,
202            Query::new(
203                QueryType::Update,
204                query,
205                Values::new(),
206                Values::new(),
207                Vec::new(),
208                Table::default(),
209            ),
210        )
211        .await
212    }
213
214    /// Downgrade the database to the previous version
215    #[allow(async_fn_in_trait)]
216    async fn rollback<'a, C>(connection: &'a C) -> Result<(), crate::Error>
217    where
218        Self: Sized,
219        C: GeekConnection<Connection = C> + 'a,
220    {
221        let query = Self::rollback_query().to_string();
222        if query.is_empty() {
223            #[cfg(feature = "log")]
224            {
225                log::debug!("No rollback query found");
226            }
227            return Ok(());
228        }
229        #[cfg(feature = "log")]
230        {
231            log::debug!("Executing rollback query: {}", query);
232        }
233        C::execute(
234            connection,
235            Query::new(
236                QueryType::Update,
237                query,
238                Values::new(),
239                Values::new(),
240                Vec::new(),
241                Table::default(),
242            ),
243        )
244        .await
245    }
246
247    /// Migrating data from one version to another
248    #[allow(async_fn_in_trait, unused_variables)]
249    async fn migrate<'a, C>(connection: &'a C) -> Result<(), crate::Error>
250    where
251        Self: Sized,
252        C: GeekConnection<Connection = C> + 'a,
253    {
254        Ok(())
255    }
256}