use crate::error::Result; use include_dir::Dir; use log::{debug, info}; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{OptionalExtension, params}; const TRACKING_TABLE: &str = "_sqlx_migrations"; /// Run SQL migrations from an embedded directory. /// /// Migrations are sorted by filename (use timestamp prefixes like `00000001_init.sql`). /// Applied migrations are tracked in `_sqlx_migrations`. pub fn run_migrations(pool: &Pool, dir: &Dir<'_>) -> Result<()> { info!("Running migrations"); // Create tracking table pool.get()?.execute( &format!( "CREATE TABLE IF NOT EXISTS {TRACKING_TABLE} ( version TEXT PRIMARY KEY, description TEXT NOT NULL, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL )" ), [], )?; // Read and sort all .sql files let mut entries: Vec<_> = dir .entries() .iter() .filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false)) .collect(); entries.sort_by_key(|e| e.path()); let mut ran_migrations = 0; for entry in &entries { let filename = entry.path().file_name().unwrap().to_str().unwrap(); let version = filename.split('_').next().unwrap(); // Check if already applied let already_applied: Option = pool .get()? .query_row( &format!("SELECT 1 FROM {TRACKING_TABLE} WHERE version = ?"), [version], |r| r.get(0), ) .optional()?; if already_applied.is_some() { debug!("Skipping already applied migration: {}", filename); continue; } let sql = entry.as_file().unwrap().contents_utf8().expect("Failed to read migration file"); info!("Applying migration: {}", filename); let conn = pool.get()?; conn.execute_batch(sql)?; // Record migration conn.execute( &format!("INSERT INTO {TRACKING_TABLE} (version, description) VALUES (?, ?)"), params![version, filename], )?; ran_migrations += 1; } if ran_migrations == 0 { info!("No migrations to run"); } else { info!("Ran {} migration(s)", ran_migrations); } Ok(()) }