use crate::UpgraderError;
use crate::db_tracker::AppliedUpgrader;
use crate::schema_loader::SchemaUpgrader;
pub fn verify_integrity(
files_upgraders: &[SchemaUpgrader],
db_upgraders: &[AppliedUpgrader],
) -> Result<(), UpgraderError> {
let mut prev_applied_on = None;
for db_u in db_upgraders {
if let Some(prev) = prev_applied_on
&& db_u.applied_on < prev
{
return Err(UpgraderError::IntegrityError(format!(
"Upgrader {}:{} was applied at {}, which is before the previous upgrader ({})",
db_u.file_id, db_u.upgrader_id, db_u.applied_on, prev
)));
}
prev_applied_on = Some(db_u.applied_on);
}
let mut files_iter = files_upgraders.iter();
let mut db_iter = db_upgraders.iter();
loop {
let f = files_iter.next();
let d = db_iter.next();
match (f, d) {
(Some(file_u), Some(db_u)) => {
if file_u.file_id != db_u.file_id || file_u.upgrader_id != db_u.upgrader_id {
let file_tuple = (file_u.file_id, file_u.upgrader_id);
let db_tuple = (db_u.file_id, db_u.upgrader_id);
if file_tuple < db_tuple {
return Err(UpgraderError::IntegrityError(format!(
"Gap detected in database migrations. File upgrader {}:{} is missing in database, but later upgrader {}:{} is present.",
file_u.file_id, file_u.upgrader_id, db_u.file_id, db_u.upgrader_id
)));
} else {
return Err(UpgraderError::IntegrityError(format!(
"Database contains an upgrader {}:{} that is missing from the migration files.",
db_u.file_id, db_u.upgrader_id
)));
}
}
if file_u.text.trim() != db_u.text.trim() {
return Err(UpgraderError::IntegrityError(format!(
"Upgrader {}:{}. SQL content has changed.",
file_u.file_id, file_u.upgrader_id
)));
}
if file_u.description.trim() != db_u.description.trim() {
return Err(UpgraderError::IntegrityError(format!(
"Upgrader {}:{}. Description has changed.\nFile: '{}'\nDB: '{}'",
file_u.file_id, file_u.upgrader_id, file_u.description, db_u.description
)));
}
}
(Some(_), None) => {
return Ok(());
}
(None, Some(_db_u)) => {
return Ok(());
}
(None, None) => {
return Ok(());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn create_schema_upgrader(
file_id: i32,
upgrader_id: i32,
text: &str,
desc: &str,
) -> SchemaUpgrader {
SchemaUpgrader {
file_id,
upgrader_id,
description: desc.to_string(),
text: text.to_string(),
}
}
fn create_applied_upgrader(
file_id: i32,
upgrader_id: i32,
text: &str,
desc: &str,
) -> AppliedUpgrader {
AppliedUpgrader {
file_id,
upgrader_id,
description: desc.to_string(),
text: text.to_string(),
applied_on: Utc::now(),
}
}
#[test]
fn test_integrity_happy_path_exact_match() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(0, 1, "SQL2", "Desc2"),
];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_happy_path_pending_migrations() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL2", "Desc2"),
create_schema_upgrader(1, 0, "SQL3", "Desc3"),
];
let db = vec![create_applied_upgrader(0, 0, "SQL1", "Desc1")];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_happy_path_db_ahead_files_subset() {
let files = vec![create_schema_upgrader(0, 0, "SQL1", "Desc1")];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(0, 1, "SQL2", "Desc2"),
];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_fail_description_changed() {
let files = vec![create_schema_upgrader(0, 0, "SQL1", "New Desc")];
let db = vec![create_applied_upgrader(0, 0, "SQL1", "Old Desc")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(msg.contains("Description has changed")),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_text_changed() {
let files = vec![create_schema_upgrader(0, 0, "New SQL", "Desc1")];
let db = vec![create_applied_upgrader(0, 0, "Old SQL", "Desc1")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(msg.contains("SQL content has changed")),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_reordered_files() {
let files = vec![create_schema_upgrader(0, 0, "SQL_A", "Desc_A")];
let db = vec![create_applied_upgrader(0, 0, "SQL_B", "Desc_B")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(
msg.contains("SQL content has changed") || msg.contains("Description has changed")
),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_moved_upgrader_between_files() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(1, 0, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(0, 1, "SQL2", "Desc2"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(msg.contains(
"Database contains an upgrader 0:1 that is missing from the migration files"
)),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_changed_file_id() {
let files = vec![create_schema_upgrader(1, 0, "SQL", "Desc")];
let db = vec![create_applied_upgrader(0, 0, "SQL", "Desc")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("Database contains an upgrader 0:0 that is missing"))
}
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_changed_upgrader_id() {
let files = vec![create_schema_upgrader(0, 1, "SQL", "Desc")];
let db = vec![create_applied_upgrader(0, 0, "SQL", "Desc")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("Database contains an upgrader 0:0 that is missing"))
}
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_inserted_at_start() {
let files = vec![
create_schema_upgrader(0, 0, "SQL_New", "Desc_New"),
create_schema_upgrader(0, 1, "SQL_Old", "Desc_Old"),
];
let db = vec![create_applied_upgrader(0, 0, "SQL_Old", "Desc_Old")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(
msg.contains("SQL content has changed") || msg.contains("Description has changed")
),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_inserted_in_middle_file_gap() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL_New", "Desc_New"),
create_schema_upgrader(0, 2, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(0, 1, "SQL2", "Desc2"), ];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(
msg.contains("SQL content has changed") || msg.contains("Description has changed")
),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_inserted_in_middle_missing_in_db() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL2", "Desc2"),
create_schema_upgrader(0, 2, "SQL3", "Desc3"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(0, 2, "SQL3", "Desc3"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(
msg.contains("Gap detected in database migrations. File upgrader 0:1 is missing")
),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_happy_path_add_to_end_of_file_no_subsequent() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL2", "Desc2"),
];
let db = vec![create_applied_upgrader(0, 0, "SQL1", "Desc1")];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_fail_add_to_end_of_file_with_subsequent_exists() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(0, 1, "SQL_New", "Desc_New"),
create_schema_upgrader(1, 0, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL1", "Desc1"),
create_applied_upgrader(1, 0, "SQL2", "Desc2"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(
msg.contains("Gap detected in database migrations. File upgrader 0:1 is missing")
),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_happy_path_new_file() {
let files = vec![
create_schema_upgrader(0, 0, "SQL1", "Desc1"),
create_schema_upgrader(1, 0, "SQL2", "Desc2"),
];
let db = vec![create_applied_upgrader(0, 0, "SQL1", "Desc1")];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_success_leading_trailing_whitespace_change() {
let files = vec![create_schema_upgrader(0, 0, " SQL ", " Desc ")];
let db = vec![create_applied_upgrader(0, 0, "SQL", "Desc")];
assert!(verify_integrity(&files, &db).is_ok());
}
#[test]
fn test_integrity_fail_internal_whitespace_change() {
let files = vec![create_schema_upgrader(0, 0, "SELECT 1", "Desc")];
let db = vec![create_applied_upgrader(0, 0, "SELECT 1", "Desc")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(msg.contains("SQL content has changed")),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_case_sensitivity() {
let files = vec![create_schema_upgrader(0, 0, "SELECT 1", "Desc")];
let db = vec![create_applied_upgrader(0, 0, "select 1", "Desc")];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => assert!(msg.contains("SQL content has changed")),
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_multiple_gaps_finds_first() {
let files = vec![
create_schema_upgrader(0, 0, "SQL0", "Desc0"),
create_schema_upgrader(0, 1, "SQL1", "Desc1"),
create_schema_upgrader(0, 2, "SQL2", "Desc2"),
create_schema_upgrader(0, 3, "SQL3", "Desc3"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL0", "Desc0"),
create_applied_upgrader(0, 3, "SQL3", "Desc3"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("File upgrader 0:1 is missing"))
}
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_ghost_upgrader_in_middle() {
let files = vec![
create_schema_upgrader(0, 0, "SQL0", "Desc0"),
create_schema_upgrader(0, 2, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL0", "Desc0"),
create_applied_upgrader(0, 1, "SQL1", "Desc1"),
create_applied_upgrader(0, 2, "SQL2", "Desc2"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("Database contains an upgrader 0:1 that is missing"))
}
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_ghost_file_gap() {
let files = vec![
create_schema_upgrader(0, 0, "SQL0", "Desc0"),
create_schema_upgrader(2, 0, "SQL2", "Desc2"),
];
let db = vec![
create_applied_upgrader(0, 0, "SQL0", "Desc0"),
create_applied_upgrader(1, 0, "SQL1", "Desc1"),
create_applied_upgrader(2, 0, "SQL2", "Desc2"),
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("Database contains an upgrader 1:0 that is missing"))
}
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_integrity_fail_applied_on_out_of_order() {
use chrono::Duration;
let now = Utc::now();
let _later = now + Duration::seconds(10);
let earlier = now - Duration::seconds(10);
let files = vec![
create_schema_upgrader(0, 0, "SQL", "Desc"),
create_schema_upgrader(0, 1, "SQL", "Desc"),
];
let db = vec![
AppliedUpgrader {
file_id: 0,
upgrader_id: 0,
description: "Desc".to_string(),
text: "SQL".to_string(),
applied_on: now,
},
AppliedUpgrader {
file_id: 0,
upgrader_id: 1,
description: "Desc".to_string(),
text: "SQL".to_string(),
applied_on: earlier,
},
];
let err = verify_integrity(&files, &db).unwrap_err();
match err {
UpgraderError::IntegrityError(msg) => {
assert!(msg.contains("Upgrader 0:1 was applied at"))
}
_ => panic!("Unexpected error type"),
}
}
}