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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Migrations directory management.
//!
//! This module is responsible for the management of the contents of the
//! migrations directory. At the top level it contains a migration_lock.toml file which lists the provider.
//! It also contains multiple subfolders, named after the migration id, and each containing:
//! - A migration script

use crate::{checksum, ConnectorError, ConnectorResult};
use std::{
    error::Error,
    fmt::Display,
    fs::{read_dir, DirEntry},
    io::{self, Write as _},
    path::{Path, PathBuf},
};
use tracing_error::SpanTrace;
use user_facing_errors::schema_engine::ProviderSwitchedError;

/// The file name for migration scripts, not including the file extension.
pub const MIGRATION_SCRIPT_FILENAME: &str = "migration";

/// The file name for the migration lock file, not including the file extension.
pub const MIGRATION_LOCK_FILENAME: &str = "migration_lock";

/// Create a directory for a new migration.
pub fn create_migration_directory(
    migrations_directory_path: &Path,
    migration_name: &str,
) -> io::Result<MigrationDirectory> {
    let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
    let directory_name = format!("{timestamp}_{migration_name}");
    let directory_path = migrations_directory_path.join(directory_name);

    if directory_path.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!(
                "The migration directory already exists at {}",
                directory_path.to_string_lossy()
            ),
        ));
    }

    std::fs::create_dir_all(&directory_path)?;

    Ok(MigrationDirectory { path: directory_path })
}

/// Write the migration_lock file to the directory.
pub fn write_migration_lock_file(migrations_directory_path: &str, provider: &str) -> std::io::Result<()> {
    let directory_path = Path::new(migrations_directory_path);
    let mut file_path = directory_path.join(MIGRATION_LOCK_FILENAME);

    file_path.set_extension("toml");

    tracing::debug!("Writing migration lockfile at {:?}", &file_path);

    let mut file = std::fs::File::create(&file_path)?;
    let content = format!(
        r##"# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "{provider}""##
    );

    file.write_all(content.as_bytes())?;

    Ok(())
}

/// Error if the provider in the schema does not match the one in the schema_lock.toml
pub fn error_on_changed_provider(migrations_directory_path: &str, provider: &str) -> ConnectorResult<()> {
    match match_provider_in_lock_file(migrations_directory_path, provider) {
        None => Ok(()),
        Some(Err(expected_provider)) => Err(ConnectorError::user_facing(ProviderSwitchedError {
            provider: provider.into(),
            expected_provider,
        })),
        Some(Ok(())) => Ok(()),
    }
}

/// Check whether provider matches. `None` means there was no migration_lock.toml file.
fn match_provider_in_lock_file(migrations_directory_path: &str, provider: &str) -> Option<Result<(), String>> {
    read_provider_from_lock_file(migrations_directory_path).map(|found_provider| {
        if found_provider == provider {
            Ok(())
        } else {
            Err(found_provider)
        }
    })
}

/// Read the provider from the migration_lock.toml. `None` means there was no migration_lock.toml
/// file in the directory.
pub fn read_provider_from_lock_file(migrations_directory_path: &str) -> Option<String> {
    let directory_path = Path::new(migrations_directory_path);
    let file_path = directory_path.join("migration_lock.toml");

    std::fs::read_to_string(file_path).ok().map(|content| {
        content
            .lines()
            .find(|line| line.starts_with("provider"))
            .map(|line| line.trim_start_matches("provider = ").trim_matches('"'))
            .unwrap_or("<PROVIDER NOT FOUND>")
            .to_owned()
    })
}

/// An IO error that occurred while reading the migrations directory.
#[derive(Debug)]
pub struct ListMigrationsError(io::Error);

impl Display for ListMigrationsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("An error occurred when reading the migrations directory.")
    }
}

impl Error for ListMigrationsError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.0)
    }
}

impl From<io::Error> for ListMigrationsError {
    fn from(err: io::Error) -> Self {
        ListMigrationsError(err)
    }
}

/// List the migrations present in the migration directory, lexicographically sorted by name.
///
/// If the migrations directory does not exist, it will not error but return an empty Vec.
pub fn list_migrations(migrations_directory_path: &Path) -> Result<Vec<MigrationDirectory>, ListMigrationsError> {
    let mut entries: Vec<MigrationDirectory> = Vec::new();

    let read_dir_entries = match read_dir(migrations_directory_path) {
        Ok(read_dir_entries) => read_dir_entries,
        Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => return Ok(entries),
        Err(err) => return Err(err.into()),
    };

    for entry in read_dir_entries {
        let entry = entry?;

        if entry.file_type()?.is_dir() {
            entries.push(entry.into());
        }
    }

    entries.sort_by(|a, b| a.migration_name().cmp(b.migration_name()));

    Ok(entries)
}

/// Proxy to a directory containing one migration, as returned by
/// `create_migration_directory` and `list_migrations`.
#[derive(Debug, Clone)]
pub struct MigrationDirectory {
    path: PathBuf,
}

/// Error while reading a migration script.
#[derive(Debug)]
pub struct ReadMigrationScriptError(pub(crate) io::Error, pub(crate) SpanTrace, pub(crate) String);

impl ReadMigrationScriptError {
    fn new(err: io::Error, file_path: &Path) -> Self {
        ReadMigrationScriptError(err, SpanTrace::capture(), file_path.to_string_lossy().into_owned())
    }
}

impl Display for ReadMigrationScriptError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Failed to read migration script at ")?;
        Display::fmt(&self.2, f)
    }
}

impl Error for ReadMigrationScriptError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.0)
    }
}

impl MigrationDirectory {
    /// Initialize a MigrationDirectory at the provided path. This will not
    /// validate that the path is valid and exists.
    pub fn new(path: PathBuf) -> MigrationDirectory {
        MigrationDirectory { path }
    }

    /// The `{timestamp}_{name}` formatted migration name.
    pub fn migration_name(&self) -> &str {
        self.path
            .file_name()
            .expect("MigrationDirectory::migration_id")
            .to_str()
            .expect("Migration directory name is not valid UTF-8.")
    }

    /// Check whether the checksum of the migration script matches the provided one.
    pub fn matches_checksum(&self, checksum_str: &str) -> Result<bool, ReadMigrationScriptError> {
        let filesystem_script = self.read_migration_script()?;
        Ok(checksum::script_matches_checksum(&filesystem_script, checksum_str))
    }

    /// Write the migration script to the directory.
    pub fn write_migration_script(&self, script: &str, extension: &str) -> std::io::Result<()> {
        let mut path = self.path.join(MIGRATION_SCRIPT_FILENAME);

        path.set_extension(extension);

        tracing::debug!("Writing migration script at {:?}", &path);

        let mut file = std::fs::File::create(&path)?;
        file.write_all(script.as_bytes())?;

        Ok(())
    }

    /// Read the migration script to a string.
    pub fn read_migration_script(&self) -> Result<String, ReadMigrationScriptError> {
        let path = self.path.join("migration.sql"); // todo why is it hardcoded here?
        std::fs::read_to_string(&path).map_err(|ioerr| ReadMigrationScriptError::new(ioerr, &path))
    }

    /// The filesystem path to the directory.
    pub fn path(&self) -> &Path {
        &self.path
    }
}

impl From<DirEntry> for MigrationDirectory {
    fn from(entry: DirEntry) -> MigrationDirectory {
        MigrationDirectory { path: entry.path() }
    }
}