ts_sql_helper_lib/
migrations.rs

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