ts_sql_helper_lib/
migrations.rs

1//! Helpers for running migrations
2//!
3
4use std::{env::current_dir, ffi::OsStr, fs, io, path::PathBuf};
5
6use postgres::GenericClient;
7
8/// Runs the migrations in `current_dir()/migrations/*.sql` on the client, migrations are executed
9/// in name order.
10pub fn perform_migrations<C: GenericClient>(
11    client: &mut C,
12    override_directory: Option<PathBuf>,
13) -> Result<(), MigrationError> {
14    let path = match override_directory {
15        Some(path) => path,
16        None => {
17            let Ok(current_dir) = current_dir() else {
18                return Ok(());
19            };
20            current_dir.join("migrations")
21        }
22    };
23
24    if !fs::exists(&path).unwrap() {
25        return Ok(());
26    }
27
28    let directory =
29        fs::read_dir(&path).map_err(|source| MigrationError::ReadMigrationDirectory { source })?;
30    let mut entries: Vec<_> = directory
31        .filter_map(|entry| match entry {
32            Ok(entry) => {
33                if entry
34                    .path()
35                    .extension()
36                    .is_some_and(|extension| extension == OsStr::new("sql"))
37                {
38                    Some(Ok(entry))
39                } else {
40                    None
41                }
42            }
43            Err(error) => Some(Err(error)),
44        })
45        .collect::<Result<_, _>>()
46        .map_err(|source| MigrationError::ReadMigrationFile { source })?;
47    entries.sort_by_key(|entry| entry.file_name());
48
49    for entry in entries {
50        let sql = fs::read_to_string(entry.path())
51            .map_err(|source| MigrationError::ReadMigrationFile { source })?;
52        client
53            .batch_execute(&sql)
54            .map_err(|source| MigrationError::ExecuteMigration { source, sql })?;
55    }
56
57    Ok(())
58}
59
60/// Error variants for migrating a database.
61#[derive(Debug)]
62#[non_exhaustive]
63#[allow(missing_docs)]
64pub enum MigrationError {
65    #[non_exhaustive]
66    ReadMigrationDirectory { source: io::Error },
67
68    #[non_exhaustive]
69    ReadMigrationFile { source: io::Error },
70
71    #[non_exhaustive]
72    ExecuteMigration {
73        source: postgres::Error,
74        sql: String,
75    },
76}
77impl core::fmt::Display for MigrationError {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        match &self {
80            Self::ReadMigrationDirectory { .. } => write!(f, "could not read migration directory"),
81            Self::ReadMigrationFile { .. } => write!(f, "could not read a migration file"),
82            Self::ExecuteMigration { sql, .. } => write!(f, "migration `{sql}` failed to execute"),
83        }
84    }
85}
86impl core::error::Error for MigrationError {
87    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
88        match &self {
89            Self::ReadMigrationDirectory { source, .. } => Some(source),
90            Self::ReadMigrationFile { source, .. } => Some(source),
91            Self::ExecuteMigration { source, .. } => Some(source),
92        }
93    }
94}