1use crate::error::{Error, Kind};
2use crate::runner::Type;
3use crate::Migration;
4use regex::Regex;
5use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8use walkdir::{DirEntry, WalkDir};
9
10#[cfg(not(feature = "int8-versions"))]
11pub type SchemaVersion = i32;
12#[cfg(feature = "int8-versions")]
13pub type SchemaVersion = i64;
14
15const STEM_RE: &str = r"^([U|V])(\d+(?:\.\d+)?)__(\w+)";
16
17fn file_stem_re() -> &'static Regex {
19 static RE: OnceLock<Regex> = OnceLock::new();
20 RE.get_or_init(|| Regex::new(STEM_RE).unwrap())
21}
22
23fn file_re_sql() -> &'static Regex {
25 static RE: OnceLock<Regex> = OnceLock::new();
26 RE.get_or_init(|| Regex::new([STEM_RE, r"\.sql$"].concat().as_str()).unwrap())
27}
28
29fn file_re_all() -> &'static Regex {
31 static RE: OnceLock<Regex> = OnceLock::new();
32 RE.get_or_init(|| Regex::new([STEM_RE, r"\.(rs|sql)$"].concat().as_str()).unwrap())
33}
34
35pub enum MigrationType {
38 All,
39 Sql,
40}
41
42impl MigrationType {
43 fn file_match_re(&self) -> &'static Regex {
44 match self {
45 MigrationType::All => file_re_all(),
46 MigrationType::Sql => file_re_sql(),
47 }
48 }
49}
50
51pub fn parse_migration_name(name: &str) -> Result<(Type, SchemaVersion, String), Error> {
53 let captures = file_stem_re()
54 .captures(name)
55 .filter(|caps| caps.len() == 4)
56 .ok_or_else(|| Error::new(Kind::InvalidName, None))?;
57 let version: SchemaVersion = captures[2]
58 .parse()
59 .map_err(|_| Error::new(Kind::InvalidVersion, None))?;
60
61 let name: String = (&captures[3]).into();
62 let prefix = match &captures[1] {
63 "V" => Type::Versioned,
64 "U" => Type::Unversioned,
65 _ => unreachable!(),
66 };
67
68 Ok((prefix, version, name))
69}
70
71pub fn find_migration_files(
79 location: impl AsRef<Path>,
80 migration_type: MigrationType,
81) -> Result<impl Iterator<Item = PathBuf>, Error> {
82 let location: &Path = location.as_ref();
83 let location = location.canonicalize().map_err(|err| {
84 Error::new(
85 Kind::InvalidMigrationPath(location.to_path_buf(), err),
86 None,
87 )
88 })?;
89
90 let re = migration_type.file_match_re();
91 let file_paths = WalkDir::new(location)
92 .sort_by_file_name()
93 .into_iter()
94 .filter_map(Result::ok)
95 .map(DirEntry::into_path)
96 .filter(move |entry| {
98 if entry.is_dir() {
99 return false;
100 }
101
102 match entry.file_name().and_then(OsStr::to_str) {
103 Some(file_name) if re.is_match(file_name) => true,
104 Some(file_name) => {
105 log::warn!(
106 "File \"{}\" does not adhere to the migration naming convention. Migrations must be named in the format [U|V]{{1}}__{{2}}.sql or [U|V]{{1}}__{{2}}.rs, where {{1}} represents the migration version and {{2}} the name.",
107 file_name
108 );
109 false
110 }
111 None => false,
112 }
113 });
114
115 Ok(file_paths)
116}
117
118pub fn load_sql_migrations(location: impl AsRef<Path>) -> Result<Vec<Migration>, Error> {
121 let migration_files = find_migration_files(location, MigrationType::Sql)?;
122
123 let mut migrations = vec![];
124
125 for path in migration_files {
126 let sql = std::fs::read_to_string(path.as_path()).map_err(|e| {
127 let path = path.to_owned();
128 let kind = match e.kind() {
129 std::io::ErrorKind::NotFound => Kind::InvalidMigrationPath(path, e),
130 _ => Kind::InvalidMigrationFile(path, e),
131 };
132
133 Error::new(kind, None)
134 })?;
135
136 let filename = path
138 .file_stem()
139 .and_then(|file| file.to_os_string().into_string().ok())
140 .unwrap();
141
142 let migration = Migration::unapplied(&filename, &sql)?;
143 migrations.push(migration);
144 }
145
146 migrations.sort();
147 Ok(migrations)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::{find_migration_files, load_sql_migrations, MigrationType};
153 use std::fs;
154 use std::path::PathBuf;
155 use tempfile::TempDir;
156
157 #[test]
158 fn finds_mod_migrations() {
159 let tmp_dir = TempDir::new().unwrap();
160 let migrations_dir = tmp_dir.path().join("migrations");
161 fs::create_dir(&migrations_dir).unwrap();
162 let sql1 = migrations_dir.join("V1__first.rs");
163 fs::File::create(&sql1).unwrap();
164 let sql2 = migrations_dir.join("V2__second.rs");
165 fs::File::create(&sql2).unwrap();
166
167 let mods: Vec<PathBuf> = find_migration_files(migrations_dir, MigrationType::All)
168 .unwrap()
169 .collect();
170 assert_eq!(sql1.canonicalize().unwrap(), mods[0]);
171 assert_eq!(sql2.canonicalize().unwrap(), mods[1]);
172 }
173
174 #[test]
175 fn ignores_mod_files_without_migration_regex_match() {
176 let tmp_dir = TempDir::new().unwrap();
177 let migrations_dir = tmp_dir.path().join("migrations");
178 fs::create_dir(&migrations_dir).unwrap();
179 let sql1 = migrations_dir.join("V1first.rs");
180 fs::File::create(sql1).unwrap();
181 let sql2 = migrations_dir.join("V2second.rs");
182 fs::File::create(sql2).unwrap();
183
184 let mut mods = find_migration_files(migrations_dir, MigrationType::All).unwrap();
185 assert!(mods.next().is_none());
186 }
187
188 #[test]
189 fn finds_sql_migrations() {
190 let tmp_dir = TempDir::new().unwrap();
191 let migrations_dir = tmp_dir.path().join("migrations");
192 fs::create_dir(&migrations_dir).unwrap();
193 let sql1 = migrations_dir.join("V1__first.sql");
194 fs::File::create(&sql1).unwrap();
195 let sql2 = migrations_dir.join("V2__second.sql");
196 fs::File::create(&sql2).unwrap();
197
198 let mods: Vec<PathBuf> = find_migration_files(migrations_dir, MigrationType::All)
199 .unwrap()
200 .collect();
201 assert_eq!(sql1.canonicalize().unwrap(), mods[0]);
202 assert_eq!(sql2.canonicalize().unwrap(), mods[1]);
203 }
204
205 #[test]
206 fn finds_sql_migrations_in_nested_directories() {
207 let tmp_dir = TempDir::new().unwrap();
208 let migrations_dir = tmp_dir.path().join("migrations");
209 let nested_dir = migrations_dir.join("foo");
210 fs::create_dir(&migrations_dir).unwrap();
211 fs::create_dir(&nested_dir).unwrap();
212
213 let sql1 = nested_dir.join("V1__first.sql");
214 let sql2 = nested_dir.join("V2__second.sql");
215 fs::File::create(&sql1).unwrap();
216 fs::File::create(&sql2).unwrap();
217
218 let mods: Vec<PathBuf> = find_migration_files(migrations_dir, MigrationType::All)
219 .unwrap()
220 .collect();
221
222 assert_eq!(sql1.canonicalize().unwrap(), mods[0]);
223 assert_eq!(sql2.canonicalize().unwrap(), mods[1]);
224 }
225
226 #[test]
227 fn finds_unversioned_migrations() {
228 let tmp_dir = TempDir::new().unwrap();
229 let migrations_dir = tmp_dir.path().join("migrations");
230 fs::create_dir(&migrations_dir).unwrap();
231 let sql1 = migrations_dir.join("U1__first.sql");
232 fs::File::create(&sql1).unwrap();
233 let sql2 = migrations_dir.join("U2__second.sql");
234 fs::File::create(&sql2).unwrap();
235
236 let mods: Vec<PathBuf> = find_migration_files(migrations_dir, MigrationType::All)
237 .unwrap()
238 .collect();
239 assert_eq!(sql1.canonicalize().unwrap(), mods[0]);
240 assert_eq!(sql2.canonicalize().unwrap(), mods[1]);
241 }
242
243 #[test]
244 fn ignores_sql_files_without_migration_regex_match() {
245 let tmp_dir = TempDir::new().unwrap();
246 let migrations_dir = tmp_dir.path().join("migrations");
247 fs::create_dir(&migrations_dir).unwrap();
248 let sql1 = migrations_dir.join("V1first.sql");
249 fs::File::create(sql1).unwrap();
250 let sql2 = migrations_dir.join("V2second.sql");
251 fs::File::create(sql2).unwrap();
252
253 let mut mods = find_migration_files(migrations_dir, MigrationType::All).unwrap();
254 assert!(mods.next().is_none());
255 }
256
257 #[test]
261 fn finds_migrations_in_sorted_order() {
262 let tmp_dir = TempDir::new().unwrap();
263 let migrations_dir = tmp_dir.path().join("migrations");
264 let nested_dir = migrations_dir.join("nested");
265 fs::create_dir(&migrations_dir).unwrap();
266 fs::create_dir(&nested_dir).unwrap();
267
268 for file in [
270 migrations_dir.join("V3__third.sql"),
271 nested_dir.join("V2__second.rs"),
272 migrations_dir.join("U4__fourth.sql"),
273 migrations_dir.join("V1__first.sql"),
274 ] {
275 fs::File::create(file).unwrap();
276 }
277
278 let expected: Vec<PathBuf> = [
279 migrations_dir.join("U4__fourth.sql"),
280 migrations_dir.join("V1__first.sql"),
281 migrations_dir.join("V3__third.sql"),
282 nested_dir.join("V2__second.rs"),
283 ]
284 .iter()
285 .map(|path| path.canonicalize().unwrap())
286 .collect();
287
288 let mods: Vec<PathBuf> = find_migration_files(&migrations_dir, MigrationType::All)
289 .unwrap()
290 .collect();
291 assert_eq!(expected, mods);
292 }
293
294 #[test]
295 fn loads_migrations_from_path() {
296 let tmp_dir = TempDir::new().unwrap();
297 let migrations_dir = tmp_dir.path().join("migrations");
298 fs::create_dir(&migrations_dir).unwrap();
299 let sql1 = migrations_dir.join("V1__first.sql");
300 fs::File::create(&sql1).unwrap();
301 let sql2 = migrations_dir.join("V2__second.sql");
302 fs::File::create(&sql2).unwrap();
303 let rs3 = migrations_dir.join("V3__third.rs");
304 fs::File::create(&rs3).unwrap();
305
306 let migrations = load_sql_migrations(migrations_dir).unwrap();
307 assert_eq!(migrations.len(), 2);
308 assert_eq!(&migrations[0].to_string(), "V1__first");
309 assert_eq!(&migrations[1].to_string(), "V2__second");
310 }
311}