dbmigrate_lib/
files.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::fs::File;
4use std::io::Read;
5use std::iter::repeat;
6use std::path::Path;
7
8use errors::{Result, ResultExt};
9use regex::Regex;
10
11/// A migration direction, can be Up or Down
12#[derive(Debug, PartialEq)]
13pub enum Direction {
14    /// Self-explanatory
15    Up,
16    /// Self-explanatory
17    Down,
18}
19
20impl ToString for Direction {
21    fn to_string(&self) -> String {
22        match *self {
23            Direction::Up => "up".to_owned(),
24            Direction::Down => "down".to_owned(),
25        }
26    }
27}
28
29/// A single direction migration file
30#[derive(Debug)]
31pub struct MigrationFile {
32    /// Content of the file
33    pub content: Option<String>,
34    /// Direction
35    pub direction: Direction,
36    /// Number
37    pub number: i32,
38    /// Filename
39    pub filename: String,
40    /// Actual migration name (filename with number removed)
41    pub name: String,
42}
43
44/// A migration has 2 files: one up and one down
45#[derive(Debug)]
46pub struct Migration {
47    /// The Up file
48    pub up: Option<MigrationFile>,
49    /// The Down file
50    pub down: Option<MigrationFile>,
51}
52
53/// Simple way to hold migrations indexed by their number
54pub type Migrations = BTreeMap<i32, Migration>;
55
56impl MigrationFile {
57    /// Used when getting the info, therefore setting content to None at that point
58    fn new(filename: &str, name: &str, number: i32, direction: Direction) -> MigrationFile {
59        MigrationFile {
60            content: None,
61            filename: filename.to_owned(),
62            number,
63            name: name.to_owned(),
64            direction,
65        }
66    }
67}
68
69/// Creates 2 migration file: one up and one down
70pub fn create_migration(path: &Path, slug: &str, number: i32) -> Result<()> {
71    let fixed_slug = slug.replace(" ", "_");
72    let filename_up = get_filename(&fixed_slug, number, Direction::Up);
73    parse_filename(&filename_up)?;
74    let filename_down = get_filename(&fixed_slug, number, Direction::Down);
75    parse_filename(&filename_down)?;
76
77    println!("Creating {}", filename_up);
78    File::create(path.join(filename_up.clone()))
79        .chain_err(|| format!("Failed to create {}", filename_up))?;
80    println!("Creating {}", filename_down);
81    File::create(path.join(filename_down.clone()))
82        .chain_err(|| format!("Failed to create {}", filename_down))?;
83
84    Ok(())
85}
86
87/// Get the filename to use for a migration using the given data
88fn get_filename(slug: &str, number: i32, direction: Direction) -> String {
89    let num = number.to_string();
90    let filler = repeat("0").take(4 - num.len()).collect::<String>();
91    filler + &num + "." + slug + "." + &direction.to_string() + ".sql"
92}
93
94/// Read the path given and read all the migration files, pairing them by migration
95/// number and checking for errors along the way
96pub fn read_migration_files(path: &Path) -> Result<Migrations> {
97    let mut btreemap: Migrations = BTreeMap::new();
98
99    for entry in fs::read_dir(path).chain_err(|| format!("Failed to open {:?}", path))? {
100        let entry = entry.unwrap();
101        // Will panic on invalid unicode in filename, unlikely (heh)
102        let info = match parse_filename(entry.file_name().to_str().unwrap()) {
103            Ok(info) => info,
104            Err(_) => continue,
105        };
106        let mut file =
107            File::open(entry.path()).chain_err(|| format!("Failed to open {:?}", entry.path()))?;
108        let mut content = String::new();
109        file.read_to_string(&mut content)?;
110
111        let migration_file = MigrationFile {
112            content: Some(content),
113            ..info
114        };
115        let migration_number = migration_file.number;
116        let mut migration = match btreemap.remove(&migration_number) {
117            None => Migration {
118                up: None,
119                down: None,
120            },
121            Some(m) => m,
122        };
123        match migration_file.direction {
124            Direction::Up if migration.up.is_none() => {
125                migration.up = Some(migration_file);
126            }
127            Direction::Down if migration.down.is_none() => {
128                migration.down = Some(migration_file);
129            }
130            _ => {
131                bail!(
132                    "There are multiple migrations with number {}",
133                    migration_number
134                )
135            }
136        };
137
138        btreemap.insert(migration_number, migration);
139    }
140
141    // Let's check the all the files we need now
142    let mut index = 1;
143    for (number, migration) in &btreemap {
144        if index != *number {
145            bail!("Files for migration {} are missing", index);
146        }
147        if migration.up.is_none() || migration.down.is_none() {
148            bail!("Migration {} is missing its up or down file", index);
149        }
150        index += 1;
151    }
152    Ok(btreemap)
153}
154
155/// Gets a filename and check whether it's a valid format.
156/// If it is, grabs all the info from it
157fn parse_filename(filename: &str) -> Result<MigrationFile> {
158    let re =
159        Regex::new(r"^(?P<number>[0-9]{4})\.(?P<name>[_0-9a-zA-Z]*)\.(?P<direction>up|down)\.sql$")
160            .unwrap();
161
162    let caps = match re.captures(filename) {
163        None => bail!("File {} has an invalid filename", filename),
164        Some(c) => c,
165    };
166
167    // Unwrapping below should be safe (in theory)
168    let number = caps
169        .name("number")
170        .unwrap()
171        .as_str()
172        .parse::<i32>()
173        .unwrap();
174    let name = caps.name("name").unwrap().as_str();
175    let direction = if caps.name("direction").unwrap().as_str() == "up" {
176        Direction::Up
177    } else {
178        Direction::Down
179    };
180
181    Ok(MigrationFile::new(filename, name, number, direction))
182}
183
184#[cfg(test)]
185mod tests {
186    use super::{get_filename, parse_filename, read_migration_files, Direction};
187    use std::fs::File;
188    use std::io::prelude::*;
189    use std::path::PathBuf;
190    use tempdir::TempDir;
191
192    fn create_file(path: &PathBuf, filename: &str) {
193        let mut new_path = path.clone();
194        new_path.push(filename);
195        let mut f = File::create(new_path.to_str().unwrap()).unwrap();
196        f.write_all(b"Hello, world!").unwrap();
197    }
198
199    #[test]
200    fn test_parse_good_filename() {
201        let result = parse_filename("0001.tests.up.sql").unwrap();
202        assert_eq!(result.number, 1);
203        assert_eq!(result.name, "tests");
204        assert_eq!(result.direction, Direction::Up);
205    }
206
207    #[test]
208    fn test_parse_bad_filename_format() {
209        // Has _ instead of . between number and name
210        let result = parse_filename("0001_tests.up.sql");
211        assert_eq!(result.is_ok(), false);
212    }
213
214    #[test]
215    fn test_get_filename_ok() {
216        let result = get_filename("initial", 1, Direction::Up);
217        assert_eq!(result, "0001.initial.up.sql");
218    }
219
220    #[test]
221    fn test_parse_good_migrations_directory() {
222        let pathbuf = TempDir::new("migrations").unwrap().into_path();
223        create_file(&pathbuf, "0001.tests.up.sql");
224        create_file(&pathbuf, "0001.tests.down.sql");
225        create_file(&pathbuf, "0002.tests_second.up.sql");
226        create_file(&pathbuf, "0002.tests_second.down.sql");
227        let migrations = read_migration_files(pathbuf.as_path());
228
229        assert_eq!(migrations.is_ok(), true);
230    }
231
232    #[test]
233    fn test_parse_missing_migrations_directory() {
234        let pathbuf = TempDir::new("migrations").unwrap().into_path();
235        create_file(&pathbuf, "0001.tests.up.sql");
236        create_file(&pathbuf, "0001.tests.down.sql");
237        create_file(&pathbuf, "0002.tests_second.up.sql");
238        let migrations = read_migration_files(pathbuf.as_path());
239
240        assert_eq!(migrations.is_err(), true);
241    }
242
243    #[test]
244    fn test_parse_skipping_migrations_directory() {
245        let pathbuf = TempDir::new("migrations").unwrap().into_path();
246        create_file(&pathbuf, "0001.tests.up.sql");
247        create_file(&pathbuf, "0001.tests.down.sql");
248        create_file(&pathbuf, "0003.tests_second.up.sql");
249        create_file(&pathbuf, "0003.tests_second.down.sql");
250        let migrations = read_migration_files(pathbuf.as_path());
251
252        assert_eq!(migrations.is_err(), true);
253    }
254
255    #[test]
256    fn test_two_migrations_same_number() {
257        let tests: &[&[&str]] = &[
258            // Extra up migration
259            &["0001.a.up.sql", "0001.a.down.sql", "0001.b.up.sql"],
260            // Extra down migration
261            &["0001.a.up.sql", "0001.a.down.sql", "0001.b.down.sql"],
262            // Extra up and down migrations
263            &[
264                "0001.a.up.sql",
265                "0001.a.down.sql",
266                "0001.b.up.sql",
267                "0001.b.down.sql",
268            ],
269        ];
270
271        for files in tests {
272            let pathbuf = TempDir::new("migrations").unwrap().into_path();
273
274            for file in files.iter() {
275                create_file(&pathbuf, file);
276            }
277
278            let migrations = read_migration_files(pathbuf.as_path());
279
280            assert_eq!(migrations.is_err(), true);
281        }
282    }
283}