//! Migration to update the patch `reviews` JSON representation. use crate::cob::cache::*; use serde_json as json; /// Run migration. pub fn run( db: &sql::Connection, migration: &Progress, callback: &mut dyn MigrateCallback, ) -> Result { // Select patches with reviews. let rows = db .prepare("SELECT id, patch FROM patches WHERE json_extract(patch, '$.reviews') != '{}'")? .into_iter() .collect::>(); // Query to update a patch to the new schema. let mut update = db.prepare( "UPDATE patches SET patch = ?1 WHERE id = ?2", )?; let mut progress = Progress::new(rows.len()); callback.progress(MigrateProgress { migration, rows: &progress, }); for row in rows { let row = row?; let id = row.try_read::<&str, _>("id")?; let mut patch = json::from_str::(row.try_read::<&str, _>("patch")?) .map_err(Error::MalformedJson)?; let patch = patch.as_object_mut().ok_or(Error::MalformedJsonSchema)?; let revisions = patch["revisions"] .as_object_mut() .ok_or(Error::MalformedJsonSchema)?; let mut transformed = false; for (_, r) in revisions.iter_mut() { let Some(revision) = r.as_object_mut() else { // Redacted revision (`null`). continue; }; let reviews = revision .get_mut("reviews") .ok_or(Error::MalformedJsonSchema)? .as_object_mut() .ok_or(Error::MalformedJsonSchema)?; for (_, review) in reviews.iter_mut() { if let Some(list) = review.as_array_mut() && let Some(last) = list.pop() { *review = last; transformed = true; } } } if transformed { let obj = json::to_string(&patch).map_err(Error::MalformedJson)?; update.reset()?; update.bind((1, obj.as_str()))?; update.bind((2, id))?; update.next()?; progress.inc(); callback.progress(MigrateProgress { migration, rows: &progress, }); } } Ok(progress.current()) } #[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { use crate::cob::cache::*; // Before the migration. const PATCH_V1: &str = include_str!("samples/patch.v1.json"); // After the migration. const PATCH_V2: &str = include_str!("samples/patch.v2.json"); #[test] fn test_migration_2() { let mut db = StoreWriter::memory().unwrap(); db.migrate_to(1, migrate::ignore).unwrap(); db.raw_query(|conn| { let mut stmt = conn.prepare( "INSERT INTO patches (id, repo, patch) VALUES (?1, ?2, ?3)", )?; stmt.bind((1, "016a91d2029ee71b9aee8d927664caf1b7885346"))?; stmt.bind((2, "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5"))?; stmt.bind((3, PATCH_V1))?; stmt.next()?; Ok::<_, sql::Error>(()) }) .unwrap(); assert_eq!(db.migrate_to(2, migrate::ignore).unwrap(), 2); let row = db .raw_query(|conn| { Ok::<_, sql::Error>( conn.prepare("SELECT patch FROM patches LIMIT 1")? .into_iter() .next() .unwrap() .unwrap(), ) }) .unwrap(); let patch = row.try_read::<&str, _>("patch").unwrap(); let actual: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(PATCH_V2).unwrap(); assert_eq!(actual, expected); } #[test] fn test_patch_json_deserialization() { serde_json::from_str::(PATCH_V1).unwrap_err(); serde_json::from_str::(PATCH_V2).unwrap(); } }