ts_sql_helper_lib/
migrations.rs1use std::{env::current_dir, ffi::OsStr, fs, io, path::PathBuf};
5
6use postgres::GenericClient;
7
8pub 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#[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}