dbmigrator/
recipe.rs

1use regex::Regex;
2use sha2::{Digest, Sha256};
3use std::cmp::Ordering;
4use std::collections::HashMap;
5use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use std::sync::Arc;
9use thiserror::Error;
10use version_compare::Cmp;
11use walkdir::{DirEntry, WalkDir};
12
13/// An Error occurred during a migration cycle
14#[derive(Debug, Error)]
15pub enum RecipeError {
16    #[error("invalid regex pattern")]
17    InvalidRegex(regex::Error),
18
19    #[error("invalid recipe script path `{path}`")]
20    InvalidRecipePath {
21        path: PathBuf,
22        source: std::io::Error,
23    },
24
25    #[error("invalid recipe script file `{path}`")]
26    InvalidRecipeFile {
27        path: PathBuf,
28        source: std::io::Error,
29    },
30
31    #[error("wrong filename format of recipe script `{file_stem}`")]
32    InvalidFilename { file_stem: String },
33
34    #[error("invalid recipe kind `{kind}`")]
35    InvalidRecipeKind { kind: String },
36
37    #[error("versions `{version}` must be unique for upgrade/baseline recipe (check `{name1}` and `{name2}`)"
38    )]
39    RepeatedVersion {
40        version: String,
41        name1: String,
42        name2: String,
43    },
44
45    #[error("old_checksum metadata is required for revert recipe `{version}` `{name}` - ")]
46    InvalidRevertMeta { version: String, name: String },
47
48    #[error("old_checksum, new_name and new_checksum metadata are required for fixup recipe `{version}` `{name}`"
49    )]
50    InvalidFixupMeta { version: String, name: String },
51
52    #[error("fixup `{version} {name}` cannot refer to existing recipe `{old_checksum}`")]
53    ConflictedFixup {
54        version: String,
55        name: String,
56        old_checksum: String,
57    },
58
59    #[error("unknown target `{new_version} {new_name} ({new_checksum})` in fixup migration `{version} {name}` for {old_checksum}`"
60    )]
61    InvalidFixupNewTarget {
62        version: String,
63        name: String,
64        old_checksum: String,
65        new_version: String,
66        new_name: String,
67        new_checksum: String,
68    },
69}
70
71#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
72pub enum RecipeKind {
73    Baseline,
74    Upgrade,
75    Revert,
76    Fixup,
77}
78
79impl FromStr for RecipeKind {
80    type Err = RecipeError;
81
82    fn from_str(s: &str) -> Result<RecipeKind, RecipeError> {
83        match s {
84            "baseline" => Ok(RecipeKind::Baseline),
85            "upgrade" => Ok(RecipeKind::Upgrade),
86            "revert" => Ok(RecipeKind::Revert),
87            "fixup" => Ok(RecipeKind::Fixup),
88            _ => Err(RecipeError::InvalidRecipeKind { kind: s.into() }),
89        }
90    }
91}
92
93impl std::fmt::Display for RecipeKind {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            RecipeKind::Baseline => write!(f, "baseline"),
97            RecipeKind::Upgrade => write!(f, "upgrade"),
98            RecipeKind::Revert => write!(f, "revert"),
99            RecipeKind::Fixup => write!(f, "fixup"),
100        }
101    }
102}
103
104#[derive(Clone, Debug)]
105enum RecipeMeta {
106    Baseline,
107    Upgrade,
108    Revert {
109        old_checksum: String,
110        maximum_version: String,
111    },
112    Fixup {
113        old_checksum: String,
114        maximum_version: String,
115        new_version: String,
116        new_name: String,
117        new_checksum: String,
118    },
119}
120
121#[derive(Clone, Debug)]
122pub struct RecipeScript {
123    version: String,
124    name: String,
125    checksum: String,
126    sql: Arc<String>,
127    meta: RecipeMeta,
128}
129
130impl RecipeScript {
131    pub fn new(
132        version: String,
133        name: String,
134        sql: String,
135        default_kind: Option<RecipeKind>,
136    ) -> Result<RecipeScript, RecipeError> {
137        let mut hasher = Sha256::new();
138        hasher.update(&sql);
139
140        let checksum = format!("{:x}", hasher.finalize());
141
142        let mut metadata = HashMap::new();
143        parse_sql_metadata(&sql, &mut metadata);
144
145        let mut version = version.to_string();
146        if let Some(meta_version) = metadata.get("version") {
147            version = meta_version.to_string();
148        }
149
150        let mut name = name.to_string();
151        if let Some(meta_name) = metadata.get("name") {
152            name = meta_name.to_string();
153        }
154
155        let mut kind = default_kind;
156        if let Some(meta_kind) = metadata.get("kind") {
157            kind = Some(RecipeKind::from_str(meta_kind)?);
158        }
159
160        let meta = match kind {
161            Some(RecipeKind::Baseline) => RecipeMeta::Baseline,
162            Some(RecipeKind::Upgrade) => RecipeMeta::Upgrade,
163            Some(RecipeKind::Revert) => {
164                if let Some(old_checksum) = metadata.get("old_checksum") {
165                    let maximum_version =
166                        metadata.get("maximum_version").unwrap_or(&version).clone();
167                    RecipeMeta::Revert {
168                        old_checksum: old_checksum.clone(),
169                        maximum_version,
170                    }
171                } else {
172                    return Err(RecipeError::InvalidRevertMeta { version, name });
173                }
174            }
175            Some(RecipeKind::Fixup) => {
176                if let (Some(old_checksum), Some(new_name), Some(new_checksum)) = (
177                    metadata.get("old_checksum"),
178                    metadata.get("new_name"),
179                    metadata.get("new_checksum"),
180                ) {
181                    let maximum_version =
182                        metadata.get("maximum_version").unwrap_or(&version).clone();
183                    let new_version = metadata.get("new_version").unwrap_or(&version).clone();
184                    RecipeMeta::Fixup {
185                        old_checksum: old_checksum.clone(),
186                        maximum_version,
187                        new_version,
188                        new_name: new_name.clone(),
189                        new_checksum: new_checksum.clone(),
190                    }
191                } else {
192                    return Err(RecipeError::InvalidFixupMeta { version, name });
193                }
194            }
195            _ => {
196                return Err(RecipeError::InvalidRecipeKind {
197                    kind: "unknown".to_string(),
198                });
199            }
200        };
201
202        Ok(RecipeScript {
203            version,
204            name,
205            checksum,
206            sql: Arc::new(sql),
207            meta,
208        })
209    }
210
211    pub fn version(&self) -> &str {
212        &self.version
213    }
214
215    pub fn name(&self) -> &str {
216        &self.name
217    }
218
219    pub fn sql(&self) -> &str {
220        &self.sql
221    }
222
223    pub fn kind(&self) -> RecipeKind {
224        match &self.meta {
225            RecipeMeta::Baseline => RecipeKind::Baseline,
226            RecipeMeta::Upgrade => RecipeKind::Upgrade,
227            RecipeMeta::Revert { .. } => RecipeKind::Revert,
228            RecipeMeta::Fixup { .. } => RecipeKind::Fixup,
229        }
230    }
231
232    pub fn is_baseline(&self) -> bool {
233        matches!(self.meta, RecipeMeta::Baseline)
234    }
235
236    pub fn is_upgrade(&self) -> bool {
237        matches!(self.meta, RecipeMeta::Upgrade)
238    }
239
240    pub fn match_checksum(&self, checksum: &str) -> bool {
241        // The minimum length of a checksum pattern is 8.
242        if checksum.len() < 8 {
243            return false;
244        }
245        self.checksum.starts_with(checksum)
246    }
247    pub fn checksum(&self) -> &str {
248        &self.checksum
249    }
250
251    pub fn checksum32(&self) -> &str {
252        &self.checksum[0..8]
253    }
254
255    pub fn old_checksum(&self) -> Option<&str> {
256        match &self.meta {
257            RecipeMeta::Revert { old_checksum, .. } => Some(old_checksum),
258            RecipeMeta::Fixup { old_checksum, .. } => Some(old_checksum),
259            _ => None,
260        }
261    }
262
263    pub fn old_checksum32(&self) -> Option<&str> {
264        match &self.meta {
265            RecipeMeta::Revert { old_checksum, .. } => Some(&old_checksum[0..8]),
266            RecipeMeta::Fixup { old_checksum, .. } => Some(&old_checksum[0..8]),
267            _ => None,
268        }
269    }
270
271    pub fn maximum_version(&self) -> Option<&str> {
272        match &self.meta {
273            RecipeMeta::Revert {
274                maximum_version, ..
275            } => Some(maximum_version),
276            RecipeMeta::Fixup {
277                maximum_version, ..
278            } => Some(maximum_version),
279            _ => None,
280        }
281    }
282
283    pub fn new_version(&self) -> Option<&str> {
284        match &self.meta {
285            RecipeMeta::Fixup { new_version, .. } => Some(new_version),
286            _ => None,
287        }
288    }
289
290    pub fn new_target(&self) -> Option<(&str, &str, &str)> {
291        match &self.meta {
292            RecipeMeta::Fixup {
293                new_version,
294                new_name,
295                new_checksum,
296                ..
297            } => Some((&new_version, new_name, new_checksum)),
298            _ => None,
299        }
300    }
301
302    pub fn new_checksum32(&self) -> Option<&str> {
303        match &self.meta {
304            RecipeMeta::Fixup { new_checksum, .. } => Some(&new_checksum[0..8]),
305            _ => None,
306        }
307    }
308}
309
310impl std::fmt::Display for RecipeScript {
311    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        write!(
313            fmt,
314            "{}{} {} ({})",
315            self.version,
316            if let Some(new_version) = self.new_version() {
317                if new_version != self.version {
318                    format!(" -> {}", new_version)
319                } else {
320                    "".to_string()
321                }
322            } else {
323                "".to_string()
324            },
325            self.name,
326            self.checksum32()
327        )
328    }
329}
330
331fn parse_sql_metadata(sql: &str, metadata: &mut HashMap<String, String>) {
332    for line in sql.lines() {
333        if !line.starts_with("--") {
334            break;
335        }
336        let parts: Vec<&str> = line[2..].splitn(2, ':').collect();
337        if parts.len() == 2 {
338            metadata.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
339        }
340    }
341}
342
343/// Find SQLs on file system recursively across given a location
344pub fn find_sql_files(
345    location: impl AsRef<Path>,
346) -> Result<impl Iterator<Item = PathBuf>, RecipeError> {
347    let location: &Path = location.as_ref();
348    let location = location
349        .canonicalize()
350        .map_err(|err| RecipeError::InvalidRecipePath {
351            path: location.to_path_buf(),
352            source: err,
353        })?;
354
355    let file_paths = WalkDir::new(location)
356        .into_iter()
357        .filter_map(Result::ok)
358        .map(DirEntry::into_path)
359        .filter(|entry| {
360            entry.is_file()
361                && match entry.extension() {
362                    Some(ext) => ext == OsStr::new("sql"),
363                    None => false,
364                }
365        });
366
367    Ok(file_paths)
368}
369
370/// Simple regex pattern for `{version}_{name}.sql` filename naming convention.
371///
372/// The version part must be alphanumeric with optional dots and dashes.
373/// For example, `1.0.0-001`, `20240201T1123`, `00001`.
374///
375/// The name part must be alphanumeric with optional dots, dashes, and underscores.
376/// For example, `create_user_table`, `add_email_column`, `issue_feature`.
377pub static SIMPLE_FILENAME_PATTERN: &str = r"^([[:alnum:].\-]+)_([[:alnum:]._\-]+)$";
378
379/// Simple recipe kind detector, allowing to determine the type of recipe
380/// using the recipe name.
381pub fn simple_kind_detector(_path: &Path, name: &str) -> Option<RecipeKind> {
382    if name.starts_with("baseline") {
383        Some(RecipeKind::Baseline)
384    } else if name.starts_with("revert") {
385        Some(RecipeKind::Revert)
386    } else if name.starts_with("fixup") {
387        Some(RecipeKind::Fixup)
388    } else {
389        Some(RecipeKind::Upgrade)
390    }
391}
392
393/// Default comparator for recipe versions. Usually requires fixed size of version parts.
394pub fn simple_compare(a: &str, b: &str) -> std::cmp::Ordering {
395    a.cmp(&b)
396}
397
398/// Compare two versions using the `version_compare` crate.
399/// Allow semver naming conventions.
400///
401/// For example, 1.0.0, 5.0.0, 5.3.0, 10.2.3, 10.10.1 will maintain the appropriate order.
402pub fn version_compare(a: &str, b: &str) -> std::cmp::Ordering {
403    let a = version_compare::Version::from(a);
404    let b = version_compare::Version::from(b);
405    match (a, b) {
406        (Some(l), Some(r)) => match l.compare(r) {
407            Cmp::Lt | Cmp::Le => std::cmp::Ordering::Less,
408            version_compare::Cmp::Eq => std::cmp::Ordering::Equal,
409            version_compare::Cmp::Gt | Cmp::Ge | Cmp::Ne => std::cmp::Ordering::Greater,
410        },
411        (Some(_), None) => Ordering::Greater,
412        (None, Some(_)) => Ordering::Less,
413        (None, None) => Ordering::Equal,
414    }
415}
416
417/// Loads SQL recipes from a path. This enables dynamic migration discovery, as opposed to
418/// embedding.
419pub fn load_sql_recipes(
420    recipes: &mut Vec<RecipeScript>,
421    file_paths: impl Iterator<Item = PathBuf>,
422    filename_pattern: &str,
423    kind_detector: Option<fn(&Path, &str) -> Option<RecipeKind>>,
424) -> Result<(), RecipeError> {
425    let re = Regex::new(filename_pattern).map_err(|e| RecipeError::InvalidRegex(e))?;
426
427    for path in file_paths {
428        let sql = std::fs::read_to_string(path.as_path()).map_err(|e| {
429            let path = path.to_owned();
430            match e.kind() {
431                std::io::ErrorKind::NotFound => RecipeError::InvalidRecipePath { path, source: e },
432                _ => RecipeError::InvalidRecipeFile { path, source: e },
433            }
434        })?;
435
436        //safe to call unwrap as find_migration_filenames returns canonical paths
437        match path
438            .file_stem()
439            .and_then(|os_str| os_str.to_os_string().into_string().ok())
440        {
441            Some(file_stem) => {
442                let captures =
443                    re.captures(&file_stem)
444                        .ok_or_else(|| RecipeError::InvalidFilename {
445                            file_stem: file_stem.clone(),
446                        })?;
447                let version: String = captures
448                    .get(1)
449                    .ok_or_else(|| RecipeError::InvalidFilename {
450                        file_stem: file_stem.clone(),
451                    })?
452                    .as_str()
453                    .to_string();
454                let name: String = captures
455                    .get(2)
456                    .ok_or_else(|| RecipeError::InvalidFilename {
457                        file_stem: file_stem.clone(),
458                    })?
459                    .as_str()
460                    .to_string();
461                let kind = match kind_detector {
462                    Some(kind_detector) => kind_detector(&path, &name),
463                    None => None,
464                };
465                let migration = RecipeScript::new(version, name, sql, kind)?;
466                recipes.push(migration);
467            }
468            None => {
469                return Err(RecipeError::InvalidRecipePath {
470                    path,
471                    source: std::io::Error::new(
472                        std::io::ErrorKind::InvalidData,
473                        "Invalid file name",
474                    ),
475                });
476            }
477        }
478    }
479    Ok(())
480}
481
482/// The recipe collection is ordered by version and verified.
483pub fn order_recipes(
484    recipes: &mut Vec<RecipeScript>,
485    version_comparator: fn(&str, &str) -> Ordering,
486) -> Result<(), RecipeError> {
487    let sorter = |item: &RecipeScript, version: &str, kind: RecipeKind| {
488        (version_comparator)(item.version(), version).then_with(|| item.kind().cmp(&kind))
489    };
490
491    recipes.sort_by(|a, b| (sorter)(a, b.version(), b.kind()));
492
493    for chunk in recipes.chunk_by(|a, b| a.version() == b.version()) {
494        let mut baseline: Option<&RecipeScript> = None;
495        let mut upgrade: Option<&RecipeScript> = None;
496
497        for item in chunk {
498            if item.is_baseline() {
499                // Check if there are no duplicate baseline recipes (only one per version).
500                if let Some(baseline) = baseline {
501                    return Err(RecipeError::RepeatedVersion {
502                        version: item.version().to_string(),
503                        name1: baseline.name().to_string(),
504                        name2: item.name().to_string(),
505                    });
506                }
507                baseline = Some(item);
508            } else if item.is_upgrade() {
509                // Check if there are no duplicate upgrade recipes (only one per version).
510                if let Some(upgrade) = upgrade {
511                    return Err(RecipeError::RepeatedVersion {
512                        version: item.version().to_string(),
513                        name1: upgrade.name().to_string(),
514                        name2: item.name().to_string(),
515                    });
516                }
517                upgrade = Some(item);
518            }
519        }
520        for item in chunk {
521            // Check if the revert/fixup script does not refer to an existing baseline or upgrade recipe.
522            if let Some(old_checksum) = item.old_checksum() {
523                if let Some(baseline) = baseline {
524                    if baseline.match_checksum(old_checksum) {
525                        return Err(RecipeError::ConflictedFixup {
526                            version: item.version().to_string(),
527                            name: item.name().to_string(),
528                            old_checksum: old_checksum.to_string(),
529                        });
530                    }
531                }
532                if let Some(upgrade) = upgrade {
533                    if upgrade.match_checksum(old_checksum) {
534                        return Err(RecipeError::ConflictedFixup {
535                            version: item.version().to_string(),
536                            name: item.name().to_string(),
537                            old_checksum: old_checksum.to_string(),
538                        });
539                    }
540                }
541                baseline = Some(item);
542            }
543        }
544    }
545    for item in recipes.iter() {
546        // Check if fixup scripts target refer to existing upgrade scripts.
547        if let Some((new_version, new_name, new_checksum)) = item.new_target() {
548            if !match recipes.binary_search_by(|a| (sorter)(a, new_version, RecipeKind::Upgrade)) {
549                Ok(index) => {
550                    recipes[index].name() == new_name && recipes[index].checksum() == new_checksum
551                }
552                Err(_) => false,
553            } {
554                return Err(RecipeError::InvalidFixupNewTarget {
555                    version: item.version().to_string(),
556                    name: item.name().to_string(),
557                    old_checksum: item.old_checksum().unwrap().to_string(),
558                    new_version: new_version.to_string(),
559                    new_name: new_name.to_string(),
560                    new_checksum: new_checksum.to_string(),
561                });
562            }
563        }
564    }
565    Ok(())
566}
567
568#[cfg(test)]
569mod tests {
570    use std::fs;
571    use std::path::PathBuf;
572    use tempfile::TempDir;
573
574    use super::*;
575
576    #[test]
577    fn test_kind_from_str() {
578        assert_eq!(
579            RecipeKind::from_str("baseline").unwrap(),
580            RecipeKind::Baseline
581        );
582        assert_eq!(
583            RecipeKind::from_str("upgrade").unwrap(),
584            RecipeKind::Upgrade
585        );
586        assert_eq!(RecipeKind::from_str("revert").unwrap(), RecipeKind::Revert);
587        assert_eq!(RecipeKind::from_str("fixup").unwrap(), RecipeKind::Fixup);
588        assert!(RecipeKind::from_str("unknown").is_err());
589    }
590
591    #[test]
592    fn test_parse_sql_metadata() {
593        let sql = "-- version: 1.0.0\n-- name: test_migration\n-- kind: upgrade\n-- old_checksum: abc123af\n-- new_checksum: def456dd\n-- maximum_version: 2.0.0\n-- new_version: 1.1.0\n-- new_name: new_test_migration\n\nSELECT * FROM test;\n-- some: data\n-- Extra comment...";
594        let mut metadata = HashMap::new();
595        parse_sql_metadata(sql, &mut metadata);
596
597        assert_eq!(metadata.get("version"), Some(&"1.0.0".to_string()));
598        assert_eq!(metadata.get("name"), Some(&"test_migration".to_string()));
599        assert_eq!(metadata.get("kind"), Some(&"upgrade".to_string()));
600        assert_eq!(metadata.get("old_checksum"), Some(&"abc123af".to_string()));
601        assert_eq!(metadata.get("new_checksum"), Some(&"def456dd".to_string()));
602        assert_eq!(metadata.get("maximum_version"), Some(&"2.0.0".to_string()));
603        assert_eq!(metadata.get("new_version"), Some(&"1.1.0".to_string()));
604        assert_eq!(
605            metadata.get("new_name"),
606            Some(&"new_test_migration".to_string())
607        );
608        assert!(metadata.get("some").is_none());
609    }
610
611    #[test]
612    fn test_parse_sql_metadata_with_no_metadata() {
613        let sql = "SELECT * FROM test;";
614        let mut metadata = HashMap::new();
615        parse_sql_metadata(sql, &mut metadata);
616
617        assert!(metadata.is_empty());
618    }
619
620    #[test]
621    fn test_parse_sql_metadata_with_partial_metadata() {
622        let sql =
623            "-- version: 1.0.0\n-- name: test_migration\nSELECT * FROM test;\n-- wrong: metadata";
624        let mut metadata = HashMap::new();
625        parse_sql_metadata(sql, &mut metadata);
626
627        assert_eq!(metadata.get("version"), Some(&"1.0.0".to_string()));
628        assert_eq!(metadata.get("name"), Some(&"test_migration".to_string()));
629        assert!(metadata.get("kind").is_none());
630        assert_eq!(metadata.len(), 2)
631    }
632
633    #[test]
634    fn test_simple_compare() {
635        assert_eq!(
636            simple_compare("20240201T112301", "20240201T112301"),
637            std::cmp::Ordering::Equal
638        );
639        assert_eq!(
640            simple_compare("20240201T112301", "20240202T112301"),
641            std::cmp::Ordering::Less
642        );
643        assert_eq!(
644            simple_compare("20240201T112301B", "20240201T112301"),
645            std::cmp::Ordering::Greater
646        );
647    }
648
649    #[test]
650    fn use_version_compare() {
651        assert_eq!(version_compare("1.0.0", "1.0.0"), std::cmp::Ordering::Equal);
652        assert_eq!(version_compare("2.0.0", "10.0.1"), std::cmp::Ordering::Less);
653        assert_eq!(
654            version_compare("1.0.0-14", "1.0.0-2"),
655            std::cmp::Ordering::Greater
656        );
657        assert_eq!(version_compare("1.0.0", "2.0.0"), std::cmp::Ordering::Less);
658        assert_eq!(
659            version_compare("2.0.0", "1.0.0"),
660            std::cmp::Ordering::Greater
661        );
662        assert_eq!(
663            version_compare("1.0.0-revB", "1.0.0-revA"),
664            std::cmp::Ordering::Greater
665        );
666        assert_eq!(
667            version_compare("1.20.4-m1", "1.100.2-m2"),
668            std::cmp::Ordering::Less
669        );
670    }
671
672    #[test]
673    fn find_sql_files_badly_named_files() {
674        let tmp_dir = TempDir::new().unwrap();
675        let migrations_dir = tmp_dir.path().join("migrations");
676        fs::create_dir(&migrations_dir).unwrap();
677        let sql1 = migrations_dir.join("2024-01-01Z1212_first.sql");
678        fs::create_dir(&sql1).unwrap();
679        let sql2 = migrations_dir.join("3.0_upgrade_comment.txt");
680        fs::File::create(sql2).unwrap();
681        let sql3 = migrations_dir.join("_3.2_upgrade");
682        fs::File::create(sql3).unwrap();
683        let sql4 = migrations_dir.join("3.2revert.SQL");
684        fs::File::create(sql4).unwrap();
685
686        assert_eq!(find_sql_files(migrations_dir).unwrap().count(), 0);
687    }
688
689    #[test]
690    fn find_sql_files_wrong_path() {
691        assert!(find_sql_files(Path::new("wrong_path")).is_err());
692    }
693
694    #[test]
695    fn find_sql_files_good_named() {
696        let tmp_dir = TempDir::new().unwrap();
697        let migrations_dir = tmp_dir.path().join("migrations");
698        fs::create_dir(&migrations_dir).unwrap();
699        let sql1 = migrations_dir.join("1.0.0_baseline.sql");
700        fs::File::create(&sql1).unwrap();
701        let sql2 = migrations_dir.join("1.1.0_upgrade_first.sql");
702        fs::File::create(&sql2).unwrap();
703        let sql5 = migrations_dir.join("2.0.1_upgrade_first.sql");
704        fs::File::create(&sql5).unwrap();
705        let sql6 = migrations_dir.join("2.0.2_upgrade_second.sql");
706        fs::File::create(&sql6).unwrap();
707        let sub_dir = migrations_dir.join("subfolder");
708        fs::create_dir(&sub_dir).unwrap();
709        let sql_ign1 = sub_dir.join("2.2.2_baseline_ignore");
710        fs::File::create(&sql_ign1).unwrap();
711        let sql7 = sub_dir.join("2.2.2_baseline.sql");
712        fs::File::create(&sql7).unwrap();
713        let sql4 = migrations_dir.join("1.2_upgrade_second.sql");
714        fs::File::create(&sql4).unwrap();
715        let sql3 = migrations_dir.join("1.2_baseline.sql");
716        fs::File::create(&sql3).unwrap();
717        let sql_ign2 = migrations_dir.join("2.2.2_baseline.txt");
718        fs::File::create(&sql_ign2).unwrap();
719
720        let mut mods: Vec<PathBuf> = find_sql_files(migrations_dir).unwrap().collect();
721        mods.sort();
722        assert_eq!(sql1.canonicalize().unwrap(), mods[0]);
723        assert_eq!(sql2.canonicalize().unwrap(), mods[1]);
724        assert_eq!(sql3.canonicalize().unwrap(), mods[2]);
725        assert_eq!(sql4.canonicalize().unwrap(), mods[3]);
726        assert_eq!(sql5.canonicalize().unwrap(), mods[4]);
727        assert_eq!(sql6.canonicalize().unwrap(), mods[5]);
728        assert_eq!(sql7.canonicalize().unwrap(), mods[6]);
729        assert_eq!(mods.len(), 7);
730    }
731
732    #[test]
733    fn use_load_sql_files_diesel() {
734        let sql_files = find_sql_files("../examples/pgsql_diesel1").unwrap();
735
736        let mut migration_scripts = Vec::new();
737        load_sql_recipes(
738            &mut migration_scripts,
739            sql_files,
740            SIMPLE_FILENAME_PATTERN,
741            Some(simple_kind_detector),
742        )
743        .unwrap();
744        for (index, script) in migration_scripts.iter().enumerate() {
745            println!("{}: {}", index, script);
746        }
747        assert_eq!(migration_scripts.len(), 9);
748        assert_eq!(
749            migration_scripts
750                .iter()
751                .filter(|a| a.kind() == RecipeKind::Baseline)
752                .count(),
753            1
754        );
755        assert_eq!(
756            migration_scripts
757                .iter()
758                .filter(|a| a.kind() == RecipeKind::Upgrade)
759                .count(),
760            8
761        );
762        assert_eq!(
763            migration_scripts
764                .iter()
765                .filter(|a| a.kind() == RecipeKind::Revert)
766                .count(),
767            0
768        );
769        assert_eq!(
770            migration_scripts
771                .iter()
772                .filter(|a| a.kind() == RecipeKind::Fixup)
773                .count(),
774            0
775        );
776
777        let sql_files = find_sql_files("../examples/pgsql_diesel2").unwrap();
778
779        let mut migration_scripts = Vec::new();
780        load_sql_recipes(
781            &mut migration_scripts,
782            sql_files,
783            SIMPLE_FILENAME_PATTERN,
784            Some(simple_kind_detector),
785        )
786        .unwrap();
787        order_recipes(&mut migration_scripts, simple_compare).unwrap();
788
789        assert_eq!(migration_scripts.len(), 21);
790        assert_eq!(
791            migration_scripts.iter().filter(|a| a.is_baseline()).count(),
792            1
793        );
794        assert_eq!(
795            migration_scripts.iter().filter(|a| a.is_upgrade()).count(),
796            20
797        );
798    }
799
800    fn use_load_sql_files_mattermost() {
801        let sql_files = find_sql_files("../examples/pgsql_mattermost_channels").unwrap();
802
803        let mut migration_scripts = Vec::new();
804        load_sql_recipes(
805            &mut migration_scripts,
806            sql_files,
807            SIMPLE_FILENAME_PATTERN,
808            Some(simple_kind_detector),
809        )
810        .unwrap();
811        order_recipes(&mut migration_scripts, simple_compare).unwrap();
812
813        assert_eq!(migration_scripts.len(), 128);
814        assert_eq!(
815            migration_scripts
816                .iter()
817                .filter(|a| a.kind() == RecipeKind::Upgrade)
818                .count(),
819            126
820        );
821        assert_eq!(
822            migration_scripts
823                .iter()
824                .filter(|a| a.kind() == RecipeKind::Revert)
825                .count(),
826            0
827        );
828        assert_eq!(
829            migration_scripts
830                .iter()
831                .filter(|a| a.kind() == RecipeKind::Fixup)
832                .count(),
833            1
834        );
835    }
836}