Skip to content

Commit de8cb85

Browse files
committed
feat: Add AlterField Operation to Migration Generator
1 parent 08eccfb commit de8cb85

2 files changed

Lines changed: 248 additions & 32 deletions

File tree

cot-cli/src/migration_generator.rs

Lines changed: 176 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -716,9 +716,9 @@ impl MigrationOperationGenerator {
716716

717717
#[must_use]
718718
fn make_alter_field_operation(
719-
_app_model: &ModelInSource,
719+
app_model: &ModelInSource,
720720
app_field: &Field,
721-
migration_model: &ModelInSource,
721+
_migration_model: &ModelInSource,
722722
migration_field: &Field,
723723
) -> Option<DynOperation> {
724724
if app_field == migration_field {
@@ -728,20 +728,15 @@ impl MigrationOperationGenerator {
728728
StatusType::Modifying,
729729
&format!(
730730
"Field '{}' from Model '{}'",
731-
&migration_field.name, migration_model.model.name
732-
),
733-
);
734-
735-
todo!();
736-
737-
#[expect(unreachable_code)]
738-
print_status_msg(
739-
StatusType::Modified,
740-
&format!(
741-
"Field '{}' from Model '{}'",
742-
&migration_field.name, migration_model.model.name
731+
&migration_field.name, app_model.model.name
743732
),
744733
);
734+
Some(DynOperation::AlterField {
735+
table_name: app_model.model.table_name.clone(),
736+
model_ty: app_model.model.resolved_ty.clone(),
737+
old_field: Box::new(migration_field.clone()),
738+
new_field: Box::new(app_field.clone()),
739+
})
745740
}
746741

747742
#[must_use]
@@ -1009,24 +1004,22 @@ impl GeneratedMigration {
10091004
} => {
10101005
let to_type = match to {
10111006
DynOperation::CreateModel { model_ty, .. } => model_ty,
1012-
DynOperation::AddField { .. } => {
1013-
unreachable!(
1014-
"AddField operation shouldn't be a dependency of CreateModel \
1015-
because it doesn't create a new model"
1016-
)
1017-
}
1018-
DynOperation::RemoveField { .. } => {
1019-
unreachable!(
1020-
"RemoveField operation shouldn't be a dependency of CreateModel \
1021-
because it doesn't create a new model"
1022-
)
1023-
}
1024-
DynOperation::RemoveModel { .. } => {
1025-
unreachable!(
1026-
"RemoveModel operation shouldn't be a dependency of CreateModel \
1027-
because it doesn't create a new model"
1028-
)
1029-
}
1007+
DynOperation::AddField { .. } => unreachable!(
1008+
"AddField operation shouldn't be a dependency of CreateModel \
1009+
because it doesn't create a new model"
1010+
),
1011+
DynOperation::RemoveField { .. } => unreachable!(
1012+
"RemoveField operation shouldn't be a dependency of CreateModel \
1013+
because it doesn't create a new model"
1014+
),
1015+
DynOperation::RemoveModel { .. } => unreachable!(
1016+
"RemoveModel operation shouldn't be a dependency of CreateModel \
1017+
because it doesn't create a new model"
1018+
),
1019+
DynOperation::AlterField { .. } => unreachable!(
1020+
"AlterField operation shouldn't be a dependency of CreateModel \
1021+
because it doesn't create a new model"
1022+
),
10301023
};
10311024
trace!(
10321025
"Removing foreign keys from {} to {}",
@@ -1063,6 +1056,11 @@ impl GeneratedMigration {
10631056
// RemoveModel doesn't create dependencies, it only removes a model
10641057
unreachable!("RemoveModel operation should never create cycles")
10651058
}
1059+
DynOperation::AlterField { .. } => {
1060+
// AlterField only changes metadata of an existing field,
1061+
// so it does not create dependency cycles.
1062+
unreachable!("AlterField operation should never create cycles")
1063+
}
10661064
}
10671065
}
10681066

@@ -1161,6 +1159,18 @@ impl GeneratedMigration {
11611159
// RemoveField Doesnt Add Foreign Keys
11621160
Vec::new()
11631161
}
1162+
DynOperation::AlterField {
1163+
new_field,
1164+
model_ty,
1165+
..
1166+
} => {
1167+
let mut ops = vec![(i, model_ty.clone())];
1168+
// Only depend on the new foreign key, not the old one
1169+
if let Some(to_type) = foreign_key_for_field(new_field) {
1170+
ops.push((i, to_type));
1171+
}
1172+
ops
1173+
}
11641174
DynOperation::RemoveModel { .. } => {
11651175
// RemoveModel Doesnt Add Foreign Keys
11661176
Vec::new()
@@ -1317,6 +1327,12 @@ pub enum DynOperation {
13171327
model_ty: syn::Type,
13181328
fields: Vec<Field>,
13191329
},
1330+
AlterField {
1331+
table_name: String,
1332+
model_ty: syn::Type,
1333+
old_field: Box<Field>,
1334+
new_field: Box<Field>,
1335+
},
13201336
}
13211337

13221338
/// Returns whether given [`Field`] is a foreign key to given type.
@@ -1371,6 +1387,22 @@ impl Repr for DynOperation {
13711387
.build()
13721388
}
13731389
}
1390+
Self::AlterField {
1391+
table_name,
1392+
old_field,
1393+
new_field,
1394+
..
1395+
} => {
1396+
let old_field = old_field.repr();
1397+
let new_field = new_field.repr();
1398+
quote! {
1399+
::cot::db::migrations::Operation::alter_field()
1400+
.table_name(::cot::db::Identifier::new(#table_name))
1401+
.old_field(#old_field)
1402+
.new_field(#new_field)
1403+
.build()
1404+
}
1405+
}
13741406
Self::RemoveModel {
13751407
table_name, fields, ..
13761408
} => {
@@ -2053,4 +2085,116 @@ mod tests {
20532085
panic!("Expected a function item");
20542086
}
20552087
}
2088+
2089+
#[test]
2090+
fn make_alter_field_operation() {
2091+
let migration_model = get_test_model();
2092+
let mut app_model = migration_model.clone();
2093+
2094+
app_model.model.fields[0].ty = parse_quote!(i32);
2095+
2096+
let migration_field = &migration_model.model.fields[0];
2097+
let app_field = &app_model.model.fields[0];
2098+
2099+
let operation = MigrationOperationGenerator::make_alter_field_operation(
2100+
&app_model,
2101+
app_field,
2102+
&migration_model,
2103+
migration_field,
2104+
);
2105+
2106+
match &operation {
2107+
Some(DynOperation::AlterField {
2108+
table_name,
2109+
model_ty,
2110+
old_field,
2111+
new_field,
2112+
}) => {
2113+
assert_eq!(table_name, "test_model");
2114+
assert_eq!(model_ty, &parse_quote!(TestModel));
2115+
assert_eq!(old_field.column_name, "field1");
2116+
assert_eq!(old_field.ty, parse_quote!(String));
2117+
assert_eq!(new_field.column_name, "field1");
2118+
assert_eq!(new_field.ty, parse_quote!(i32));
2119+
}
2120+
_ => panic!("Expected Some(DynOperation::AlterField)"),
2121+
}
2122+
}
2123+
2124+
#[test]
2125+
fn generate_operations_with_altered_field() {
2126+
let migration_model = get_test_model();
2127+
let mut app_model = migration_model.clone();
2128+
2129+
app_model.model.fields[0].ty = parse_quote!(i32);
2130+
2131+
let app_models = vec![app_model.clone()];
2132+
let migration_models = vec![migration_model.clone()];
2133+
2134+
let (modified_models, operations) =
2135+
MigrationGenerator::generate_operations(&app_models, &migration_models);
2136+
2137+
assert_eq!(modified_models.len(), 1);
2138+
assert!(
2139+
operations.iter().any(|op| match op {
2140+
DynOperation::AlterField {
2141+
old_field,
2142+
new_field,
2143+
..
2144+
} => old_field.ty == parse_quote!(String) && new_field.ty == parse_quote!(i32),
2145+
_ => false,
2146+
}),
2147+
"Expected an AlterField operation for changed type"
2148+
);
2149+
}
2150+
2151+
#[test]
2152+
fn repr_for_alter_field_operation() {
2153+
let op = DynOperation::AlterField {
2154+
table_name: "test_table".to_string(),
2155+
model_ty: parse_quote!(TestModel),
2156+
old_field: Box::new(Field {
2157+
name: format_ident!("test_field"),
2158+
column_name: "test_field".to_string(),
2159+
ty: parse_quote!(String),
2160+
auto_value: false,
2161+
primary_key: false,
2162+
unique: false,
2163+
foreign_key: None,
2164+
}),
2165+
new_field: Box::new(Field {
2166+
name: format_ident!("test_field"),
2167+
column_name: "test_field".to_string(),
2168+
ty: parse_quote!(i32),
2169+
auto_value: false,
2170+
primary_key: false,
2171+
unique: false,
2172+
foreign_key: None,
2173+
}),
2174+
};
2175+
2176+
let tokens = op.repr();
2177+
let tokens_str = tokens.to_string();
2178+
2179+
assert!(
2180+
tokens_str.contains("alter_field"),
2181+
"Should call alter_field() but got: {tokens_str}"
2182+
);
2183+
assert!(
2184+
tokens_str.contains("table_name"),
2185+
"Should call table_name() but got: {tokens_str}"
2186+
);
2187+
assert!(
2188+
tokens_str.contains("old_field"),
2189+
"Should call old_field() but got: {tokens_str}"
2190+
);
2191+
assert!(
2192+
tokens_str.contains("new_field"),
2193+
"Should call new_field() but got: {tokens_str}"
2194+
);
2195+
assert!(
2196+
tokens_str.contains("build"),
2197+
"Should call build() but got: {tokens_str}"
2198+
);
2199+
}
20562200
}

cot/src/db/migrations.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,17 @@ impl Operation {
494494
.to_owned();
495495
database.execute_schema(query).await?;
496496
}
497+
OperationInner::AlterField {
498+
table_name,
499+
old_field: _,
500+
new_field,
501+
} => {
502+
let query = sea_query::Table::alter()
503+
.table(*table_name)
504+
.modify_column(new_field.as_column_def(database))
505+
.to_owned();
506+
database.execute_schema(query).await?;
507+
}
497508
OperationInner::RemoveModel {
498509
table_name,
499510
fields: _,
@@ -560,6 +571,18 @@ impl Operation {
560571
.to_owned();
561572
database.execute_schema(query).await?;
562573
}
574+
OperationInner::AlterField {
575+
table_name,
576+
old_field,
577+
new_field: _,
578+
} => {
579+
// To reverse an alteration, set the column back to the old definition
580+
let query = sea_query::Table::alter()
581+
.table(*table_name)
582+
.modify_column(old_field.as_column_def(database))
583+
.to_owned();
584+
database.execute_schema(query).await?;
585+
}
563586
OperationInner::RemoveModel { table_name, fields } => {
564587
let mut query = sea_query::Table::create().table(*table_name).to_owned();
565588
for field in *fields {
@@ -606,6 +629,11 @@ enum OperationInner {
606629
table_name: Identifier,
607630
fields: &'static [Field],
608631
},
632+
AlterField {
633+
table_name: Identifier,
634+
old_field: Field,
635+
new_field: Field,
636+
},
609637
}
610638

611639
/// A field in a model.
@@ -1530,6 +1558,50 @@ impl RemoveModelBuilder {
15301558
}
15311559
}
15321560

1561+
pub const fn alter_field() -> AlterFieldBuilder {
1562+
AlterFieldBuilder::new()
1563+
}
1564+
1565+
#[derive(Debug, Copy, Clone)]
1566+
pub struct AlterFieldBuilder {
1567+
table_name: Option<Identifier>,
1568+
old_field: Option<Field>,
1569+
new_field: Option<Field>,
1570+
}
1571+
1572+
impl AlterFieldBuilder {
1573+
const fn new() -> Self {
1574+
Self {
1575+
table_name: None,
1576+
old_field: None,
1577+
new_field: None,
1578+
}
1579+
}
1580+
1581+
pub const fn table_name(mut self, table_name: Identifier) -> Self {
1582+
self.table_name = Some(table_name);
1583+
self
1584+
}
1585+
1586+
pub const fn old_field(mut self, field: Field) -> Self {
1587+
self.old_field = Some(field);
1588+
self
1589+
}
1590+
1591+
pub const fn new_field(mut self, field: Field) -> Self {
1592+
self.new_field = Some(field);
1593+
self
1594+
}
1595+
1596+
pub const fn build(self) -> Operation {
1597+
Operation::new(OperationInner::AlterField {
1598+
table_name: unwrap_builder_option!(self, table_name),
1599+
old_field: unwrap_builder_option!(self, old_field),
1600+
new_field: unwrap_builder_option!(self, new_field),
1601+
})
1602+
}
1603+
}
1604+
15331605
/// A trait for defining a migration.
15341606
///
15351607
/// # Cot CLI Usage

0 commit comments

Comments
 (0)