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
18fn 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}