midas_core/
lookup.rs

1use anyhow::{
2  Context,
3  Result as AnyhowResult,
4};
5use indoc::indoc;
6use regex::Regex;
7use std::collections::BTreeMap;
8use std::fs::{
9  self,
10  File,
11};
12use std::io::prelude::*;
13use std::io::BufReader;
14use std::path::Path;
15use std::string::ToString;
16use std::time::{
17  SystemTime,
18  UNIX_EPOCH,
19};
20
21pub type VecStr = Vec<String>;
22
23#[derive(Debug)]
24pub struct MigrationFile {
25  pub content_up: Option<VecStr>,
26  pub content_down: Option<VecStr>,
27  pub number: i64,
28  pub filename: String,
29}
30
31impl MigrationFile {
32  fn new(filename: &str, number: i64) -> Self {
33    Self {
34      content_up: None,
35      content_down: None,
36      filename: filename.to_owned(),
37      number,
38    }
39  }
40}
41
42/// A map of migration files
43pub type MigrationFiles = BTreeMap<i64, MigrationFile>;
44
45/// Parse the migration file
46fn parse_file(filename: &str) -> AnyhowResult<MigrationFile> {
47  // Regex to parse the migration file
48  let re = Regex::new(r"^(?P<number>[0-9]{13})_(?P<name>[_0-9a-zA-Z]*)\.sql$")?;
49
50  // Parse the filename
51  let result = re
52    .captures(filename)
53    .with_context(|| format!("Invalid filename found on {filename}"))?;
54
55  // Extract the number
56  let number = result
57    .name("number")
58    .context("The migration file timestamp is missing")?
59    .as_str()
60    .parse::<i64>()?;
61
62  Ok(MigrationFile::new(filename, number))
63}
64
65/// Build the migration list
66pub fn build_migration_list(path: &Path) -> AnyhowResult<MigrationFiles> {
67  let mut files: MigrationFiles = BTreeMap::new();
68  let entries = fs::read_dir(path)?.filter_map(Result::ok).collect::<Vec<_>>();
69
70  for entry in entries {
71    let filename = entry.file_name();
72    let Ok(info) = parse_file(filename.to_str().context("Filename is not valid")?) else {
73      continue;
74    };
75
76    let file = File::open(entry.path())?;
77    let mut buf_reader = BufReader::new(file);
78    let mut content = String::new();
79    buf_reader.read_to_string(&mut content)?;
80
81    let split_vec: Vec<String> = content.split('\n').map(ToString::to_string).collect();
82
83    let pos_up = split_vec
84      .iter()
85      .position(|s| s == "-- !UP" || s == "-- !UP\r")
86      .context("Parser can't find the UP migration")?;
87    let pos_down = split_vec
88      .iter()
89      .position(|s| s == "-- !DOWN" || s == "-- !DOWN\r")
90      .context("Parser can't find the DOWN migration")?;
91
92    let content_up = &split_vec[(pos_up + 1)..pos_down];
93    let content_down = &split_vec[(pos_down + 1)..];
94
95    let migration = MigrationFile {
96      content_up: Some(content_up.to_vec()),
97      content_down: Some(content_down.to_vec()),
98      ..info
99    };
100
101    log::trace!("Running the migration: {:?} {:?}", migration, migration.filename);
102    files.insert(migration.number, migration);
103  }
104
105  Ok(files)
106}
107
108/// Generate a timestamp string
109fn timestamp() -> String {
110  let start = SystemTime::now();
111  let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
112  since_the_epoch.as_millis().to_string()
113}
114
115// Create a new migration file
116pub fn create_migration_file(path: &Path, slug: &str) -> AnyhowResult<()> {
117  let filename = timestamp() + "_" + slug + ".sql";
118  let filepath = path.join(filename);
119
120  log::trace!("Creating new migration file: {:?}", filepath);
121  let mut f = File::create(filepath)?;
122  let contents = indoc! {"\
123    -- # Put your SQL below migration seperator.
124    -- !UP
125
126    -- !DOWN
127  "};
128
129  f.write_all(contents.as_bytes())?;
130  f.sync_all()?;
131
132  Ok(())
133}
134
135#[cfg(test)]
136mod tests {
137  use super::*;
138
139  #[test]
140  fn it_should_parse_correct_migration_filename() {
141    let result = parse_file("0000000000000_initial.sql").unwrap();
142    assert_eq!(result.number, 0);
143    assert_eq!(result.filename, "0000000000000_initial.sql");
144  }
145}