midas_core/
lookup.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use anyhow::{
  Context,
  Result as AnyhowResult,
};
use indoc::indoc;
use regex::Regex;
use std::collections::BTreeMap;
use std::fs::{
  self,
  File,
};
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use std::string::ToString;
use std::time::{
  SystemTime,
  UNIX_EPOCH,
};

pub type VecStr = Vec<String>;

#[derive(Debug)]
pub struct MigrationFile {
  pub content_up: Option<VecStr>,
  pub content_down: Option<VecStr>,
  pub number: i64,
  pub filename: String,
}

impl MigrationFile {
  fn new(filename: &str, number: i64) -> Self {
    Self {
      content_up: None,
      content_down: None,
      filename: filename.to_owned(),
      number,
    }
  }
}

/// A map of migration files
pub type MigrationFiles = BTreeMap<i64, MigrationFile>;

fn parse_file(filename: &str) -> AnyhowResult<MigrationFile> {
  let re = Regex::new(r"^(?P<number>[0-9]{13})_(?P<name>[_0-9a-zA-Z]*)\.sql$")?;

  let result = re
    .captures(filename)
    .with_context(|| format!("Invalid filename found on {filename}"))?;

  let number = result
    .name("number")
    .context("The migration file timestamp is missing")?
    .as_str()
    .parse::<i64>()?;

  Ok(MigrationFile::new(filename, number))
}

pub fn build_migration_list(path: &Path) -> AnyhowResult<MigrationFiles> {
  let mut files: MigrationFiles = BTreeMap::new();
  let entries = fs::read_dir(path)?.filter_map(Result::ok).collect::<Vec<_>>();

  for entry in entries {
    let filename = entry.file_name();
    let Ok(info) = parse_file(filename.to_str().context("Filename is not valid")?) else {
      continue;
    };

    let file = File::open(entry.path())?;
    let mut buf_reader = BufReader::new(file);
    let mut content = String::new();
    buf_reader.read_to_string(&mut content)?;

    let split_vec: Vec<String> = content.split('\n').map(ToString::to_string).collect();

    let pos_up = split_vec
      .iter()
      .position(|s| s == "-- !UP" || s == "-- !UP\r")
      .context("Parser can't find the UP migration")?;
    let pos_down = split_vec
      .iter()
      .position(|s| s == "-- !DOWN" || s == "-- !DOWN\r")
      .context("Parser can't find the DOWN migration")?;

    let content_up = &split_vec[(pos_up + 1)..pos_down];
    let content_down = &split_vec[(pos_down + 1)..];

    let migration = MigrationFile {
      content_up: Some(content_up.to_vec()),
      content_down: Some(content_down.to_vec()),
      ..info
    };

    log::trace!("Running the migration: {:?} {:?}", migration, migration.filename);
    files.insert(migration.number, migration);
  }

  Ok(files)
}

/// Generate a timestamp string
fn timestamp() -> String {
  let start = SystemTime::now();
  let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
  since_the_epoch.as_millis().to_string()
}

// Create a new migration file
pub fn create_migration_file(path: &Path, slug: &str) -> AnyhowResult<()> {
  let filename = timestamp() + "_" + slug + ".sql";
  let filepath = path.join(filename);

  log::trace!("Creating new migration file: {:?}", filepath);
  let mut f = File::create(filepath)?;
  let contents = indoc! {"\
    -- # Put your SQL below migration seperator.
    -- !UP

    -- !DOWN
  "};

  f.write_all(contents.as_bytes())?;
  f.sync_all()?;

  Ok(())
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_parse_file() {
    let result = parse_file("0000000000000_initial.sql").unwrap();
    assert_eq!(result.number, 0);
    assert_eq!(result.filename, "0000000000000_initial.sql");
  }
}