use crate::database::DatabaseConnection; use crate::path::PathBuilder; use crate::StdResult; use std::fs; use std::path::PathBuf; #[derive(Debug)] pub struct Migration { upgrade_sql: PathBuf, downgrade_sql: PathBuf, name: String, } impl Migration { pub(crate) fn new(directory: &PathBuf) -> Option { if directory.is_dir() { let name = directory .file_name() .and_then(|name| name.to_str()) .unwrap_or_default(); let upgrade_sql = PathBuilder::from(directory).append("up.sql").build(); let downgrade_sql = PathBuilder::from(directory).append("down.sql").build(); if upgrade_sql.exists() && downgrade_sql.exists() { return Some(Migration { upgrade_sql, downgrade_sql, name: String::from(name), }); } } None } } impl Migration { pub fn name(&self) -> &String { &self.name } fn upgrade_sql_content(&self) -> StdResult { let content = fs::read_to_string(&self.upgrade_sql)?; Ok(content) } fn downgrade_sql_content(&self) -> StdResult { let content = fs::read_to_string(&self.downgrade_sql)?; Ok(content) } } pub struct MigrationManager { pub(crate) conn: Conn, } impl MigrationManager { pub fn new(conn: Conn) -> Self { MigrationManager { conn } } } pub fn is_migrations_table_not_found(error: D) -> bool { error .to_string() .contains(r#"relation "migrations" does not exist"#) } pub trait DatabaseMigrationManager { fn apply_sql(&mut self, sql_content: &str) -> StdResult<()>; fn create_migrations_table(&mut self) -> StdResult<()>; fn insert_migration_info(&mut self, name: &str) -> StdResult; fn delete_migration_info(&mut self, name: &str) -> StdResult; fn upgrade(&mut self, migration: &Migration) -> StdResult<()> { let content = migration.upgrade_sql_content()?; self.create_migrations_table()?; self.apply_sql(&content)?; self.insert_migration_info(migration.name())?; Ok(()) } fn downgrade(&mut self, migration: &Migration) -> StdResult<()> { let content = migration.downgrade_sql_content()?; self.apply_sql(&content)?; self.delete_migration_info(migration.name())?; Ok(()) } } impl DatabaseMigrationManager for MigrationManager where Conn: DatabaseConnection, { fn apply_sql(&mut self, sql_content: &str) -> StdResult<()> { self.conn.batch_execute(sql_content) } fn create_migrations_table(&mut self) -> StdResult<()> { self.conn.batch_execute( r#"CREATE TABLE IF NOT EXISTS migrations ( id serial PRIMARY KEY, name text NOT NULL UNIQUE )"#, ) } fn insert_migration_info(&mut self, name: &str) -> StdResult { self.conn .execute("INSERT INTO migrations (name) VALUES ($1)", &[&name]) } fn delete_migration_info(&mut self, name: &str) -> StdResult { self.conn .execute("DELETE FROM migrations WHERE name = $1", &[&name]) } } pub trait MigrationNames { const APPLIED_MIGRATIONS_STMT: &'static str = "SELECT name FROM migrations ORDER BY id DESC"; fn applied_migration_names(&mut self) -> StdResult>; } pub fn filter_pending_migrations( migrations: Vec, applied_migration_names: &[String], ) -> Vec { migrations .into_iter() .filter(|m| !applied_migration_names.contains(m.name())) .collect() }