use std::sync::Mutex;
use type_bridge_orm::session::backend::BoxFuture;
use crate::state::MigrationStateStore;
use crate::{AppliedMigrationRecord, MigrationRunRecord, Result};
pub struct InMemoryStateStore {
records: Mutex<Vec<AppliedMigrationRecord>>,
runs: Mutex<Vec<MigrationRunRecord>>,
}
impl InMemoryStateStore {
pub fn new() -> Self {
Self {
records: Mutex::new(Vec::new()),
runs: Mutex::new(Vec::new()),
}
}
}
impl Default for InMemoryStateStore {
fn default() -> Self {
Self::new()
}
}
impl MigrationStateStore for InMemoryStateStore {
fn ensure_schema(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(async { Ok(()) })
}
fn load_applied(&self) -> BoxFuture<'_, Result<Vec<AppliedMigrationRecord>>> {
let snapshot = self.records.lock().unwrap().clone();
Box::pin(async move { Ok(snapshot) })
}
fn load_runs(&self) -> BoxFuture<'_, Result<Vec<MigrationRunRecord>>> {
let snapshot = self.runs.lock().unwrap().clone();
Box::pin(async move { Ok(snapshot) })
}
fn record_applied(&self, record: AppliedMigrationRecord) -> BoxFuture<'_, Result<()>> {
{
let mut records = self.records.lock().unwrap();
if let Some(existing) = records
.iter_mut()
.find(|r| r.app_label == record.app_label && r.name == record.name)
{
*existing = record;
} else {
records.push(record);
}
}
Box::pin(async { Ok(()) })
}
fn record_unapplied<'a>(
&'a self,
app_label: &'a str,
name: &'a str,
) -> BoxFuture<'a, Result<()>> {
{
let mut records = self.records.lock().unwrap();
records.retain(|r| !(r.app_label == app_label && r.name == name));
}
Box::pin(async { Ok(()) })
}
fn record_run(&self, record: MigrationRunRecord) -> BoxFuture<'_, Result<()>> {
{
let mut runs = self.runs.lock().unwrap();
if let Some(existing) = runs.iter_mut().find(|r| r.run_id == record.run_id) {
*existing = record;
} else {
runs.push(record);
}
}
Box::pin(async { Ok(()) })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::MigrationStateStore;
fn record(app: &str, name: &str) -> AppliedMigrationRecord {
AppliedMigrationRecord {
app_label: app.to_string(),
name: name.to_string(),
checksum: format!("{app}-{name}-checksum"),
applied_at: Some("2026-06-05T00:00:00.000000".to_string()),
}
}
fn run_record(run_id: &str, status: &str) -> MigrationRunRecord {
MigrationRunRecord {
run_id: run_id.to_string(),
app_label: "app".to_string(),
name: "0001_initial".to_string(),
checksum: "checksum".to_string(),
direction: "apply".to_string(),
status: status.to_string(),
started_at: "2026-06-05T00:00:00.000000".to_string(),
finished_at: None,
error: None,
executor_ip: None,
executor_mac: None,
}
}
#[tokio::test]
async fn ensure_schema_is_a_no_op_ok() {
let store = InMemoryStateStore::new();
let result = store.ensure_schema().await;
assert!(result.is_ok());
let result2 = store.ensure_schema().await;
assert!(result2.is_ok());
}
#[tokio::test]
async fn load_applied_on_empty_store_returns_empty_vec() {
let store = InMemoryStateStore::new();
let applied = store.load_applied().await.unwrap();
assert!(applied.is_empty());
}
#[tokio::test]
async fn load_runs_on_empty_store_returns_empty_vec() {
let store = InMemoryStateStore::new();
let runs = store.load_runs().await.unwrap();
assert!(runs.is_empty());
}
#[tokio::test]
async fn record_applied_then_load_returns_the_record() {
let store = InMemoryStateStore::new();
let rec = record("myapp", "0001_initial");
store.record_applied(rec.clone()).await.unwrap();
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 1);
assert_eq!(applied[0], rec);
}
#[tokio::test]
async fn record_run_then_load_returns_the_record() {
let store = InMemoryStateStore::new();
let rec = run_record("run-1", "started");
store.record_run(rec.clone()).await.unwrap();
let runs = store.load_runs().await.unwrap();
assert_eq!(runs, vec![rec]);
}
#[tokio::test]
async fn record_applied_twice_does_not_duplicate() {
let store = InMemoryStateStore::new();
let rec = record("myapp", "0001_initial");
store.record_applied(rec.clone()).await.unwrap();
let rec2 = AppliedMigrationRecord {
checksum: "updated-checksum".to_string(),
..rec.clone()
};
store.record_applied(rec2.clone()).await.unwrap();
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 1, "must not duplicate on re-record");
assert_eq!(applied[0].checksum, "updated-checksum");
}
#[tokio::test]
async fn record_run_twice_replaces_existing_run() {
let store = InMemoryStateStore::new();
let rec = run_record("run-1", "started");
let finished = MigrationRunRecord {
status: "succeeded".to_string(),
finished_at: Some("2026-06-05T00:00:01.000000".to_string()),
..rec.clone()
};
store.record_run(rec).await.unwrap();
store.record_run(finished.clone()).await.unwrap();
let runs = store.load_runs().await.unwrap();
assert_eq!(runs, vec![finished]);
}
#[tokio::test]
async fn record_unapplied_removes_the_entry() {
let store = InMemoryStateStore::new();
store
.record_applied(record("myapp", "0001_initial"))
.await
.unwrap();
store
.record_applied(record("myapp", "0002_next"))
.await
.unwrap();
store
.record_unapplied("myapp", "0001_initial")
.await
.unwrap();
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 1);
assert_eq!(applied[0].name, "0002_next");
}
#[tokio::test]
async fn record_unapplied_absent_is_ok_no_op() {
let store = InMemoryStateStore::new();
let result = store.record_unapplied("myapp", "0001_initial").await;
assert!(result.is_ok());
store
.record_applied(record("myapp", "0002_next"))
.await
.unwrap();
let result2 = store.record_unapplied("myapp", "0001_initial").await;
assert!(result2.is_ok());
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 1);
}
#[tokio::test]
async fn insertion_order_is_stable_across_record_and_unrecord_sequences() {
let store = InMemoryStateStore::new();
store
.record_applied(record("app", "0001_initial"))
.await
.unwrap();
store
.record_applied(record("app", "0002_second"))
.await
.unwrap();
store
.record_applied(record("app", "0003_third"))
.await
.unwrap();
store.record_unapplied("app", "0002_second").await.unwrap();
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 2);
assert_eq!(applied[0].name, "0001_initial");
assert_eq!(applied[1].name, "0003_third");
store
.record_applied(record("app", "0002_second"))
.await
.unwrap();
let applied2 = store.load_applied().await.unwrap();
assert_eq!(applied2.len(), 3);
assert_eq!(applied2[0].name, "0001_initial");
assert_eq!(applied2[1].name, "0003_third");
assert_eq!(applied2[2].name, "0002_second");
}
#[tokio::test]
async fn trait_object_round_trip_is_consistent() {
let store: Box<dyn MigrationStateStore> = Box::new(InMemoryStateStore::new());
store.ensure_schema().await.unwrap();
assert!(store.load_applied().await.unwrap().is_empty());
let rec_a = record("orders", "0001_initial");
let rec_b = record("orders", "0002_add_index");
store.record_applied(rec_a.clone()).await.unwrap();
store.record_applied(rec_b.clone()).await.unwrap();
let applied = store.load_applied().await.unwrap();
assert_eq!(applied.len(), 2);
assert_eq!(applied[0], rec_a);
assert_eq!(applied[1], rec_b);
store
.record_unapplied("orders", "0001_initial")
.await
.unwrap();
let after_unrecord = store.load_applied().await.unwrap();
assert_eq!(after_unrecord.len(), 1);
assert_eq!(after_unrecord[0], rec_b);
store
.record_unapplied("orders", "0002_add_index")
.await
.unwrap();
assert!(store.load_applied().await.unwrap().is_empty());
}
}