From 4a86c4250a25ceee072111db3507c7dfbd181a43 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Wed, 7 Dec 2022 10:43:05 -0500 Subject: [PATCH] Implemented a nice migration library. --- src/main.rs | 2 +- src/migration.rs | 98 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index e451b5f..8ceecbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -315,7 +315,7 @@ fn main() { set_logger(&m); let mut conn = get_connection(&m); let migrations: Vec = Vec::new(); - migrate(&mut conn, & migrations); + migrate(&mut conn, & migrations).unwrap(); let mut logic = ProCommandLineLogic::new(); process_command_line(&conn, &m, &mut logic); } \ No newline at end of file diff --git a/src/migration.rs b/src/migration.rs index 1ac4f88..bade694 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -1,40 +1,70 @@ +use std::time::Instant; use rusqlite::{Connection, Transaction}; - use sha2::{Digest, Sha256}; +#[derive(Debug)] +pub enum IntegrityError { + MigrationCheckFailed { + migration_id: usize, + migration_definition: String, + migration_sha256: String, + existing_migration_sha256: String, + }, +} + +#[derive(Debug)] +pub enum Error { + Sql(rusqlite::Error), + Integrity(IntegrityError), +} + const INITIAL_MIGRATION: &str = " CREATE TABLE IF NOT EXISTS shitty_migrations ( - id INTEGER PRIMARY KEY, - sha256 BLOB(2) + id INTEGER PRIMARY KEY, + creation_timestamp DATETIME DEFAULT current_timestamp NOT NULL, + duration REAL DEFAULT 0 NOT NULL, + sha256 BLOB(2) NOT NULL ); "; -fn get_id_of_new_migration(tx: &Transaction) -> usize { +fn get_id_of_new_migration(tx: &Transaction) -> Result { let q = "SELECT COALESCE(MAX(id) + 1, 0) FROM shitty_migrations"; - return tx.query_row(q, (), |row| row.get::(0)).unwrap(); + return tx.query_row(q, (), |row| row.get::(0)); } -pub fn migrate(conn: &mut Connection, migrations: &Vec) { - let tx = conn.transaction().unwrap(); - tx.execute(INITIAL_MIGRATION, ()).unwrap(); - let id = get_id_of_new_migration(&tx); +pub fn migrate(conn: &mut Connection, migrations: &Vec) -> Result<(), Error> { + let tx = conn.transaction().map_err(|e| Error::Sql(e))?; + tx.execute(INITIAL_MIGRATION, ()).map_err(|e| Error::Sql(e))?; + let id = get_id_of_new_migration(&tx).map_err(|e| Error::Sql(e))?; for (i, migration) in migrations.iter().enumerate() { if i < id { - check_migration(&tx, i, migration); + check_migration(&tx, i, migration)?; } else { - apply_migration(&tx, i, migration); + apply_migration(&tx, i, migration)?; } } - tx.commit().unwrap(); + tx.commit().map_err(|e| Error::Sql(e))?; + return Ok(()); } -fn apply_migration(tx: &Transaction, id: usize, migration: &String) { +fn apply_migration(tx: &Transaction, id: usize, migration: &String) -> Result<(), Error> { let hash = compute_migration_sha256(migration); - tx.execute(migration, ()).unwrap(); + + let q = "INSERT INTO shitty_migrations(id, sha256) VALUES (?1, ?2)"; - tx.execute(q, (&id, &hash)).unwrap(); + tx.execute(q, (&id, &hash)).map_err(|e| Error::Sql(e))?; + // let migration_id = tx.last_insert_rowid(); + + let now = Instant::now(); + tx.execute(migration, ()).map_err(|e| Error::Sql(e))?; + let elapsed_time = now.elapsed(); + + let q = "UPDATE shitty_migrations SET duration = ?1 WHERE id = ?2"; + tx.execute(q, (elapsed_time.as_secs_f32(), &id)).map_err(|e| Error::Sql(e))?; + + return Ok(()); } fn compute_migration_sha256(migration: &String) -> String { @@ -44,11 +74,19 @@ fn compute_migration_sha256(migration: &String) -> String { return format!("{:x}", ret); } -fn check_migration(tx: &Transaction, id: usize, migration: &String) { +fn check_migration(tx: &Transaction, id: usize, migration: &String) -> Result<(), Error> { let q = "SELECT sha256 FROM shitty_migrations WHERE id = ?1"; - let res = tx.query_row(q, (&id, ), |row| row.get::(0)).unwrap(); + let res = tx.query_row(q, (&id, ), |row| row.get::(0)).map_err(|e| Error::Sql(e))?; let expected = compute_migration_sha256(migration); - assert_eq!(&res, &expected); + if !res.eq(&expected) { + return Err(Error::Integrity(IntegrityError::MigrationCheckFailed { + migration_definition: migration.clone(), + migration_id: id, + migration_sha256: expected, + existing_migration_sha256: res, + })); + } + return Ok(()); } #[cfg(test)] @@ -70,9 +108,10 @@ mod test { #[test] fn basic() { let mut conn = Connection::open_in_memory().unwrap(); - migrate(&mut conn, &create_basic_migrations()); + let ret = migrate(&mut conn, &create_basic_migrations()); + assert!(ret.is_ok()); let tx = conn.transaction().unwrap(); - assert_eq!(get_id_of_new_migration(&tx), 2); + assert_eq!(get_id_of_new_migration(&tx).unwrap(), 2); let q = "SELECT COUNT(*) FROM people;"; let c = tx.query_row(q, (), |x| x.get::(0)).unwrap(); @@ -80,27 +119,28 @@ mod test { } #[test] - fn idempotency() { + fn idempotency() -> Result<(), Error> { let mut conn = Connection::open_in_memory().unwrap(); - migrate(&mut conn, &create_basic_migrations()); - migrate(&mut conn, &create_basic_migrations()); + migrate(&mut conn, &create_basic_migrations())?; + migrate(&mut conn, &create_basic_migrations())?; let tx = conn.transaction().unwrap(); - assert_eq!(get_id_of_new_migration(&tx), 2); + assert_eq!(get_id_of_new_migration(&tx).unwrap(), 2); let q = "SELECT COUNT(*) FROM people;"; let c = tx.query_row(q, (), |x| x.get::(0)).unwrap(); assert_eq!(c, c); + return Ok(()); } - #[ignore] #[test] - fn broken_migration() { + fn broken_migration() -> Result<(), Error> { let mut conn = Connection::open_in_memory().unwrap(); - migrate(&mut conn, &create_basic_migrations()); - migrate(&mut conn, &create_basic_migrations_2()); - + migrate(&mut conn, &create_basic_migrations())?; + let ret = migrate(&mut conn, &create_basic_migrations_2()); + assert!(ret.is_err()); + return Ok(()); } } \ No newline at end of file