Skip to main content

rorm_cli/utils/
migrations.rs

1use std::fs::{read_dir, read_to_string, DirEntry, File};
2use std::io::Write;
3use std::path::Path;
4
5use anyhow::{anyhow, Context};
6use rorm_declaration::imr::InternalModelFormat;
7use rorm_declaration::migration::{Migration, MigrationFile};
8
9use crate::utils::imr_as_state::InternalModelFormatExt;
10use crate::utils::re::RE;
11
12/**
13This function is used to convert the [InternalModelFormat] into its TOML representation.
14
15`migration` [Migration]: Migration to be converted into TOML
16`path` [&str]: The path to write the resulting TOML to
17 */
18pub fn convert_migration_to_file(migration: Migration, path: &Path) -> anyhow::Result<()> {
19    let toml_str = toml::to_string_pretty(&MigrationFile { migration })
20        .with_context(|| "Error while serializing migration")?;
21
22    let mut output = File::create(path).with_context(|| {
23        format!(
24            "Error while opening file {:?} to write migration into",
25            path.file_name()
26        )
27    })?;
28    write!(output, "{toml_str}").with_context(|| "Error while writing to migration file")?;
29
30    Ok(())
31}
32
33/**
34This function tries to convert a file to a [Migration].
35
36`path` [&DirEntry]: Path to the file that should be parsed.
37*/
38pub fn convert_file_to_migration(path: &DirEntry) -> anyhow::Result<MigrationFile> {
39    let toml_str = read_to_string(path.path()).with_context(|| {
40        format!(
41            "Error occurred while reading {}",
42            path.path().to_str().unwrap()
43        )
44    })?;
45
46    let mut migration: MigrationFile = toml::from_str(toml_str.as_str()).with_context(|| {
47        format!(
48            "Error while deserializing migration {:?} from TOML",
49            path.file_name()
50        )
51    })?;
52
53    migration.migration.id = path.path().file_stem().unwrap().to_str().unwrap()[..4].parse()?;
54    migration.migration.name = path.path().file_stem().unwrap().to_str().unwrap()[5..].to_string();
55
56    Ok(migration)
57}
58
59pub(crate) fn get_migration_files(migration_dir: &str) -> anyhow::Result<Vec<DirEntry>> {
60    let dir_entries =
61        read_dir(migration_dir).with_context(|| "Error while searching the migration directory")?;
62
63    let file_list: Vec<DirEntry> = dir_entries
64        .filter(|x| {
65            x.as_ref().unwrap().file_type().unwrap().is_file()
66                && RE.migration_allowed_name.is_match(
67                    x.as_ref()
68                        .unwrap()
69                        .file_name()
70                        .into_string()
71                        .unwrap()
72                        .as_str(),
73                )
74        })
75        .map(|x| x.unwrap())
76        .collect();
77
78    Ok(file_list)
79}
80
81/**
82Helper function to retrieve a sorted list of migrations in a given directory.
83
84This strips also migrations, that were replaced.
85
86**Parameter**:
87- `migration_dir`: [&str] The directory to search for files.
88  this point onwards.
89*/
90pub fn get_existing_migrations(migration_dir: &str) -> anyhow::Result<Vec<Migration>> {
91    let migrations = get_all_existing_migrations(migration_dir)?;
92
93    let mut migration_list: Vec<Migration> = vec![];
94
95    // Filter out migrations that replace migrations
96    for m in migrations {
97        if m.replaces.is_empty() {
98            migration_list.push(m);
99        }
100    }
101
102    let mut sorted_migration_list: Vec<Migration> = vec![];
103
104    let mut current_id = None;
105    loop {
106        match current_id {
107            None => {
108                if let Some(&initial) = migration_list
109                    .iter()
110                    .filter(|x| x.initial)
111                    .collect::<Vec<&Migration>>()
112                    .first()
113                {
114                    current_id = Some(initial.id);
115                    sorted_migration_list.push(initial.clone());
116                    continue;
117                }
118            }
119            Some(curr) => {
120                if let Some(&next) = migration_list
121                    .iter()
122                    .filter(|x| {
123                        if let Some(dependency) = x.dependency {
124                            dependency == curr
125                        } else {
126                            false
127                        }
128                    })
129                    .collect::<Vec<&Migration>>()
130                    .first()
131                {
132                    current_id = Some(next.id);
133                    sorted_migration_list.push(next.clone());
134                    continue;
135                }
136            }
137        }
138        break;
139    }
140
141    if sorted_migration_list.len() != migration_list.len() {
142        return Err(anyhow!("Migrations does not assemble to a coherent list."));
143    }
144
145    Ok(sorted_migration_list)
146}
147
148/**
149Helper function to retrieve an unsorted list of **all** migrations in a given directory.
150
151`migration_dir`: [&str] The directory to search for files.
152 */
153pub fn get_all_existing_migrations(migration_dir: &str) -> anyhow::Result<Vec<Migration>> {
154    let file_list = get_migration_files(migration_dir)?;
155    let mut migration_list: Vec<Migration> = vec![];
156    for file in &file_list {
157        migration_list.push(convert_file_to_migration(file)?.migration);
158    }
159
160    Ok(migration_list)
161}
162
163/**
164Helper function to converts a list of migrations to an internal model.
165
166`migrations`: [Vec<Migration>]: List of migrations
167 */
168pub fn convert_migrations_to_internal_models(
169    migrations: &[Migration],
170) -> anyhow::Result<InternalModelFormat> {
171    let mut state = InternalModelFormat { models: Vec::new() };
172
173    for migration in migrations {
174        for operation in &migration.operations {
175            state.apply_operation(operation)?;
176        }
177    }
178
179    Ok(state)
180}
181
182#[cfg(test)]
183mod test {
184    use std::path::Path;
185
186    use rorm_declaration::migration::Migration;
187    use temp_dir::TempDir;
188
189    use crate::utils::migrations::{convert_migration_to_file, get_existing_migrations};
190
191    #[test]
192    fn test_get_existing_migrations_non_initial() {
193        let tmp = TempDir::new().expect("Could not create a temporary directory");
194        let p = tmp.path().join("0001_not_initial.toml");
195
196        let migration = Migration {
197            hash: "".to_string(),
198            initial: false,
199            id: 0,
200            name: "".to_string(),
201            dependency: None,
202            replaces: vec![],
203            operations: vec![],
204        };
205
206        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
207            .expect("Could not write to file");
208
209        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_err());
210    }
211
212    #[test]
213    fn test_get_existing_migrations_initial() {
214        let tmp = TempDir::new().expect("Could not create a temporary directory");
215        let p = tmp.path().join("0001_initial.toml");
216
217        let migration = Migration {
218            hash: "".to_string(),
219            initial: true,
220            id: 0,
221            name: "".to_string(),
222            dependency: None,
223            replaces: vec![],
224            operations: vec![],
225        };
226
227        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
228            .expect("Could not write to file");
229
230        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_ok());
231    }
232
233    #[test]
234    fn test_get_existing_migrations_multiple_connected() {
235        let tmp = TempDir::new().expect("Could not create a temporary directory");
236        let p = tmp.path().join("0001_initial.toml");
237        let p_2 = tmp.path().join("00002_foobar.toml");
238
239        let migration = Migration {
240            hash: "".to_string(),
241            initial: true,
242            id: 0,
243            name: "".to_string(),
244            dependency: None,
245            replaces: vec![],
246            operations: vec![],
247        };
248
249        convert_migration_to_file(migration, Path::new(p.to_str().unwrap()))
250            .expect("Could not write to file");
251
252        let migration = Migration {
253            hash: "".to_string(),
254            initial: false,
255            id: 0,
256            name: "".to_string(),
257            dependency: Some(1),
258            replaces: vec![],
259            operations: vec![],
260        };
261
262        convert_migration_to_file(migration, Path::new(p_2.to_str().unwrap()))
263            .expect("Could not write to file");
264
265        assert!(get_existing_migrations(tmp.path().to_str().unwrap()).is_ok());
266    }
267}