use crate::database::Database;
use crate::error::NookError;
const META_COLLECTION: &str = "_meta";
const APPLIED_KEY: &[u8] = b"applied_versions";
pub struct MigrationStatus {
pub current_version: u32,
pub applied_count: usize,
}
pub struct Runner<'a> {
db: &'a Database,
}
impl<'a> Runner<'a> {
#[must_use]
pub const fn new(db: &'a Database) -> Self {
Self { db }
}
fn applied(&self) -> Result<Vec<u32>, NookError> {
let raw = self.db.read(|tx| tx.get(META_COLLECTION, APPLIED_KEY))?;
raw.map_or_else(
|| Ok(vec![]),
|b| {
serde_json::from_slice(&b).map_err(|e| NookError::Migration {
msg: format!("corrupt ledger: {e}"),
})
},
)
}
pub fn list_applied(&self) -> Result<Vec<u32>, NookError> {
self.applied()
}
pub fn status(&self) -> Result<MigrationStatus, NookError> {
let a = self.applied()?;
Ok(MigrationStatus {
current_version: a.iter().copied().max().unwrap_or(0),
applied_count: a.len(),
})
}
pub fn list_pending(&self, all: &[u32]) -> Result<Vec<u32>, NookError> {
let a = self.applied()?;
Ok(all.iter().copied().filter(|v| !a.contains(v)).collect())
}
pub fn run(&self, all: &[u32]) -> Result<(), NookError> {
self.db.write(|tx| {
let raw = tx.get(META_COLLECTION, APPLIED_KEY)?;
let mut a: Vec<u32> = match raw {
None => Vec::new(),
Some(b) => serde_json::from_slice(&b).map_err(|e| NookError::Migration {
msg: format!("corrupt ledger: {e}"),
})?,
};
let initial_len = a.len();
for v in all {
if !a.contains(v) {
a.push(*v);
}
}
if a.len() == initial_len {
return Ok(());
}
a.sort_unstable();
a.dedup();
let bytes =
serde_json::to_vec(&a).map_err(|e| NookError::Migration { msg: e.to_string() })?;
tx.put(META_COLLECTION, APPLIED_KEY, &bytes)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::Database;
fn db() -> (tempfile::TempDir, Database) {
let d = tempfile::tempdir().unwrap();
let db = Database::open(d.path().join("t.db")).unwrap();
(d, db)
}
#[test]
fn status_starts_at_zero_and_run_advances_version() {
let (_d, db) = db();
let r = Runner::new(&db);
assert_eq!(r.status().unwrap().current_version, 0);
assert_eq!(r.list_applied().unwrap(), Vec::<u32>::new());
assert_eq!(r.list_pending(&[1, 2]).unwrap(), vec![1, 2]);
r.run(&[1, 2]).unwrap();
assert_eq!(r.status().unwrap().current_version, 2);
assert_eq!(r.list_applied().unwrap(), vec![1, 2]);
assert_eq!(r.list_pending(&[1, 2, 3]).unwrap(), vec![3]);
}
#[test]
fn run_is_idempotent_for_already_applied_versions() {
let (_d, db) = db();
let r = Runner::new(&db);
r.run(&[1]).unwrap();
r.run(&[1]).unwrap(); assert_eq!(r.status().unwrap().current_version, 1);
}
}