164 lines
5.1 KiB
Rust
164 lines
5.1 KiB
Rust
use resource::resource_str;
|
|
use rusqlite::{Connection, Transaction};
|
|
use sha2::{Digest, Sha256};
|
|
use std::time::Instant;
|
|
|
|
#[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,
|
|
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) -> Result<usize, rusqlite::Error> {
|
|
let q = "SELECT COALESCE(MAX(id) + 1, 0) FROM shitty_migrations";
|
|
return tx.query_row(q, (), |row| row.get::<usize, usize>(0));
|
|
}
|
|
|
|
pub fn migrate(conn: &mut Connection, migrations: &Vec<String>) -> 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)?;
|
|
} else {
|
|
apply_migration(&tx, i, migration)?;
|
|
}
|
|
}
|
|
tx.commit().map_err(|e| Error::Sql(e))?;
|
|
return Ok(());
|
|
}
|
|
|
|
fn apply_migration(tx: &Transaction, id: usize, migration: &String) -> Result<(), Error> {
|
|
let hash = compute_migration_sha256(migration);
|
|
|
|
let q = "INSERT INTO shitty_migrations(id, sha256) VALUES (?1, ?2)";
|
|
tx.execute(q, (&id, &hash)).map_err(|e| Error::Sql(e))?;
|
|
|
|
let now = Instant::now();
|
|
tx.execute_batch(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 {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(migration);
|
|
let ret = hasher.finalize();
|
|
return format!("{:x}", ret);
|
|
}
|
|
|
|
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::<usize, String>(0))
|
|
.map_err(|e| Error::Sql(e))?;
|
|
let expected = compute_migration_sha256(migration);
|
|
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(());
|
|
}
|
|
|
|
fn get_migrations() -> Vec<String> {
|
|
let mut ret = Vec::new();
|
|
ret.push(
|
|
resource_str!("resources/migrations/sql/create_tables_001.sql")
|
|
.as_ref()
|
|
.to_string(),
|
|
);
|
|
return ret;
|
|
}
|
|
|
|
pub fn apply_migrations(conn: &mut Connection) -> Result<(), Error> {
|
|
let migrations = get_migrations();
|
|
return migrate(conn, &migrations);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_migration {
|
|
use super::*;
|
|
|
|
fn create_basic_migrations() -> Vec<String> {
|
|
let migration1 = "CREATE TABLE people(name TEXT, age INTEGER);".to_string();
|
|
let migration2 = "INSERT INTO people(name, age) VALUES (\"didier\", 37);".to_string();
|
|
return vec![migration1, migration2];
|
|
}
|
|
|
|
fn create_basic_migrations_2() -> Vec<String> {
|
|
let migration1 = "CREATE TABLE people(name TEXT, age INTEGER);".to_string();
|
|
let migration2 = "INSERT INTO people(name, age) VALUES (\"stephanie\", 38);".to_string();
|
|
return vec![migration1, migration2];
|
|
}
|
|
|
|
#[test]
|
|
fn basic() {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
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).unwrap(), 2);
|
|
|
|
let q = "SELECT COUNT(*) FROM people;";
|
|
let c = tx.query_row(q, (), |x| x.get::<usize, usize>(0)).unwrap();
|
|
assert_eq!(c, c);
|
|
}
|
|
|
|
#[test]
|
|
fn idempotency() -> Result<(), Error> {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
|
|
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).unwrap(), 2);
|
|
let q = "SELECT COUNT(*) FROM people;";
|
|
let c = tx.query_row(q, (), |x| x.get::<usize, usize>(0)).unwrap();
|
|
assert_eq!(c, c);
|
|
return Ok(());
|
|
}
|
|
|
|
#[test]
|
|
fn broken_migration() -> Result<(), Error> {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
|
|
migrate(&mut conn, &create_basic_migrations())?;
|
|
let ret = migrate(&mut conn, &create_basic_migrations_2());
|
|
assert!(ret.is_err());
|
|
return Ok(());
|
|
}
|
|
}
|