ts_sql_helper_lib/
migrations.rs1use std::{
5 env::current_dir,
6 ffi::OsStr,
7 fs::{self, DirEntry},
8 io,
9 path::PathBuf,
10};
11
12pub 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")]
34pub 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#[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}