use std::sync::Mutex;
use async_trait::async_trait;
use chrono::Utc;
use soma_schema::driver::{AppliedMigration, LockGuard, MigrationDriver};
use soma_schema::error::Result;
use soma_schema::include_dir::include_dir;
use soma_schema::migration::Migration;
use soma_schema::Migrator;
static FIXTURE_DIR: soma_schema::include_dir::Dir =
include_dir!("$CARGO_MANIFEST_DIR/tests/fixtures/embedded-migrations");
struct InMemoryDriver {
rows: Mutex<Vec<AppliedMigration>>,
apply_order: Mutex<Vec<String>>,
}
impl InMemoryDriver {
fn new() -> Self {
Self {
rows: Mutex::new(Vec::new()),
apply_order: Mutex::new(Vec::new()),
}
}
}
struct NoopLock;
impl LockGuard for NoopLock {}
#[async_trait]
impl MigrationDriver for InMemoryDriver {
async fn acquire_lock(&self) -> Result<Box<dyn LockGuard>> {
Ok(Box::new(NoopLock))
}
async fn run_setup_sql(&self, _name: &str, _sql: &str) -> Result<()> {
Ok(())
}
async fn ensure_tracking_table(&self) -> Result<()> {
Ok(())
}
async fn applied(&self) -> Result<Vec<AppliedMigration>> {
let rows = self.rows.lock().unwrap();
let out = rows
.iter()
.map(|r| {
AppliedMigration::new(
r.version,
r.file.clone(),
r.name.clone(),
r.checksum.clone(),
r.description.clone(),
r.batch,
r.applied_at,
r.applied_by.clone(),
r.execution_ms,
)
})
.collect();
Ok(out)
}
async fn apply(&self, migration: &Migration, _up_sql: &str, batch: i32) -> Result<()> {
let row = AppliedMigration::new(
migration.version,
migration.file.clone(),
migration.name.clone(),
migration.checksum.clone(),
None,
batch,
Utc::now(),
"in_memory".to_owned(),
None,
);
self.rows.lock().unwrap().push(row);
self.apply_order
.lock()
.unwrap()
.push(migration.file.clone());
Ok(())
}
async fn revert(&self, applied: &AppliedMigration, _down_sql: &str) -> Result<()> {
let mut rows = self.rows.lock().unwrap();
if let Some(pos) = rows
.iter()
.position(|r| r.version == applied.version && r.file == applied.file)
{
rows.remove(pos);
}
Ok(())
}
}
#[tokio::test]
async fn test_from_embedded_up_applies_migrations() {
let migrator = Migrator::from_embedded(&FIXTURE_DIR).expect("from_embedded should succeed");
let driver = InMemoryDriver::new();
migrator
.up(&driver)
.await
.expect("up() via from_embedded should succeed");
let order = driver.apply_order.lock().unwrap().clone();
assert_eq!(order.len(), 1, "one migration in the fixture");
assert_eq!(order[0], "20260101_01_init.sql");
}
#[tokio::test]
async fn test_from_embedded_up_is_idempotent() {
let migrator = Migrator::from_embedded(&FIXTURE_DIR).expect("from_embedded should succeed");
let driver = InMemoryDriver::new();
migrator.up(&driver).await.unwrap();
migrator.up(&driver).await.unwrap();
let order = driver.apply_order.lock().unwrap().clone();
assert_eq!(order.len(), 1, "second up() should apply nothing new");
}