use crate::db::connection::DatabasePool;
use sqlx::migrate::Migrator;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
pub async fn run_migrations(pool: &DatabasePool) -> crate::Result<()> {
let migrations_path = Path::new("migrations");
if !migrations_path.exists() {
return Err(crate::TlsError::DatabaseError(
"Migrations directory not found".to_string(),
));
}
match pool {
DatabasePool::Postgres(pg_pool) => {
let mut migrator = Migrator::new(migrations_path).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("Failed to create migrator: {}", e))
})?;
migrator.set_locking(false);
migrator.run(pg_pool).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("PostgreSQL migration failed: {}", e))
})?;
}
DatabasePool::Sqlite(sqlite_pool) => {
run_sqlite_migrations_manual(sqlite_pool, migrations_path).await?;
}
}
Ok(())
}
async fn run_sqlite_migrations_manual(
pool: &sqlx::SqlitePool,
migrations_path: &Path,
) -> crate::Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
success BOOLEAN NOT NULL,
execution_time BIGINT NOT NULL,
checksum BLOB NOT NULL
)
"#,
)
.execute(pool)
.await
.map_err(|e| {
crate::TlsError::DatabaseError(format!("Failed to create migrations table: {}", e))
})?;
let mut migration_files: Vec<PathBuf> = fs::read_dir(migrations_path)
.map_err(|e| {
crate::TlsError::DatabaseError(format!("Failed to read migrations directory: {}", e))
})?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "sql") {
Some(path)
} else {
None
}
})
.collect();
migration_files.sort();
for migration_file in migration_files {
let filename = migration_file
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
crate::TlsError::DatabaseError("Invalid migration filename".to_string())
})?
.to_string();
let version_str = filename
.chars()
.take_while(|c| c.is_numeric() || *c == '_')
.filter(|c| c.is_numeric())
.collect::<String>();
let version: i64 = version_str.parse().map_err(|_| {
crate::TlsError::DatabaseError(format!(
"Failed to parse migration version from {}",
filename
))
})?;
let already_run: bool =
sqlx::query_scalar("SELECT COUNT(*) > 0 FROM _sqlx_migrations WHERE version = ?")
.bind(version)
.fetch_one(pool)
.await
.map_err(|e| {
crate::TlsError::DatabaseError(format!(
"Failed to check migration status: {}",
e
))
})?;
if already_run {
continue; }
let sql_content = fs::read_to_string(&migration_file).map_err(|e| {
crate::TlsError::DatabaseError(format!(
"Failed to read migration file {}: {}",
filename, e
))
})?;
for statement in sql_content.split(';').filter(|s| !s.trim().is_empty()) {
sqlx::query(statement).execute(pool).await.map_err(|e| {
crate::TlsError::DatabaseError(format!(
"Failed to execute migration {}: {}",
filename, e
))
})?;
}
let checksum_placeholder = vec![0u8; 16]; sqlx::query(
"INSERT INTO _sqlx_migrations (version, description, success, execution_time, checksum) VALUES (?, ?, ?, ?, ?)"
)
.bind(version)
.bind(&filename)
.bind(true)
.bind(0i64) .bind(&checksum_placeholder)
.execute(pool)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Failed to record migration {}: {}", filename, e)))?;
}
Ok(())
}
pub async fn revert_migration(pool: &DatabasePool) -> crate::Result<()> {
let migrations_path = Path::new("migrations");
let migrator = Migrator::new(migrations_path)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Failed to create migrator: {}", e)))?;
match pool {
DatabasePool::Postgres(pg_pool) => {
migrator.undo(pg_pool, 1).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("PostgreSQL migration revert failed: {}", e))
})?;
}
DatabasePool::Sqlite(sqlite_pool) => {
migrator.undo(sqlite_pool, 1).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("SQLite migration revert failed: {}", e))
})?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::config::DatabaseConfig;
use std::path::PathBuf;
#[tokio::test]
async fn test_migrations_with_sqlite() {
let config = DatabaseConfig::sqlite(PathBuf::from(":memory:"));
let pool = DatabasePool::new(&config)
.await
.expect("test assertion should succeed");
pool.close().await;
}
}