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#[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 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
343pub 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
370pub static SIMPLE_FILENAME_PATTERN: &str = r"^([[:alnum:].\-]+)_([[:alnum:]._\-]+)$";
378
379pub 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
393pub fn simple_compare(a: &str, b: &str) -> std::cmp::Ordering {
395 a.cmp(&b)
396}
397
398pub 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
417pub 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 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
482pub 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 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 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 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 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}