fly/
file.rs

1use crate::{error::Error, error::Result, migration::Migration};
2use std::io::Read;
3use std::path::Path;
4
5pub fn list(migrate_dir: impl AsRef<Path>) -> Result<Vec<Migration>> {
6    let paths = std::fs::read_dir(migrate_dir.as_ref())?;
7
8    let mut migrations = Vec::new();
9    for path in paths {
10        let path = path?.path();
11        if valid_migration_file_path(&path) {
12            migrations.push(parse_migration_from_file(path)?);
13        }
14    }
15    Ok(migrations)
16}
17
18// Check that a path is a file, ends in .sql, and does not start with a dot.
19fn valid_migration_file_path(path: impl AsRef<Path>) -> bool {
20    let path = path.as_ref();
21    path.is_file()
22        && path
23            .extension()
24            .map_or(false, |f| f.to_string_lossy() == "sql")
25        && path
26            .file_name()
27            .map_or(false, |f| !f.to_string_lossy().starts_with("."))
28}
29
30fn parse_migration_from_file(path: impl AsRef<Path>) -> Result<Migration> {
31    let name = path
32        .as_ref()
33        .file_name()
34        .ok_or(Error::FilenameRequired)?
35        .to_str()
36        .ok_or(Error::FilenameBadEncoding)?
37        .to_string();
38    let file = std::fs::File::open(&path)?;
39    parse_migration(name, file)
40}
41
42fn parse_migration(name: String, mut reader: impl Read) -> Result<Migration> {
43    let mut contents = String::new();
44    reader.read_to_string(&mut contents)?;
45    let mut statements = contents.split('\n');
46    let mut up = String::new();
47    let mut down = String::new();
48    let mut has_up = false;
49    let mut has_down = false;
50    for line in &mut statements {
51        if line == "-- up" {
52            if has_down {
53                return Err(Error::MigrationFileFormatError {
54                    reason: "up migration must come first".to_string(),
55                    name,
56                });
57            } else {
58                has_up = true;
59            }
60            break;
61        }
62    }
63    for line in &mut statements {
64        if line == "-- up" {
65            return Err(Error::MigrationFileFormatError {
66                reason: "only one up migration allowed".to_string(),
67                name,
68            });
69        }
70        if line == "-- down" {
71            has_down = true;
72            break;
73        }
74        up.push_str(line);
75        up.push('\n');
76    }
77    for line in &mut statements {
78        if line == "-- down" {
79            return Err(Error::MigrationFileFormatError {
80                reason: "only one down migration allowed".to_string(),
81                name,
82            });
83        }
84        down.push_str(line);
85        down.push('\n');
86    }
87
88    if !(has_down && has_up) {
89        return Err(Error::MigrationFileFormatError {
90            reason: "both up and down migrations must be defined".to_string(),
91            name,
92        });
93    }
94    Ok(Migration {
95        up_sql: up.trim().to_string(),
96        down_sql: down.trim().to_string(),
97        name,
98    })
99}
100
101#[cfg(test)]
102mod test {
103    use std::io::Cursor;
104
105    use super::*;
106
107    #[test]
108    fn test_parse_migration_empty_string() -> Result<()> {
109        let migration_str = "".to_string();
110        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
111
112        assert!(result.is_err());
113        assert_eq!(
114            result.err().unwrap().to_string(),
115            "bad migration file format in foo: both up and down migrations must be defined"
116        );
117
118        Ok(())
119    }
120
121    #[test]
122    fn test_parse_migration_just_up() -> Result<()> {
123        let migration_str = "
124-- up
125create table users (id int);
126"
127        .to_string();
128        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
129
130        assert!(result.is_err());
131        assert_eq!(
132            result.err().unwrap().to_string(),
133            "bad migration file format in foo: both up and down migrations must be defined"
134        );
135
136        Ok(())
137    }
138
139    #[test]
140    fn test_parse_migration_just_down() -> Result<()> {
141        let migration_str = "
142-- down
143drop table users;
144"
145        .to_string();
146        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
147
148        assert!(result.is_err());
149        assert_eq!(
150            result.err().unwrap().to_string(),
151            "bad migration file format in foo: both up and down migrations must be defined"
152        );
153
154        Ok(())
155    }
156
157    #[test]
158    fn test_parse_migration_multiple_ups() -> Result<()> {
159        let migration_str = "
160-- up
161create table users (id int);
162
163-- up
164alter table users add column is_active boolean default true;
165
166-- down
167drop table users;
168"
169        .to_string();
170        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
171
172        assert!(result.is_err());
173        assert_eq!(
174            result.err().unwrap().to_string(),
175            "bad migration file format in foo: only one up migration allowed"
176        );
177
178        Ok(())
179    }
180
181    #[test]
182    fn test_parse_migration_multiple_downs() -> Result<()> {
183        let migration_str = "
184-- up
185create table users (id int);
186
187-- down
188alter table users remove column id;
189
190-- down
191drop table users;
192"
193        .to_string();
194        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
195
196        assert!(result.is_err());
197        assert_eq!(
198            result.err().unwrap().to_string(),
199            "bad migration file format in foo: only one down migration allowed"
200        );
201
202        Ok(())
203    }
204
205    #[test]
206    fn test_parse_normal_migration() -> Result<()> {
207        let migration_str = "
208-- up
209create table users (
210  id int
211);
212
213-- down
214drop table users;
215"
216        .to_string();
217        let result = parse_migration("foo".to_string(), Cursor::new(migration_str));
218
219        assert!(result.is_ok());
220        assert_eq!(
221            result.ok().unwrap(),
222            Migration {
223                name: "foo".to_string(),
224                up_sql: "create table users (
225  id int
226);"
227                .to_string(),
228                down_sql: "drop table users;".to_string(),
229            }
230        );
231
232        Ok(())
233    }
234}