geekorm_core/migrations/
validate.rs

1//! # Validate
2use crate::error::MigrationError;
3use crate::{Database, backends::TableInfo};
4
5use super::MigrationState;
6
7/// Migration validator
8#[derive(Debug)]
9pub struct Validator {
10    /// Errors
11    pub errors: Vec<MigrationError>,
12    /// Quick validation
13    pub quick: bool,
14}
15
16/// Validate the database schema
17///
18/// Errors are returned if an issue has occured, not if the database is out of date
19pub fn validate_database(
20    database_tables: &super::DatabaseTables,
21    migration_database: &Database,
22    validator: &mut Validator,
23) -> Result<MigrationState, crate::Error> {
24    let mut state = MigrationState::UpToDate;
25    #[cfg(feature = "log")]
26    {
27        log::debug!("Validating database schema");
28    }
29
30    // Simple checks first
31    if database_tables.is_empty() || migration_database.tables.is_empty() {
32        return Ok(MigrationState::Initialized);
33    }
34    // // Mismatched table count
35    if database_tables.len() != migration_database.tables.len() {
36        #[cfg(feature = "log")]
37        {
38            log::info!("Database Tables :: {:?}", database_tables);
39            log::info!(
40                "Migration Tables :: {:?}",
41                migration_database.get_table_names()
42            );
43        }
44        state = MigrationState::OutOfDate("Table count mismatch".to_string());
45        if validator.quick {
46            return Ok(state);
47        }
48    }
49
50    // Validate each table
51    for (name, table) in database_tables {
52        if let Some(mtable) = migration_database.get_table(name.as_str()) {
53            // Mismatched column count
54            if mtable.columns.len() != table.len() {
55                state = MigrationState::OutOfDate("Column count mismatch".to_string());
56            }
57
58            for dbcolumn in table {
59                #[cfg(feature = "log")]
60                {
61                    log::debug!("Columns :: {:?}", dbcolumn);
62                }
63                if let Some(mcolumn) = mtable.columns.get(dbcolumn.name.as_str()) {
64                    match validate_column(name, dbcolumn, mcolumn, &mut validator.errors) {
65                        MigrationState::UpToDate | MigrationState::Initialized => {}
66                        MigrationState::OutOfDate(reason) => {
67                            state = MigrationState::OutOfDate(reason);
68                            if validator.quick {
69                                return Ok(state);
70                            }
71                        }
72                    }
73                } else {
74                    validator.errors.push(MigrationError::MissingColumn {
75                        table: name.to_string(),
76                        column: dbcolumn.name.to_string(),
77                    });
78
79                    state = MigrationState::OutOfDate(format!(
80                        "Column not found: {}.{}",
81                        name, dbcolumn.name
82                    ));
83                    if validator.quick {
84                        return Ok(state);
85                    }
86                }
87            }
88
89            // HACK: This is a little hacky, but we need to validate all columns
90            for mcolumn in mtable.columns.columns.iter() {
91                #[cfg(feature = "log")]
92                {
93                    log::debug!("Migration Columns :: {:?}", mcolumn);
94                }
95                if let Some(dbcolumn) = table.iter().find(|c| c.name == mcolumn.name) {
96                    match validate_column(name, dbcolumn, mcolumn, &mut validator.errors) {
97                        MigrationState::UpToDate | MigrationState::Initialized => {}
98                        MigrationState::OutOfDate(reason) => {
99                            state = MigrationState::OutOfDate(reason);
100                            if validator.quick {
101                                return Ok(state);
102                            }
103                        }
104                    }
105                } else {
106                    validator.errors.push(MigrationError::MissingColumn {
107                        table: name.to_string(),
108                        column: mcolumn.name.to_string(),
109                    });
110                    state = MigrationState::OutOfDate(format!(
111                        "Column not found: {}.{}",
112                        name, mcolumn.name
113                    ));
114                    if validator.quick {
115                        return Ok(state);
116                    }
117                }
118            }
119        } else {
120            validator
121                .errors
122                .push(MigrationError::MissingTable(name.to_string()));
123            // If a table is not found, the database is out of date
124            state = MigrationState::OutOfDate(format!("Table not found: {}", name));
125            if validator.quick {
126                return Ok(state);
127            }
128        }
129    }
130    Ok(state)
131}
132
133/// Validate a column
134///
135/// `column` is the source of truth (the migration file)
136fn validate_column(
137    table: &String,
138    dbcolumn: &TableInfo,
139    column: &crate::Column,
140    errors: &mut Vec<MigrationError>,
141) -> MigrationState {
142    let mut state = MigrationState::UpToDate;
143
144    // Primary key check
145    if column.is_primary_key() && dbcolumn.pk != 1 {
146        errors.push(MigrationError::ColumnTypeMismatch {
147            table: table.to_string(),
148            column: column.name.clone(),
149            feature: "primary-key".to_string(),
150        });
151
152        state = MigrationState::OutOfDate("Primary key constraint not set".to_string());
153    }
154    // Not null check
155    if column.is_not_null() && dbcolumn.notnull == 0 {
156        errors.push(MigrationError::ColumnTypeMismatch {
157            table: table.to_string(),
158            column: column.name.clone(),
159            feature: "not-null".to_string(),
160        });
161    }
162
163    state
164}