use super::{Migration, MigrationError, Operation, Result};
use std::collections::HashSet;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SquashOptions {
pub optimize: bool,
pub no_optimize: bool,
}
impl Default for SquashOptions {
fn default() -> Self {
Self {
optimize: true,
no_optimize: false,
}
}
}
pub struct MigrationSquasher {
_private: (),
}
impl MigrationSquasher {
pub fn new() -> Self {
Self { _private: () }
}
pub fn squash(
&self,
migrations: &[Migration],
squashed_name: impl Into<String>,
options: SquashOptions,
) -> Result<Migration> {
if migrations.is_empty() {
return Err(MigrationError::InvalidMigration(
"Cannot squash empty migration list".to_string(),
));
}
let app_label = &migrations[0].app_label;
if !migrations.iter().all(|m| m.app_label == *app_label) {
return Err(MigrationError::InvalidMigration(
"All migrations must belong to the same app".to_string(),
));
}
let mut operations = Vec::new();
for migration in migrations {
operations.extend(migration.operations.clone());
}
if options.optimize && !options.no_optimize {
operations = self.optimize_operations(operations);
}
let mut squashed = Migration::new(squashed_name, app_label.clone());
squashed.operations = operations;
for migration in migrations {
squashed
.replaces
.push((migration.app_label.clone(), migration.name.clone()));
}
let squashed_set: HashSet<(&str, &str)> = migrations
.iter()
.map(|m| (m.app_label.as_str(), m.name.as_str()))
.collect();
let mut seen_deps: HashSet<(&str, &str)> = HashSet::new();
for migration in migrations {
for (dep_app, dep_name) in &migration.dependencies {
if *dep_app != *app_label
|| !squashed_set.contains(&(dep_app.as_str(), dep_name.as_str()))
{
if seen_deps.insert((dep_app.as_str(), dep_name.as_str())) {
squashed
.dependencies
.push((dep_app.clone(), dep_name.clone()));
}
}
}
}
Ok(squashed)
}
pub fn optimize_operations(&self, operations: Vec<Operation>) -> Vec<Operation> {
let mut optimized = Vec::new();
let mut created_tables = HashSet::new();
let mut dropped_tables = HashSet::new();
for operation in operations {
let should_push = match &operation {
Operation::CreateTable { name, .. } => {
dropped_tables.remove(name);
created_tables.insert(name.clone());
true
}
Operation::DropTable { name } => {
if created_tables.contains(name) {
optimized.retain(
|op| !matches!(op, Operation::CreateTable { name: table_name, .. } if table_name == name),
);
created_tables.remove(name);
false
} else {
dropped_tables.insert(name.clone());
true
}
}
Operation::AddColumn { table, .. } => {
!dropped_tables.contains(table)
}
Operation::DropColumn { table, column } => {
let had_add = optimized.iter().any(|op| {
matches!(op, Operation::AddColumn { table: t, column: c, .. } if t == table && c.name == *column)
});
if had_add {
optimized.retain(|op| {
!matches!(op, Operation::AddColumn { table: t, column: c, .. } if t == table && c.name == *column)
});
false
} else {
!dropped_tables.contains(table)
}
}
Operation::AlterColumn { table, .. } => {
!dropped_tables.contains(table)
}
Operation::RenameTable { old_name, .. } => {
!dropped_tables.contains(old_name)
}
Operation::RenameColumn { table, .. } => {
!dropped_tables.contains(table)
}
_ => true,
};
if should_push {
optimized.push(operation);
}
}
optimized
}
}
impl Default for MigrationSquasher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrations::{ColumnDefinition, FieldType};
#[test]
fn test_squash_basic() {
let migration1 = Migration::new("0001_initial", "myapp");
let migration2 =
Migration::new("0002_add_field", "myapp").add_dependency("myapp", "0001_initial");
let migrations = vec![migration1, migration2];
let squasher = MigrationSquasher::new();
let squashed = squasher
.squash(&migrations, "0001_squashed_0002", SquashOptions::default())
.unwrap();
assert_eq!(squashed.name, "0001_squashed_0002");
assert_eq!(squashed.app_label, "myapp");
assert_eq!(squashed.replaces.len(), 2);
}
#[test]
fn test_squash_empty_migrations() {
let squasher = MigrationSquasher::new();
let result = squasher.squash(&[], "squashed", SquashOptions::default());
assert!(result.is_err());
}
#[test]
fn test_squash_different_apps() {
let migration1 = Migration::new("0001_initial", "app1");
let migration2 = Migration::new("0002_add_field", "app2");
let migrations = vec![migration1, migration2];
let squasher = MigrationSquasher::new();
let result = squasher.squash(&migrations, "squashed", SquashOptions::default());
assert!(result.is_err());
}
#[test]
fn test_optimize_create_drop_table() {
let squasher = MigrationSquasher::new();
let ops = vec![
Operation::CreateTable {
name: "temp".to_string(),
columns: vec![ColumnDefinition::new("id", FieldType::Integer)],
constraints: vec![],
without_rowid: None,
partition: None,
interleave_in_parent: None,
},
Operation::DropTable {
name: "temp".to_string(),
},
];
let optimized = squasher.optimize_operations(ops);
assert_eq!(optimized.len(), 0);
}
#[test]
fn test_optimize_add_drop_column() {
let squasher = MigrationSquasher::new();
let ops = vec![
Operation::AddColumn {
table: "users".to_string(),
column: ColumnDefinition::new("temp_field", FieldType::VarChar(100)),
mysql_options: None,
},
Operation::DropColumn {
table: "users".to_string(),
column: "temp_field".to_string(),
},
];
let optimized = squasher.optimize_operations(ops);
assert_eq!(optimized.len(), 0);
}
#[test]
fn test_optimize_no_optimization() {
let squasher = MigrationSquasher::new();
let ops = vec![
Operation::CreateTable {
name: "users".to_string(),
columns: vec![ColumnDefinition::new("id", FieldType::Integer)],
constraints: vec![],
without_rowid: None,
partition: None,
interleave_in_parent: None,
},
Operation::AddColumn {
table: "users".to_string(),
column: ColumnDefinition::new("name", FieldType::VarChar(100)),
mysql_options: None,
},
];
let optimized = squasher.optimize_operations(ops.clone());
assert_eq!(optimized.len(), ops.len());
}
#[test]
fn test_squash_with_operations() {
let migration1 =
Migration::new("0001_initial", "myapp").add_operation(Operation::CreateTable {
name: "users".to_string(),
columns: vec![ColumnDefinition::new(
"id",
FieldType::Custom("INTEGER PRIMARY KEY".to_string()),
)],
constraints: vec![],
without_rowid: None,
partition: None,
interleave_in_parent: None,
});
let migration2 = Migration::new("0002_add_field", "myapp")
.add_dependency("myapp", "0001_initial")
.add_operation(Operation::AddColumn {
table: "users".to_string(),
column: ColumnDefinition::new("name", FieldType::VarChar(100)),
mysql_options: None,
});
let migrations = vec![migration1, migration2];
let squasher = MigrationSquasher::new();
let squashed = squasher
.squash(&migrations, "0001_squashed_0002", SquashOptions::default())
.unwrap();
assert_eq!(squashed.operations.len(), 2);
}
#[test]
fn test_squash_external_dependencies() {
let migration1 =
Migration::new("0001_initial", "myapp").add_dependency("other_app", "0001_initial");
let migration2 =
Migration::new("0002_add_field", "myapp").add_dependency("myapp", "0001_initial");
let migrations = vec![migration1, migration2];
let squasher = MigrationSquasher::new();
let squashed = squasher
.squash(&migrations, "0001_squashed_0002", SquashOptions::default())
.unwrap();
assert_eq!(squashed.dependencies.len(), 1);
assert_eq!(squashed.dependencies[0].0, "other_app");
}
}