diff --git a/cot-cli/src/migration_generator.rs b/cot-cli/src/migration_generator.rs index 4fc8a6a33..1401da304 100644 --- a/cot-cli/src/migration_generator.rs +++ b/cot-cli/src/migration_generator.rs @@ -816,9 +816,9 @@ impl MigrationOperationGenerator { #[must_use] fn make_alter_field_operation( - _app_model: &ModelInSource, + app_model: &ModelInSource, app_field: &Field, - migration_model: &ModelInSource, + _migration_model: &ModelInSource, migration_field: &Field, ) -> Option { if app_field == migration_field { @@ -828,20 +828,26 @@ impl MigrationOperationGenerator { StatusType::Modifying, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + &migration_field.name, app_model.model.name ), ); - todo!(); + let op = DynOperation::AlterField { + table_name: app_model.model.table_name.clone(), + model_ty: app_model.model.resolved_ty.clone(), + old_field: Box::new(migration_field.clone()), + new_field: Box::new(app_field.clone()), + }; - #[expect(unreachable_code)] print_status_msg( StatusType::Modified, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + &migration_field.name, app_model.model.name ), ); + + Some(op) } #[must_use] @@ -1130,23 +1136,8 @@ impl GeneratedMigration { } => { let to_type = match to { DynOperation::CreateModel { model_ty, .. } => model_ty, - DynOperation::AddField { .. } => { - unreachable!( - "AddField operation shouldn't be a dependency of CreateModel \ - because it doesn't create a new model" - ) - } - DynOperation::RemoveField { .. } => { - unreachable!( - "RemoveField operation shouldn't be a dependency of CreateModel \ - because it doesn't create a new model" - ) - } - DynOperation::RemoveModel { .. } => { - unreachable!( - "RemoveModel operation shouldn't be a dependency of CreateModel \ - because it doesn't create a new model" - ) + _ => { + unreachable!("Only CreateModel can be a dependency target for CreateModel") } }; trace!( @@ -1171,18 +1162,10 @@ impl GeneratedMigration { result } - DynOperation::AddField { .. } => { - // AddField only links two already existing models together, so - // removing it shouldn't ever affect whether a graph is cyclic - unreachable!("AddField operation should never create cycles") - } - DynOperation::RemoveField { .. } => { - // RemoveField doesn't create dependencies, it only removes a field - unreachable!("RemoveField operation should never create cycles") - } - DynOperation::RemoveModel { .. } => { - // RemoveModel doesn't create dependencies, it only removes a model - unreachable!("RemoveModel operation should never create cycles") + _ => { + // Only CreateModel can create dependency cycles; all other ops + // change existing schema without introducing new FK dependencies. + unreachable!("Only CreateModel operation can create cycles") } } } @@ -1282,6 +1265,18 @@ impl GeneratedMigration { // RemoveField Doesnt Add Foreign Keys Vec::new() } + DynOperation::AlterField { + new_field, + model_ty, + .. + } => { + let mut ops = vec![(i, model_ty.clone())]; + // Only depend on the new foreign key, not the old one + if let Some(to_type) = foreign_key_for_field(new_field) { + ops.push((i, to_type)); + } + ops + } DynOperation::RemoveModel { .. } => { // RemoveModel Doesnt Add Foreign Keys Vec::new() @@ -1414,6 +1409,7 @@ impl Repr for DynDependency { /// runtime and is using codegen types. /// /// This is used to generate migration files. +#[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum DynOperation { CreateModel { @@ -1438,6 +1434,12 @@ pub enum DynOperation { model_ty: syn::Type, fields: Vec, }, + AlterField { + table_name: String, + model_ty: syn::Type, + old_field: Box, + new_field: Box, + }, } /// Returns whether given [`Field`] is a foreign key to given type. @@ -1492,6 +1494,22 @@ impl Repr for DynOperation { .build() } } + Self::AlterField { + table_name, + old_field, + new_field, + .. + } => { + let old_field = old_field.repr(); + let new_field = new_field.repr(); + quote! { + ::cot::db::migrations::Operation::alter_field() + .table_name(::cot::db::Identifier::new(#table_name)) + .old_field(#old_field) + .new_field(#new_field) + .build() + } + } Self::RemoveModel { table_name, fields, .. } => { @@ -1855,6 +1873,96 @@ mod tests { })); } + #[test] + fn get_foreign_key_dependencies_with_alter_field() { + // AlterField where the new field gains a foreign key should produce a + // dependency on the referenced model. The old field's foreign key + // (if any) is intentionally not tracked here. A `CreateModel` for the + // altered table is included so the altered model is not flagged as an + // external dependency by `get_foreign_key_dependencies`. + let operations = vec![ + DynOperation::CreateModel { + table_name: "table1".to_string(), + model_ty: parse_quote!(Table1), + fields: vec![], + }, + DynOperation::AlterField { + table_name: "table1".to_string(), + model_ty: parse_quote!(Table1), + old_field: Box::new(Field { + name: format_ident!("field1"), + column_name: "field1".to_string(), + ty: parse_quote!(i32), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: None, + }), + new_field: Box::new(Field { + name: format_ident!("field1"), + column_name: "field1".to_string(), + ty: parse_quote!(ForeignKey), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: Some(ForeignKeySpec { + to_model: parse_quote!(crate::Table2), + }), + }), + }, + ]; + + let external_dependencies = GeneratedMigration::get_foreign_key_dependencies(&operations); + assert_eq!(external_dependencies.len(), 1); + assert_eq!( + external_dependencies[0], + DynDependency::Model { + model_type: parse_quote!(crate::Table2), + } + ); + } + + #[test] + fn get_foreign_key_dependencies_alter_field_no_new_foreign_key() { + // If the new field has no foreign key, AlterField should produce no + // external dependencies (when the altered table itself is created in + // the same migration), even when the old field had one. + let operations = vec![ + DynOperation::CreateModel { + table_name: "table1".to_string(), + model_ty: parse_quote!(Table1), + fields: vec![], + }, + DynOperation::AlterField { + table_name: "table1".to_string(), + model_ty: parse_quote!(Table1), + old_field: Box::new(Field { + name: format_ident!("field1"), + column_name: "field1".to_string(), + ty: parse_quote!(ForeignKey), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: Some(ForeignKeySpec { + to_model: parse_quote!(crate::Table2), + }), + }), + new_field: Box::new(Field { + name: format_ident!("field1"), + column_name: "field1".to_string(), + ty: parse_quote!(i32), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: None, + }), + }, + ]; + + let external_dependencies = GeneratedMigration::get_foreign_key_dependencies(&operations); + assert!(external_dependencies.is_empty()); + } + fn get_test_model() -> ModelInSource { ModelInSource { model_item: parse_quote! { @@ -2210,4 +2318,241 @@ mod tests { panic!("Expected a function item"); } } + + #[test] + fn make_alter_field_operation() { + let migration_model = get_test_model(); + let mut app_model = migration_model.clone(); + + app_model.model.fields[0].ty = parse_quote!(i32); + + let migration_field = &migration_model.model.fields[0]; + let app_field = &app_model.model.fields[0]; + + let operation = MigrationOperationGenerator::make_alter_field_operation( + &app_model, + app_field, + &migration_model, + migration_field, + ); + + match &operation { + Some(DynOperation::AlterField { + table_name, + model_ty, + old_field, + new_field, + }) => { + assert_eq!(table_name, "test_model"); + assert_eq!(model_ty, &parse_quote!(TestModel)); + assert_eq!(old_field.column_name, "field1"); + assert_eq!(old_field.ty, parse_quote!(String)); + assert_eq!(new_field.column_name, "field1"); + assert_eq!(new_field.ty, parse_quote!(i32)); + } + _ => panic!("Expected Some(DynOperation::AlterField)"), + } + } + + #[test] + fn generate_operations_with_altered_field() { + let migration_model = get_test_model(); + let mut app_model = migration_model.clone(); + + app_model.model.fields[0].ty = parse_quote!(i32); + + let app_models = vec![app_model.clone()]; + let migration_models = vec![migration_model.clone()]; + + let (modified_models, operations) = + MigrationGenerator::generate_operations(&app_models, &migration_models); + + assert_eq!(modified_models.len(), 1); + assert!( + operations.iter().any(|op| match op { + DynOperation::AlterField { + old_field, + new_field, + .. + } => old_field.ty == parse_quote!(String) && new_field.ty == parse_quote!(i32), + _ => false, + }), + "Expected an AlterField operation for changed type" + ); + } + + #[test] + fn repr_for_alter_field_operation() { + let op = DynOperation::AlterField { + table_name: "test_table".to_string(), + model_ty: parse_quote!(TestModel), + old_field: Box::new(Field { + name: format_ident!("test_field"), + column_name: "test_field".to_string(), + ty: parse_quote!(String), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: None, + }), + new_field: Box::new(Field { + name: format_ident!("test_field"), + column_name: "test_field".to_string(), + ty: parse_quote!(i32), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: None, + }), + }; + + let tokens = op.repr(); + let tokens_str = tokens.to_string(); + + assert!( + tokens_str.contains("alter_field"), + "Should call alter_field() but got: {tokens_str}" + ); + assert!( + tokens_str.contains("table_name"), + "Should call table_name() but got: {tokens_str}" + ); + assert!( + tokens_str.contains("old_field"), + "Should call old_field() but got: {tokens_str}" + ); + assert!( + tokens_str.contains("new_field"), + "Should call new_field() but got: {tokens_str}" + ); + assert!( + tokens_str.contains("build"), + "Should call build() but got: {tokens_str}" + ); + } + + #[test] + fn make_alter_field_operation_type_change() { + let migration_model = get_test_model(); + let mut app_model = migration_model.clone(); + + app_model.model.fields[0].ty = parse_quote!(i32); + + let migration_field = &migration_model.model.fields[0]; + let app_field = &app_model.model.fields[0]; + + let alter_op = MigrationOperationGenerator::make_alter_field_operation( + &app_model, + app_field, + &migration_model, + migration_field, + ); + + match alter_op { + Some(DynOperation::AlterField { + table_name, + model_ty, + old_field, + new_field, + }) => { + assert_eq!(table_name, "test_model"); + assert_eq!(model_ty, parse_quote!(TestModel)); + // The old field type should be String + assert_eq!(old_field.ty, parse_quote!(String)); + // The new field type should be i32 + assert_eq!(new_field.ty, parse_quote!(i32)); + assert_eq!(old_field.column_name, new_field.column_name); + } + _ => panic!("Expected DynOperation::AlterField for type change"), + } + } + + #[test] + fn make_alter_field_operation_nullable_change() { + let migration_model = get_test_model(); + let mut app_model = migration_model.clone(); + + app_model.model.fields[0].ty = parse_quote!(Option); + + let migration_field = &migration_model.model.fields[0]; + let app_field = &app_model.model.fields[0]; + + let alter_op = MigrationOperationGenerator::make_alter_field_operation( + &app_model, + app_field, + &migration_model, + migration_field, + ); + + match alter_op { + Some(DynOperation::AlterField { + table_name, + model_ty, + old_field, + new_field, + }) => { + assert_eq!(table_name, "test_model"); + assert_eq!(model_ty, parse_quote!(TestModel)); + // Old field type is String, new is Option + assert_eq!(old_field.ty, parse_quote!(String)); + assert_eq!(new_field.ty, parse_quote!(Option)); + assert_eq!(old_field.column_name, new_field.column_name); + } + _ => panic!("Expected DynOperation::AlterField for nullability change"), + } + } + + #[test] + fn make_alter_field_operation_primary_key_change() { + let migration_model = get_test_model(); + let mut app_model = migration_model.clone(); + + app_model.model.fields[0].primary_key = true; + + let migration_field = &migration_model.model.fields[0]; + let app_field = &app_model.model.fields[0]; + + let alter_op = MigrationOperationGenerator::make_alter_field_operation( + &app_model, + app_field, + &migration_model, + migration_field, + ); + + match alter_op { + Some(DynOperation::AlterField { + table_name, + model_ty, + old_field, + new_field, + }) => { + assert_eq!(table_name, "test_model"); + assert_eq!(model_ty, parse_quote!(TestModel)); + assert_ne!(old_field.primary_key, new_field.primary_key); + assert!(new_field.primary_key); + } + _ => panic!("Expected DynOperation::AlterField for primary_key change"), + } + } + + #[test] + fn make_alter_field_operation_no_change_returns_none() { + let migration_model = get_test_model(); + let app_model = migration_model.clone(); + + let migration_field = &migration_model.model.fields[0]; + let app_field = &app_model.model.fields[0]; + + let alter_op = MigrationOperationGenerator::make_alter_field_operation( + &app_model, + app_field, + &migration_model, + migration_field, + ); + + assert!( + alter_op.is_none(), + "No operation should be produced for identical fields" + ); + } } diff --git a/cot/src/db/migrations.rs b/cot/src/db/migrations.rs index 51dc19761..dbaefe72c 100644 --- a/cot/src/db/migrations.rs +++ b/cot/src/db/migrations.rs @@ -430,6 +430,60 @@ impl Operation { RemoveModelBuilder::new() } + /// Returns a builder for an operation that alters an existing field on a + /// model. + /// + /// Typically, you shouldn't need to use this directly. Instead, in most + /// cases, this can be automatically generated by the Cot CLI. + /// + /// # Examples + /// + /// Note: `AlterField` is currently only supported on PostgreSQL and + /// MySQL backends; SQLite's `ALTER TABLE` does not support modifying + /// existing columns. + /// + /// ```no_run + /// use cot::db::migrations::{Field, Operation}; + /// use cot::db::{DatabaseField, Identifier}; + /// + /// # #[tokio::main] + /// # async fn main() -> cot::Result<()> { + /// const FIELDS: &[Field] = &[ + /// Field::new(Identifier::new("id"), ::TYPE) + /// .primary_key() + /// .auto(), + /// Field::new(Identifier::new("name"), ::TYPE), + /// ]; + /// + /// // First create the table + /// const CREATE_OPERATION: Operation = Operation::create_model() + /// .table_name(Identifier::new("todoapp__my_model")) + /// .fields(FIELDS) + /// .build(); + /// let database = cot::db::Database::new("postgres://localhost/example").await?; + /// CREATE_OPERATION.forwards(&database).await?; + /// + /// // Then alter the `name` field to allow null values + /// const OPERATION: Operation = Operation::alter_field() + /// .table_name(Identifier::new("todoapp__my_model")) + /// .old_field(Field::new( + /// Identifier::new("name"), + /// ::TYPE, + /// )) + /// .new_field( + /// Field::new(Identifier::new("name"), ::TYPE).null(), + /// ) + /// .build(); + /// + /// OPERATION.forwards(&database).await?; + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub const fn alter_field() -> AlterFieldBuilder { + AlterFieldBuilder::new() + } + /// Returns a builder for a custom operation. /// /// # Examples @@ -521,6 +575,23 @@ impl Operation { .to_owned(); database.execute_schema(query).await?; } + OperationInner::AlterField { + table_name, + old_field, + new_field, + } => { + if old_field.foreign_key.is_some() || new_field.foreign_key.is_some() { + unimplemented!( + "AlterField with foreign key constraints is not yet supported; \ + use separate RemoveField / AddField operations instead" + ); + } + let query = sea_query::Table::alter() + .table(*table_name) + .modify_column(new_field.as_column_def(database)) + .to_owned(); + database.execute_schema(query).await?; + } OperationInner::RemoveModel { table_name, fields: _, @@ -594,6 +665,24 @@ impl Operation { .to_owned(); database.execute_schema(query).await?; } + OperationInner::AlterField { + table_name, + old_field, + new_field, + } => { + if old_field.foreign_key.is_some() || new_field.foreign_key.is_some() { + unimplemented!( + "AlterField with foreign key constraints is not yet supported; \ + use separate RemoveField / AddField operations instead" + ); + } + // To reverse an alteration, set the column back to the old definition + let query = sea_query::Table::alter() + .table(*table_name) + .modify_column(old_field.as_column_def(database)) + .to_owned(); + database.execute_schema(query).await?; + } OperationInner::RemoveModel { table_name, fields } => { let mut query = sea_query::Table::create().table(*table_name).to_owned(); for field in *fields { @@ -679,6 +768,11 @@ enum OperationInner { table_name: Identifier, fields: &'static [Field], }, + AlterField { + table_name: Identifier, + old_field: Field, + new_field: Field, + }, Custom { forwards: CustomOperationFn, backwards: Option, @@ -1667,6 +1761,172 @@ impl CustomBuilder { } } +/// A builder for altering an existing field on a model. +/// +/// Typically, you shouldn't need to use this directly. Instead, in most +/// cases, this can be automatically generated by the Cot CLI. +/// +/// Note: `AlterField` is currently only supported on PostgreSQL and MySQL +/// backends; SQLite's `ALTER TABLE` does not support modifying existing +/// columns. +/// +/// # Examples +/// +/// ```no_run +/// use cot::db::migrations::{Field, Operation}; +/// use cot::db::{DatabaseField, Identifier}; +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// # const CREATE_MODEL_OPERATION: Operation = Operation::create_model() +/// # .table_name(Identifier::new("todoapp__my_model")) +/// # .fields(&[ +/// # Field::new(Identifier::new("id"), ::TYPE) +/// # .primary_key() +/// # .auto(), +/// # Field::new(Identifier::new("name"), ::TYPE), +/// # ]) +/// # .build(); +/// const OPERATION: Operation = Operation::alter_field() +/// .table_name(Identifier::new("todoapp__my_model")) +/// .old_field(Field::new( +/// Identifier::new("name"), +/// ::TYPE, +/// )) +/// .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) +/// .build(); +/// +/// # let database = cot::db::Database::new("postgres://localhost/example").await?; +/// # CREATE_MODEL_OPERATION.forwards(&database).await?; +/// # OPERATION.forwards(&database).await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct AlterFieldBuilder { + table_name: Option, + old_field: Option, + new_field: Option, +} + +impl Default for AlterFieldBuilder { + fn default() -> Self { + Self::new() + } +} + +impl AlterFieldBuilder { + #[must_use] + const fn new() -> Self { + Self { + table_name: None, + old_field: None, + new_field: None, + } + } + + /// Sets the name of the table to alter the field in. + /// + /// # Cot CLI Usage + /// + /// Typically, you shouldn't need to use this directly. Instead, in most + /// cases, this can be automatically generated by the Cot CLI. + /// + /// # Examples + /// + /// ``` + /// use cot::db::migrations::{Field, Operation}; + /// use cot::db::{DatabaseField, Identifier}; + /// + /// const OPERATION: Operation = Operation::alter_field() + /// .table_name(Identifier::new("todoapp__my_model")) + /// .old_field(Field::new( + /// Identifier::new("name"), + /// ::TYPE, + /// )) + /// .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + /// .build(); + /// ``` + #[must_use] + pub const fn table_name(mut self, table_name: Identifier) -> Self { + self.table_name = Some(table_name); + self + } + + /// Sets the previous definition of the field that is being altered. + /// + /// This is used to roll the migration back via [`Operation::backwards`]. + /// + /// # Cot CLI Usage + /// + /// Typically, you shouldn't need to use this directly. Instead, in most + /// cases, this can be automatically generated by the Cot CLI. + /// + /// # Examples + /// + /// ``` + /// use cot::db::migrations::{Field, Operation}; + /// use cot::db::{DatabaseField, Identifier}; + /// + /// const OPERATION: Operation = Operation::alter_field() + /// .table_name(Identifier::new("todoapp__my_model")) + /// .old_field(Field::new( + /// Identifier::new("name"), + /// ::TYPE, + /// )) + /// .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + /// .build(); + /// ``` + #[must_use] + pub const fn old_field(mut self, field: Field) -> Self { + self.old_field = Some(field); + self + } + + /// Sets the new definition of the field that is being altered. + /// + /// # Cot CLI Usage + /// + /// Typically, you shouldn't need to use this directly. Instead, in most + /// cases, this can be automatically generated by the Cot CLI. + /// + /// # Examples + /// + /// ``` + /// use cot::db::migrations::{Field, Operation}; + /// use cot::db::{DatabaseField, Identifier}; + /// + /// const OPERATION: Operation = Operation::alter_field() + /// .table_name(Identifier::new("todoapp__my_model")) + /// .old_field(Field::new( + /// Identifier::new("name"), + /// ::TYPE, + /// )) + /// .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + /// .build(); + /// ``` + #[must_use] + pub const fn new_field(mut self, field: Field) -> Self { + self.new_field = Some(field); + self + } + + /// Builds the [`Operation`]. + /// + /// # Panics + /// + /// Panics if [`Self::table_name`], [`Self::old_field`] or + /// [`Self::new_field`] have not been called. + #[must_use] + pub const fn build(self) -> Operation { + Operation::new(OperationInner::AlterField { + table_name: unwrap_builder_option!(self, table_name), + old_field: unwrap_builder_option!(self, old_field), + new_field: unwrap_builder_option!(self, new_field), + }) + } +} + /// A trait for defining a migration. /// /// # Cot CLI Usage @@ -2403,6 +2663,196 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_operation_alter_field() { + let operation = Operation::alter_field() + .table_name(Identifier::new("testapp__test_model")) + .old_field(Field::new( + Identifier::new("name"), + ::TYPE, + )) + .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + .build(); + + if let OperationInner::AlterField { + table_name, + old_field, + new_field, + } = operation.inner + { + assert_eq!(table_name.to_string(), "testapp__test_model"); + assert_eq!(old_field.name.to_string(), "name"); + assert_eq!(old_field.ty, ColumnType::Text); + assert_eq!(new_field.name.to_string(), "name"); + assert!(new_field.null); + } else { + panic!("Expected OperationInner::AlterField"); + } + } + + // Note: AlterField is only tested against PostgreSQL and MySQL because + // SQLite's `sea_query` backend does not support `ALTER TABLE ... MODIFY + // COLUMN` (it panics). These tests follow the pattern used by the + // ignored `*_postgres` / `*_mysql` tests generated by + // `cot_macros::dbtest`. + + async fn run_alter_field_forwards(test_db: &mut TestDatabase) { + const FIELDS: &[Field] = &[ + Field::new(Identifier::new("id"), ::TYPE) + .primary_key() + .auto(), + Field::new(Identifier::new("name"), ::TYPE), + ]; + + let create_operation = Operation::create_model() + .table_name(Identifier::new("testapp__test_model")) + .fields(FIELDS) + .build(); + + create_operation + .forwards(&test_db.database()) + .await + .unwrap(); + + let alter_operation = Operation::alter_field() + .table_name(Identifier::new("testapp__test_model")) + .old_field(Field::new( + Identifier::new("name"), + ::TYPE, + )) + .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + .build(); + + let result = alter_operation.forwards(&test_db.database()).await; + assert!(result.is_ok()); + } + + async fn run_alter_field_backwards(test_db: &mut TestDatabase) { + const FIELDS: &[Field] = &[ + Field::new(Identifier::new("id"), ::TYPE) + .primary_key() + .auto(), + Field::new(Identifier::new("name"), ::TYPE), + ]; + + let create_operation = Operation::create_model() + .table_name(Identifier::new("testapp__test_model")) + .fields(FIELDS) + .build(); + + create_operation + .forwards(&test_db.database()) + .await + .unwrap(); + + let alter_operation = Operation::alter_field() + .table_name(Identifier::new("testapp__test_model")) + .old_field(Field::new( + Identifier::new("name"), + ::TYPE, + )) + .new_field(Field::new(Identifier::new("name"), ::TYPE).null()) + .build(); + + alter_operation.forwards(&test_db.database()).await.unwrap(); + + let result = alter_operation.backwards(&test_db.database()).await; + assert!(result.is_ok()); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn test_alter_field_operation_forwards_postgres() { + let mut database = + TestDatabase::new_postgres(stringify!(test_alter_field_operation_forwards)) + .await + .expect("failed to create PostgreSQL test database"); + run_alter_field_forwards(&mut database).await; + database + .cleanup() + .await + .expect("failed to clean up PostgreSQL test database"); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn test_alter_field_operation_forwards_mysql() { + let mut database = TestDatabase::new_mysql(stringify!(test_alter_field_operation_forwards)) + .await + .expect("failed to create MySQL test database"); + run_alter_field_forwards(&mut database).await; + database + .cleanup() + .await + .expect("failed to clean up MySQL test database"); + } + + #[ignore = "Tests that use PostgreSQL are ignored by default"] + #[cot::test] + async fn test_alter_field_operation_backwards_postgres() { + let mut database = + TestDatabase::new_postgres(stringify!(test_alter_field_operation_backwards)) + .await + .expect("failed to create PostgreSQL test database"); + run_alter_field_backwards(&mut database).await; + database + .cleanup() + .await + .expect("failed to clean up PostgreSQL test database"); + } + + #[ignore = "Tests that use MySQL are ignored by default"] + #[cot::test] + async fn test_alter_field_operation_backwards_mysql() { + let mut database = + TestDatabase::new_mysql(stringify!(test_alter_field_operation_backwards)) + .await + .expect("failed to create MySQL test database"); + run_alter_field_backwards(&mut database).await; + database + .cleanup() + .await + .expect("failed to clean up MySQL test database"); + } + + #[tokio::test] + #[should_panic(expected = "AlterField with foreign key constraints is not yet supported")] + async fn test_alter_field_new_fk_forwards_panics() { + let database = Database::new("sqlite::memory:").await.unwrap(); + let operation = Operation::alter_field() + .table_name(Identifier::new("testapp__test_model")) + .old_field(Field::new(Identifier::new("col"), ColumnType::Integer)) + .new_field( + Field::new(Identifier::new("col"), ColumnType::Integer).foreign_key( + Identifier::new("other_table"), + Identifier::new("id"), + ForeignKeyOnDeletePolicy::Cascade, + ForeignKeyOnUpdatePolicy::Cascade, + ), + ) + .build(); + operation.forwards(&database).await.unwrap(); + } + + #[tokio::test] + #[should_panic(expected = "AlterField with foreign key constraints is not yet supported")] + async fn test_alter_field_old_fk_backwards_panics() { + let database = Database::new("sqlite::memory:").await.unwrap(); + let operation = Operation::alter_field() + .table_name(Identifier::new("testapp__test_model")) + .old_field( + Field::new(Identifier::new("col"), ColumnType::Integer).foreign_key( + Identifier::new("other_table"), + Identifier::new("id"), + ForeignKeyOnDeletePolicy::Cascade, + ForeignKeyOnUpdatePolicy::Cascade, + ), + ) + .new_field(Field::new(Identifier::new("col"), ColumnType::Integer)) + .build(); + operation.backwards(&database).await.unwrap(); + } + #[test] fn test_remove_field_builder_new() { let builder = RemoveFieldBuilder::new();