diff --git a/butane_core/src/migrations/adb.rs b/butane_core/src/migrations/adb.rs index 2a5ffae2..8e2c5612 100644 --- a/butane_core/src/migrations/adb.rs +++ b/butane_core/src/migrations/adb.rs @@ -5,7 +5,7 @@ use crate::{Error, Result, SqlType, SqlVal}; use serde::{de::Deserializer, de::Visitor, ser::Serializer, Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; /// Identifier for a type as used in a database column. Supports both /// [`SqlType`] and identifiers known only by name. @@ -248,7 +248,7 @@ impl ADB { } /// Abstract representation of a database table schema. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ATable { pub name: String, pub columns: Vec, @@ -388,7 +388,7 @@ impl AColumn { } /// Individual operation use to apply a migration. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Operation { //future improvement: support column renames AddTable(ATable), @@ -402,8 +402,10 @@ pub enum Operation { /// Determine the operations necessary to move the database schema from `old` to `new`. pub fn diff(old: &ADB, new: &ADB) -> Vec { let mut ops: Vec = Vec::new(); - let new_names: HashSet<&String> = new.tables.keys().collect(); - let old_names: HashSet<&String> = old.tables.keys().collect(); + let new_names: BTreeSet<&String> = new.tables.keys().collect(); + let old_names: BTreeSet<&String> = old.tables.keys().collect(); + + // Add new tables let new_tables = new_names.difference(&old_names); for added in new_tables { let added: &str = added.as_ref(); @@ -411,9 +413,13 @@ pub fn diff(old: &ADB, new: &ADB) -> Vec { new.tables.get(added).expect("no table").clone(), )); } + + // Remove tables for removed in old_names.difference(&new_names) { ops.push(Operation::RemoveTable((*removed).to_string())); } + + // Change existing tables for table in new_names.intersection(&old_names) { let table: &str = table.as_ref(); ops.append(&mut diff_table( @@ -430,8 +436,10 @@ fn col_by_name<'a>(columns: &'a [AColumn], name: &str) -> Option<&'a AColumn> { fn diff_table(old: &ATable, new: &ATable) -> Vec { let mut ops: Vec = Vec::new(); - let new_names: HashSet<&String> = new.columns.iter().map(|c| &c.name).collect(); - let old_names: HashSet<&String> = old.columns.iter().map(|c| &c.name).collect(); + let new_names: BTreeSet<&String> = new.columns.iter().map(|c| &c.name).collect(); + let old_names: BTreeSet<&String> = old.columns.iter().map(|c| &c.name).collect(); + + // Add columns let added_names = new_names.difference(&old_names); for added in added_names { let added: &str = added.as_ref(); @@ -440,12 +448,16 @@ fn diff_table(old: &ATable, new: &ATable) -> Vec { col_by_name(&new.columns, added).unwrap().clone(), )); } + + // Remove columns for removed in old_names.difference(&new_names) { ops.push(Operation::RemoveColumn( old.name.clone(), (*removed).to_string(), )); } + + // Change columns for colname in new_names.intersection(&old_names) { let colname: &str = colname.as_ref(); let col = col_by_name(&new.columns, colname).unwrap(); diff --git a/butane_core/tests/migration.rs b/butane_core/tests/migration.rs new file mode 100644 index 00000000..d5a28453 --- /dev/null +++ b/butane_core/tests/migration.rs @@ -0,0 +1,139 @@ +use butane_core::migrations::adb::*; +use butane_core::SqlType; + +#[test] +fn empty_diff() { + let old = ADB::default(); + let new = ADB::default(); + let ops = diff(&old, &new); + assert_eq!(ops, vec![]); +} + +#[test] +fn add_table() { + let old = ADB::default(); + let mut new = ADB::default(); + let mut table = ATable::new("a".to_owned()); + let column = AColumn::new_simple( + "a".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column); + new.replace_table(table.clone()); + + let ops = diff(&old, &new); + + assert_eq!(ops, vec![Operation::AddTable(table)]); +} + +#[test] +fn remove_table() { + let mut old = ADB::default(); + let new = ADB::default(); + let mut table = ATable::new("a".to_owned()); + let column = AColumn::new_simple( + "a".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column); + old.replace_table(table.clone()); + + let ops = diff(&old, &new); + + let expected_op = Operation::RemoveTable("a".to_owned()); + assert_eq!(ops, vec![expected_op]); +} + +#[test] +fn stable_table_alpha_order() { + let old = ADB::default(); + let mut new = ADB::default(); + + // Insert tables out of order + let mut table_b = ATable::new("b".to_owned()); + let column_b = AColumn::new_simple( + "b".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table_b.add_column(column_b); + new.replace_table(table_b.clone()); + + let mut table_a = ATable::new("a".to_owned()); + let column_a = AColumn::new_simple( + "a".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table_a.add_column(column_a); + new.replace_table(table_a.clone()); + + let ops = diff(&old, &new); + + assert_eq!( + ops, + vec![Operation::AddTable(table_a), Operation::AddTable(table_b)] + ); +} + +#[test] +fn stable_new_table_column_insert_order() { + let old = ADB::default(); + let mut new = ADB::default(); + let mut table = ATable::new("a".to_owned()); + + // Insert columns out of order + let column_b = AColumn::new_simple( + "b".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column_b.clone()); + + let column_a = AColumn::new_simple( + "a".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column_a.clone()); + + // Add updated table to new + new.replace_table(table.clone()); + + let ops = diff(&old, &new); + + // Columns remain in insertion order + assert_eq!(ops, vec![Operation::AddTable(table)]); +} + +#[test] +fn stable_add_column_alpha_order() { + let mut old = ADB::default(); + let mut new = ADB::default(); + let mut table = ATable::new("a".to_owned()); + + // Add empty table to old + old.replace_table(table.clone()); + + // Insert columns out of order + let column_b = AColumn::new_simple( + "b".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column_b.clone()); + + let column_a = AColumn::new_simple( + "a".to_owned(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)), + ); + table.add_column(column_a.clone()); + + // Add updated table to new + new.replace_table(table.clone()); + + let ops = diff(&old, &new); + + assert_eq!( + ops, + vec![ + Operation::AddColumn("a".to_owned(), column_a), + Operation::AddColumn("a".to_owned(), column_b), + ] + ); +}