ts_sql_helper_lib/
migrations.rs

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