fly/
planner.rs

1use crate::db::Db;
2use crate::error::Result;
3use crate::file;
4use crate::migration::{Migration, MigrationWithMeta};
5use std::collections::HashMap;
6use std::fmt::Display;
7use std::path::Path;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ApplicationState {
11    Pending {
12        definition: Migration,
13    },
14    Applied {
15        definition: Migration,
16        application: MigrationWithMeta,
17    },
18    Changed {
19        definition: Migration,
20        application: MigrationWithMeta,
21    },
22    Removed {
23        application: MigrationWithMeta,
24    },
25}
26
27impl ApplicationState {
28    pub fn is_pending(&self) -> bool {
29        matches!(self, ApplicationState::Pending { .. })
30    }
31
32    pub fn is_applied(&self) -> bool {
33        matches!(self, ApplicationState::Applied { .. })
34    }
35
36    pub fn is_changed(&self) -> bool {
37        matches!(self, ApplicationState::Changed { .. })
38    }
39
40    pub fn is_removed(&self) -> bool {
41        matches!(self, ApplicationState::Removed { .. })
42    }
43
44    pub fn name(&self) -> &str {
45        match self {
46            ApplicationState::Pending { definition } => &definition.name,
47            ApplicationState::Applied {
48                definition,
49                application: _,
50            } => &definition.name,
51            ApplicationState::Changed {
52                definition,
53                application: _,
54            } => &definition.name,
55            ApplicationState::Removed { application } => &application.migration.name,
56        }
57    }
58}
59
60impl Display for ApplicationState {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            ApplicationState::Pending { definition: file } => write!(f, "{} [pending]", file.name),
64            ApplicationState::Applied {
65                definition: file,
66                application: _,
67            } => {
68                write!(f, "{} [applied]", file.name)
69            }
70            ApplicationState::Changed {
71                definition: file,
72                application: _,
73            } => write!(f, "{} ** CHANGED **", file.name),
74            ApplicationState::Removed { application: db } => {
75                write!(f, "{} ** NO FILE **", db.migration.name)
76            }
77        }
78    }
79}
80
81pub fn get_all_migration_state(
82    db: &mut Db,
83    migrate_dir: impl AsRef<Path>,
84) -> Result<Vec<ApplicationState>> {
85    let definitions = file::list(migrate_dir.as_ref())?;
86    let applications = db.list()?;
87    Ok(get_all_migration_state_impl(definitions, applications))
88}
89
90fn get_all_migration_state_impl(
91    definitions: Vec<Migration>,
92    applications: Vec<MigrationWithMeta>,
93) -> Vec<ApplicationState> {
94    let definitions = definitions
95        .into_iter()
96        .map(|m| (m.name.clone(), m))
97        .collect::<HashMap<String, Migration>>();
98    let applications = applications
99        .into_iter()
100        .map(|m| (m.migration.name.clone(), m))
101        .collect::<HashMap<String, MigrationWithMeta>>();
102
103    let mut all_names = definitions
104        .values()
105        .map(|v| v.name.clone())
106        .chain(applications.values().map(|v| v.migration.name.clone()))
107        .collect::<Vec<String>>();
108    all_names.sort();
109    all_names.dedup();
110
111    all_names
112        .iter()
113        .map(|name| {
114            let definition = definitions.get(name);
115            let application = applications.get(name);
116
117            match (definition, application) {
118                (None, Some(application)) => ApplicationState::Removed {
119                    application: application.clone(),
120                },
121                (Some(definition), None) => ApplicationState::Pending {
122                    definition: definition.clone(),
123                },
124                (Some(definition), Some(application)) => {
125                    if application.migration == *definition {
126                        ApplicationState::Applied {
127                            definition: definition.clone(),
128                            application: application.clone(),
129                        }
130                    } else {
131                        ApplicationState::Changed {
132                            definition: definition.clone(),
133                            application: application.clone(),
134                        }
135                    }
136                }
137                (None, None) => unreachable!(),
138            }
139        })
140        .collect::<Vec<_>>()
141}
142
143#[cfg(test)]
144mod test {
145    use crate::migration::MigrationMeta;
146    use rand::seq::SliceRandom;
147    use std::time::SystemTime;
148
149    use super::*;
150
151    #[test]
152    fn test_get_all_migration_state_empty() {
153        let result = get_all_migration_state_impl(Vec::new(), Vec::new());
154        assert_eq!(result, Vec::new());
155    }
156
157    #[test]
158    fn test_get_all_migration_state_pending() {
159        let definition = build_migration("migration", "up", "down");
160        let result = get_all_migration_state_impl(vec![definition.clone()], Vec::new());
161        assert_eq!(
162            result,
163            vec![ApplicationState::Pending {
164                definition: definition
165            }]
166        );
167    }
168
169    #[test]
170    fn test_get_all_migration_state_removed() {
171        let application = build_migration_meta("migration", "up", "down");
172        let result = get_all_migration_state_impl(Vec::new(), vec![application.clone()]);
173        assert_eq!(
174            result,
175            vec![ApplicationState::Removed {
176                application: application
177            }]
178        );
179    }
180
181    #[test]
182    fn test_get_all_migration_state_applied() {
183        let definition = build_migration("migration", "up", "down");
184        let application = build_migration_meta("migration", "up", "down");
185        let result =
186            get_all_migration_state_impl(vec![definition.clone()], vec![application.clone()]);
187        assert_eq!(
188            result,
189            vec![ApplicationState::Applied {
190                definition: definition,
191                application: application
192            }]
193        );
194    }
195
196    #[test]
197    fn test_get_all_migration_up_state_changed() {
198        let definition = build_migration("migration", "up-changed", "down");
199        let application = build_migration_meta("migration", "up", "down");
200        let result =
201            get_all_migration_state_impl(vec![definition.clone()], vec![application.clone()]);
202        assert_eq!(
203            result,
204            vec![ApplicationState::Changed {
205                definition: definition,
206                application: application
207            }]
208        );
209    }
210
211    #[test]
212    fn test_get_all_migration_down_state_changed() {
213        let definition = build_migration("migration", "up", "down-changed");
214        let application = build_migration_meta("migration", "up", "down");
215        let result =
216            get_all_migration_state_impl(vec![definition.clone()], vec![application.clone()]);
217        assert_eq!(
218            result,
219            vec![ApplicationState::Changed {
220                definition: definition,
221                application: application
222            }]
223        );
224    }
225
226    #[test]
227    fn test_get_all_migration_state_multiple() {
228        let definition_a = build_migration("1-migration", "1-up", "1-down");
229        let definition_b = build_migration("2-migration", "2-up", "2-down");
230        let definition_c = build_migration("3-migration", "3-up", "3-down");
231
232        let application_a = build_migration_meta("1-migration", "1-up", "1-down");
233        let application_b = build_migration_meta("2-migration", "2-up-changed", "2-down");
234        let application_c = build_migration_meta("4-migration", "4-up", "4-down");
235
236        let mut definitions = vec![
237            definition_a.clone(),
238            definition_b.clone(),
239            definition_c.clone(),
240        ];
241        let mut applications = vec![
242            application_a.clone(),
243            application_b.clone(),
244            application_c.clone(),
245        ];
246
247        // The particular order of definitions/applications should not
248        // matter, as they are keyed and sorted by name.
249        let mut rng = rand::thread_rng();
250        definitions.shuffle(&mut rng);
251        applications.shuffle(&mut rng);
252
253        let result = get_all_migration_state_impl(definitions, applications);
254        assert_eq!(
255            result,
256            vec![
257                ApplicationState::Applied {
258                    definition: definition_a,
259                    application: application_a
260                },
261                ApplicationState::Changed {
262                    definition: definition_b,
263                    application: application_b
264                },
265                ApplicationState::Pending {
266                    definition: definition_c
267                },
268                ApplicationState::Removed {
269                    application: application_c
270                },
271            ]
272        );
273    }
274
275    fn build_migration(name: &'static str, up: &'static str, down: &'static str) -> Migration {
276        Migration {
277            up_sql: up.to_string(),
278            down_sql: down.to_string(),
279            name: name.to_string(),
280        }
281    }
282
283    fn build_migration_meta(
284        name: &'static str,
285        up: &'static str,
286        down: &'static str,
287    ) -> MigrationWithMeta {
288        let now = SystemTime::now();
289
290        MigrationWithMeta {
291            meta: MigrationMeta {
292                id: 123,
293                created_at: now,
294            },
295            migration: build_migration(name, up, down),
296        }
297    }
298}