#[cfg(feature = "server")]
use sqlx::{PgPool, Row};
#[cfg(feature = "server")]
use std::path::Path;
#[cfg(feature = "server")]
use tokio::fs;
#[cfg(feature = "server")]
#[derive(Debug)]
pub struct Migration {
pub version: i32,
pub name: String,
pub sql: String,
}
#[cfg(feature = "server")]
pub struct MigrationRunner {
pool: PgPool,
}
#[cfg(feature = "server")]
impl MigrationRunner {
pub async fn new(database_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
let pool = PgPool::connect(database_url).await?;
Ok(Self { pool })
}
pub async fn init(&self) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS _migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"#,
)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_applied_migrations(&self) -> Result<Vec<i32>, Box<dyn std::error::Error>> {
let rows = sqlx::query("SELECT version FROM _migrations ORDER BY version")
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(|row| row.get("version")).collect())
}
pub async fn load_migrations<P: AsRef<Path>>(
&self,
migrations_dir: P,
) -> Result<Vec<Migration>, Box<dyn std::error::Error>> {
let mut migrations = Vec::new();
let mut entries = fs::read_dir(migrations_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("sql") {
let filename = path.file_name().unwrap().to_string_lossy();
if let Some((version_str, name_part)) = filename.split_once('_') {
if let Ok(version) = version_str.parse::<i32>() {
let name = name_part.trim_end_matches(".sql").to_string();
let sql = fs::read_to_string(&path).await?;
migrations.push(Migration { version, name, sql });
}
}
}
}
migrations.sort_by_key(|m| m.version);
Ok(migrations)
}
pub async fn migrate_up(
&self,
migrations: Vec<Migration>,
) -> Result<(), Box<dyn std::error::Error>> {
let applied = self.get_applied_migrations().await?;
let applied_set: std::collections::HashSet<i32> = applied.into_iter().collect();
for migration in migrations {
if applied_set.contains(&migration.version) {
println!(
" ⏭ Migration {} already applied: {}",
migration.version, migration.name
);
continue;
}
println!(
" ⚙ Applying migration {}: {}",
migration.version, migration.name
);
let mut tx = self.pool.begin().await?;
sqlx::query(&migration.sql)
.execute(&mut *tx)
.await
.map_err(|e| format!("Failed to execute migration {}: {}", migration.version, e))?;
sqlx::query(
"INSERT INTO _migrations (version, name, applied_at) VALUES ($1, $2, NOW())",
)
.bind(migration.version)
.bind(&migration.name)
.execute(&mut *tx)
.await?;
tx.commit().await?;
println!(" ✓ Migration {} applied successfully", migration.version);
}
Ok(())
}
pub async fn migrate_down(
&self,
migrations: Vec<Migration>,
) -> Result<(), Box<dyn std::error::Error>> {
let applied = self.get_applied_migrations().await?;
if applied.is_empty() {
println!(" ℹ No migrations to rollback");
return Ok(());
}
let last_version = applied.last().unwrap();
let migration = migrations.iter().find(|m| m.version == *last_version);
if let Some(migration) = migration {
println!(
" ⚙ Rolling back migration {}: {}",
migration.version, migration.name
);
println!(" ⚠ Note: Down migrations are not yet supported");
println!(" ℹ To rollback, manually drop tables and remove entry from _migrations");
sqlx::query("DELETE FROM _migrations WHERE version = $1")
.bind(migration.version)
.execute(&self.pool)
.await?;
println!(" ✓ Migration {} marked as rolled back", migration.version);
} else {
println!(
" ⚠ Migration {} not found in migrations directory",
last_version
);
}
Ok(())
}
pub async fn status(
&self,
migrations: Vec<Migration>,
) -> Result<(), Box<dyn std::error::Error>> {
let applied = self.get_applied_migrations().await?;
let applied_set: std::collections::HashSet<i32> = applied.into_iter().collect();
println!("\n Migration Status:");
println!(" {}", "─".repeat(60));
for migration in migrations {
let status = if applied_set.contains(&migration.version) {
"✓ Applied"
} else {
"✗ Pending"
};
println!(
" {} | {:3} | {}",
status, migration.version, migration.name
);
}
println!(" {}", "─".repeat(60));
println!();
Ok(())
}
pub async fn close(self) {
self.pool.close().await;
}
}
#[cfg(all(test, feature = "server"))]
mod tests {
#[tokio::test]
#[ignore] async fn test_migration_runner() {
}
}