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
use anyhow::{Context, Result as AnyhowResult};
use indoc::indoc;
use regex::Regex;
use std::collections::BTreeMap;
use std::fs::{read_dir, File};
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
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,
        }
    }
}

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();

    for entry in read_dir(path)? {
        let entry = entry?;
        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(std::string::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::debug!(
            "Running the migration: {:?} {:?}",
            migration,
            migration.filename
        );
        files.insert(migration.number, migration);
    }

    Ok(files)
}

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()
}

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

    log::debug!("Creating new migration file: {:?}", filepath);
    let mut f = File::create(filepath)?;
    let contents = indoc! {"\
        -- # Put the 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");
    }
}