From 2bed72b1f5ab7937faf5e0c345dfdfb0079511ee Mon Sep 17 00:00:00 2001 From: Phillip Assmann Date: Sun, 25 Oct 2026 18:17:09 +0100 Subject: [PATCH] fix(migrations): keep explicit table names in generated migration models The migration generator cleared all attributes of a model when writing it into a migration file, including an explicit `table_name`. The migration model then derived its table name from its own struct name, so the next `cot migration make` compared it against a different table and generated a bogus remove/create pair for every model with a custom table name. --- cot-cli/src/migration_generator.rs | 19 +++++- cot-cli/tests/migration_generator.rs | 68 +++++++++++++++++++ .../custom_table_name/step_1.rs | 9 +++ .../custom_table_name/step_2.rs | 10 +++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 cot-cli/tests/migration_generator/custom_table_name/step_1.rs create mode 100644 cot-cli/tests/migration_generator/custom_table_name/step_2.rs diff --git a/cot-cli/src/migration_generator.rs b/cot-cli/src/migration_generator.rs index be4e08c73..91085379b 100644 --- a/cot-cli/src/migration_generator.rs +++ b/cot-cli/src/migration_generator.rs @@ -601,6 +601,16 @@ impl MigrationGenerator { #[must_use] fn model_to_migration_model(model: &ModelInSource) -> TokenStream { + // keep the table name if it has been set explicitly, as it would + // otherwise be derived from the (prefixed) name of the migration model + let table_name = model + .model_item + .attrs + .iter() + .find(|attr| is_model_attr(attr)) + .and_then(|attr| ModelArgs::from_meta(&attr.meta).ok()) + .and_then(|args| args.table_name); + let mut model_source = model.model_item.clone(); model_source.vis = syn::Visibility::Inherited; model_source.ident = format_ident!("_{}", model_source.ident); @@ -608,9 +618,12 @@ impl MigrationGenerator { model_source .attrs .push(syn::parse_quote! {#[derive(::core::fmt::Debug)]}); - model_source - .attrs - .push(syn::parse_quote! {#[::cot::db::model(model_type = "migration")]}); + model_source.attrs.push(match table_name { + Some(table_name) => syn::parse_quote! { + #[::cot::db::model(model_type = "migration", table_name = #table_name)] + }, + None => syn::parse_quote! {#[::cot::db::model(model_type = "migration")]}, + }); quote! { #model_source } diff --git a/cot-cli/tests/migration_generator.rs b/cot-cli/tests/migration_generator.rs index 1b17160d0..b3681518b 100644 --- a/cot-cli/tests/migration_generator.rs +++ b/cot-cli/tests/migration_generator.rs @@ -60,6 +60,74 @@ fn create_model_state_test() { assert!(field.foreign_key.clone().is_some()); } +/// Test that the migration generator will generate the correct create model +/// migration if the model has a custom table name, and will also correctly +/// generate the add field migration +#[test] +fn custom_table_name_test() { + let generator = test_generator(); + let src = include_str!("migration_generator/custom_table_name/step_1.rs"); + let source_files = vec![SourceFile::parse(PathBuf::from("main.rs"), src).unwrap()]; + + let migration = generator + .generate_migrations_as_generated_from_files(source_files.clone()) + .unwrap() + .unwrap(); + + let migration_file = generator + .generate_migrations_as_source_from_files(source_files) + .unwrap() + .unwrap(); + + assert_eq!(migration.migration_name, "m_0001_initial"); + assert_eq!(migration.dependencies, []); + + let (table_name, fields) = unwrap_create_model(&migration.operations[0]); + assert_eq!(table_name, "cot__myamazing_table_name"); + assert_eq!(fields.len(), 2); + + let field = &fields[0]; + assert_eq!(field.column_name, "id"); + assert!(field.primary_key); + assert!(field.auto_value); + assert!(field.foreign_key.clone().is_none()); + + let field = &fields[1]; + assert_eq!(field.column_name, "field_1"); + assert!(!field.primary_key); + assert!(!field.auto_value); + assert!(field.foreign_key.clone().is_none()); + + let src = include_str!("migration_generator/custom_table_name/step_2.rs"); + let source_files = vec![ + SourceFile::parse(PathBuf::from("main.rs"), src).unwrap(), + SourceFile::parse(PathBuf::from(&migration_file.name), &migration_file.content).unwrap(), + ]; + + let migration = generator + .generate_migrations_as_generated_from_files(source_files) + .unwrap() + .unwrap(); + + assert_eq!(migration.dependencies.len(), 2); + assert!(migration.dependencies.contains(&DynDependency::Migration { + app: "cot".to_string(), + migration: "m_0001_initial".to_string() + })); + assert!(migration.dependencies.contains(&DynDependency::Model { + model_type: parse_quote!(crate::CustomModel), + })); + + assert_eq!(migration.operations.len(), 1); + + let (table_name, field) = unwrap_add_field(&migration.operations[0]); + assert_eq!(table_name, "cot__myamazing_table_name"); + assert_eq!(field.column_name, "field_2"); + assert!(!field.primary_key); + assert!(!field.auto_value); + assert!(field.foreign_key.clone().is_none()); +} + #[test] fn create_models_foreign_key() { let generator = test_generator(); diff --git a/cot-cli/tests/migration_generator/custom_table_name/step_1.rs b/cot-cli/tests/migration_generator/custom_table_name/step_1.rs new file mode 100644 index 000000000..aa6d19fec --- /dev/null +++ b/cot-cli/tests/migration_generator/custom_table_name/step_1.rs @@ -0,0 +1,9 @@ +use cot::db::{Auto, LimitedString, model}; + +#[derive(Debug)] +#[model(table_name = "myamazing_table_name")] +struct CustomModel { + #[model(primary_key)] + id: Auto, + field_1: String, +} diff --git a/cot-cli/tests/migration_generator/custom_table_name/step_2.rs b/cot-cli/tests/migration_generator/custom_table_name/step_2.rs new file mode 100644 index 000000000..a4cb7ee8f --- /dev/null +++ b/cot-cli/tests/migration_generator/custom_table_name/step_2.rs @@ -0,0 +1,10 @@ +use cot::db::{Auto, LimitedString, model}; + +#[derive(Debug)] +#[model(table_name = "myamazing_table_name")] +struct CustomModel { + #[model(primary_key)] + id: Auto, + field_1: String, + field_2: i32, +}