cot_cli/
migration_generator.rs

1use std::collections::{HashMap, HashSet};
2use std::error::Error;
3use std::fmt::{Debug, Display};
4use std::fs::File;
5use std::io::{Read, Write};
6use std::path::{Path, PathBuf};
7
8use anyhow::{bail, Context};
9use cargo_toml::Manifest;
10use cot::db::migrations::{DynMigration, MigrationEngine};
11use cot_codegen::model::{Field, Model, ModelArgs, ModelOpts, ModelType};
12use cot_codegen::symbol_resolver::SymbolResolver;
13use darling::FromMeta;
14use petgraph::graph::DiGraph;
15use petgraph::visit::EdgeRef;
16use proc_macro2::TokenStream;
17use quote::{format_ident, quote, ToTokens};
18use syn::{parse_quote, Meta};
19use tracing::{debug, trace};
20
21use crate::utils::{find_cargo_toml, print_status_msg, StatusType};
22
23pub fn make_migrations(path: &Path, options: MigrationGeneratorOptions) -> anyhow::Result<()> {
24    match find_cargo_toml(
25        &path
26            .canonicalize()
27            .with_context(|| "unable to canonicalize Cargo.toml path")?,
28    ) {
29        Some(cargo_toml_path) => {
30            let manifest = Manifest::from_path(&cargo_toml_path)
31                .with_context(|| "unable to read Cargo.toml")?;
32            let crate_name = manifest
33                .package
34                .with_context(|| "unable to find package in Cargo.toml")?
35                .name;
36
37            let mut generator = MigrationGenerator::new(cargo_toml_path, crate_name, options);
38            generator
39                .generate_and_write_migrations()
40                .with_context(|| "unable to generate migrations")?;
41            generator
42                .write_migrations_module()
43                .with_context(|| "unable to write migrations.rs")?;
44        }
45        None => {
46            bail!("Cargo.toml not found in the specified directory or any parent directory.")
47        }
48    }
49
50    Ok(())
51}
52
53#[derive(Debug, Clone, Default)]
54pub struct MigrationGeneratorOptions {
55    pub app_name: Option<String>,
56    pub output_dir: Option<PathBuf>,
57}
58
59#[derive(Debug)]
60pub struct MigrationGenerator {
61    cargo_toml_path: PathBuf,
62    crate_name: String,
63    options: MigrationGeneratorOptions,
64}
65
66const MIGRATIONS_MODULE_NAME: &str = "migrations";
67const MIGRATIONS_MODULE_PREFIX: &str = "m_";
68
69impl MigrationGenerator {
70    #[must_use]
71    pub fn new(
72        cargo_toml_path: PathBuf,
73        crate_name: String,
74        options: MigrationGeneratorOptions,
75    ) -> Self {
76        Self {
77            cargo_toml_path,
78            crate_name,
79            options,
80        }
81    }
82
83    fn generate_and_write_migrations(&mut self) -> anyhow::Result<()> {
84        let source_files = self.get_source_files()?;
85
86        if let Some(migration) = self.generate_migrations_to_write(source_files)? {
87            print_status_msg(
88                StatusType::Creating,
89                &format!("Migration '{}'", migration.name),
90            );
91
92            self.write_migration(&migration)?;
93
94            print_status_msg(
95                StatusType::Created,
96                &format!("Migration '{}'", migration.name),
97            );
98        }
99
100        Ok(())
101    }
102
103    /// Generate migrations as a ready-to-write source code.
104    pub fn generate_migrations_to_write(
105        &mut self,
106        source_files: Vec<SourceFile>,
107    ) -> anyhow::Result<Option<MigrationAsSource>> {
108        if let Some(migration) = self.generate_migrations(source_files)? {
109            let migration_name = migration.migration_name.clone();
110            let content = self.generate_migration_file_content(migration);
111            Ok(Some(MigrationAsSource::new(migration_name, content)))
112        } else {
113            Ok(None)
114        }
115    }
116
117    /// Generate migrations and return internal structures that can be used to
118    /// generate source code.
119    pub fn generate_migrations(
120        &mut self,
121        source_files: Vec<SourceFile>,
122    ) -> anyhow::Result<Option<GeneratedMigration>> {
123        let AppState { models, migrations } = self.process_source_files(source_files)?;
124        let migration_processor = MigrationProcessor::new(migrations)?;
125        let migration_models = migration_processor.latest_models();
126
127        let (modified_models, operations) = self.generate_operations(&models, &migration_models);
128        if operations.is_empty() {
129            Ok(None)
130        } else {
131            let migration_name = migration_processor.next_migration_name()?;
132            let dependencies = migration_processor.base_dependencies();
133
134            let migration =
135                GeneratedMigration::new(migration_name, modified_models, dependencies, operations);
136            Ok(Some(migration))
137        }
138    }
139
140    fn get_source_files(&mut self) -> anyhow::Result<Vec<SourceFile>> {
141        let src_dir = self
142            .cargo_toml_path
143            .parent()
144            .with_context(|| "unable to find parent dir")?
145            .join("src");
146        let src_dir = src_dir
147            .canonicalize()
148            .with_context(|| "unable to canonicalize src dir")?;
149
150        let source_file_paths = Self::find_source_files(&src_dir)?;
151        let source_files = source_file_paths
152            .into_iter()
153            .map(|path| {
154                Self::parse_file(&src_dir, path.clone())
155                    .with_context(|| format!("unable to parse file: {path:?}"))
156            })
157            .collect::<anyhow::Result<Vec<_>>>()?;
158        Ok(source_files)
159    }
160
161    fn find_source_files(src_dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
162        let mut paths = Vec::new();
163        for entry in glob::glob(src_dir.join("**/*.rs").to_str().unwrap())
164            .with_context(|| "unable to find Rust source files with glob")?
165        {
166            let path = entry?;
167            paths.push(
168                path.strip_prefix(src_dir)
169                    .expect("path must be in src dir")
170                    .to_path_buf(),
171            );
172        }
173
174        Ok(paths)
175    }
176
177    fn process_source_files(&self, source_files: Vec<SourceFile>) -> anyhow::Result<AppState> {
178        let mut app_state = AppState::new();
179
180        for source_file in source_files {
181            let path = source_file.path.clone();
182            self.process_parsed_file(source_file, &mut app_state)
183                .with_context(|| format!("unable to find models in file: {path:?}"))?;
184        }
185
186        Ok(app_state)
187    }
188
189    fn parse_file(src_dir: &Path, path: PathBuf) -> anyhow::Result<SourceFile> {
190        let full_path = src_dir.join(&path);
191        debug!("Parsing file: {:?}", &full_path);
192        let mut file = File::open(&full_path).with_context(|| "unable to open file")?;
193
194        let mut src = String::new();
195        file.read_to_string(&mut src)
196            .with_context(|| format!("unable to read file: {full_path:?}"))?;
197
198        SourceFile::parse(path, &src)
199    }
200
201    fn process_parsed_file(
202        &self,
203        SourceFile {
204            path,
205            content: file,
206        }: SourceFile,
207        app_state: &mut AppState,
208    ) -> anyhow::Result<()> {
209        trace!("Processing file: {:?}", &path);
210
211        let symbol_resolver = SymbolResolver::from_file(&file, &path);
212
213        let mut migration_models = Vec::new();
214        for item in file.items {
215            if let syn::Item::Struct(mut item) = item {
216                for attr in &item.attrs.clone() {
217                    if is_model_attr(attr) {
218                        symbol_resolver.resolve_struct(&mut item);
219
220                        let args = Self::args_from_attr(&path, attr)?;
221                        let model_in_source =
222                            ModelInSource::from_item(item, &args, &symbol_resolver)?;
223
224                        match args.model_type {
225                            ModelType::Application => {
226                                trace!(
227                                    "Found an Application model: {}",
228                                    model_in_source.model.name.to_string()
229                                );
230                                app_state.models.push(model_in_source);
231                            }
232                            ModelType::Migration => {
233                                trace!(
234                                    "Found a Migration model: {}",
235                                    model_in_source.model.name.to_string()
236                                );
237                                migration_models.push(model_in_source);
238                            }
239                            ModelType::Internal => {}
240                        }
241
242                        break;
243                    }
244                }
245            }
246        }
247
248        if !migration_models.is_empty() {
249            let migration_name = path
250                .file_stem()
251                .with_context(|| format!("unable to get migration file name: {}", path.display()))?
252                .to_string_lossy()
253                .to_string();
254            app_state.migrations.push(Migration {
255                app_name: self.crate_name.clone(),
256                name: migration_name,
257                models: migration_models,
258            });
259        }
260
261        Ok(())
262    }
263
264    fn args_from_attr(path: &Path, attr: &syn::Attribute) -> Result<ModelArgs, ParsingError> {
265        match attr.meta {
266            Meta::Path(_) => {
267                // Means `#[model]` without any arguments
268                Ok(ModelArgs::default())
269            }
270            _ => ModelArgs::from_meta(&attr.meta).map_err(|e| {
271                ParsingError::from_darling(
272                    "couldn't parse model macro arguments",
273                    path.to_owned(),
274                    &e,
275                )
276            }),
277        }
278    }
279
280    #[must_use]
281    fn generate_operations(
282        &self,
283        app_models: &Vec<ModelInSource>,
284        migration_models: &Vec<ModelInSource>,
285    ) -> (Vec<ModelInSource>, Vec<DynOperation>) {
286        let mut operations = Vec::new();
287        let mut modified_models = Vec::new();
288
289        let mut all_model_names = HashSet::new();
290        let mut app_models_map = HashMap::new();
291        for model in app_models {
292            all_model_names.insert(model.model.table_name.clone());
293            app_models_map.insert(model.model.table_name.clone(), model);
294        }
295        let mut migration_models_map = HashMap::new();
296        for model in migration_models {
297            all_model_names.insert(model.model.table_name.clone());
298            migration_models_map.insert(model.model.table_name.clone(), model);
299        }
300        let mut all_model_names: Vec<_> = all_model_names.into_iter().collect();
301        all_model_names.sort();
302
303        for model_name in all_model_names {
304            let app_model = app_models_map.get(&model_name);
305            let migration_model = migration_models_map.get(&model_name);
306
307            match (app_model, migration_model) {
308                (Some(&app_model), None) => {
309                    operations.push(Self::make_create_model_operation(app_model));
310                    modified_models.push(app_model.clone());
311                }
312                (Some(&app_model), Some(&migration_model)) => {
313                    if app_model.model != migration_model.model {
314                        modified_models.push(app_model.clone());
315                        operations
316                            .extend(self.make_alter_model_operations(app_model, migration_model));
317                    }
318                }
319                (None, Some(&migration_model)) => {
320                    operations.push(self.make_remove_model_operation(migration_model));
321                }
322                (None, None) => unreachable!(),
323            }
324        }
325
326        (modified_models, operations)
327    }
328
329    #[must_use]
330    fn make_create_model_operation(app_model: &ModelInSource) -> DynOperation {
331        print_status_msg(
332            StatusType::Creating,
333            &format!("Model '{}'", app_model.model.table_name),
334        );
335        let op = DynOperation::CreateModel {
336            table_name: app_model.model.table_name.clone(),
337            model_ty: app_model.model.resolved_ty.clone(),
338            fields: app_model.model.fields.clone(),
339        };
340        print_status_msg(
341            StatusType::Created,
342            &format!("Model '{}'", app_model.model.table_name),
343        );
344        op
345    }
346
347    #[must_use]
348    fn make_alter_model_operations(
349        &self,
350        app_model: &ModelInSource,
351        migration_model: &ModelInSource,
352    ) -> Vec<DynOperation> {
353        let mut all_field_names = HashSet::new();
354        let mut app_model_fields = HashMap::new();
355        print_status_msg(
356            StatusType::Modifying,
357            &format!("Model '{}'", app_model.model.table_name),
358        );
359
360        for field in &app_model.model.fields {
361            all_field_names.insert(field.column_name.clone());
362            app_model_fields.insert(field.column_name.clone(), field);
363        }
364        let mut migration_model_fields = HashMap::new();
365        for field in &migration_model.model.fields {
366            all_field_names.insert(field.column_name.clone());
367            migration_model_fields.insert(field.column_name.clone(), field);
368        }
369
370        let mut all_field_names: Vec<_> = all_field_names.into_iter().collect();
371        // sort to ensure deterministic order
372        all_field_names.sort();
373
374        let mut operations = Vec::new();
375        for field_name in all_field_names {
376            let app_field = app_model_fields.get(&field_name);
377            let migration_field = migration_model_fields.get(&field_name);
378
379            match (app_field, migration_field) {
380                (Some(app_field), None) => {
381                    operations.push(Self::make_add_field_operation(app_model, app_field));
382                }
383                (Some(app_field), Some(migration_field)) => {
384                    let operation = self.make_alter_field_operation(
385                        app_model,
386                        app_field,
387                        migration_model,
388                        migration_field,
389                    );
390                    if let Some(operation) = operation {
391                        operations.push(operation);
392                    }
393                }
394                (None, Some(migration_field)) => {
395                    operations
396                        .push(self.make_remove_field_operation(migration_model, migration_field));
397                }
398                (None, None) => unreachable!(),
399            }
400        }
401        print_status_msg(
402            StatusType::Modified,
403            &format!("Model '{}'", app_model.model.table_name),
404        );
405
406        operations
407    }
408
409    #[must_use]
410    fn make_add_field_operation(app_model: &ModelInSource, field: &Field) -> DynOperation {
411        print_status_msg(
412            StatusType::Adding,
413            &format!(
414                "Field '{}' to Model '{}'",
415                &field.field_name, app_model.model.name
416            ),
417        );
418
419        let op = DynOperation::AddField {
420            table_name: app_model.model.table_name.clone(),
421            model_ty: app_model.model.resolved_ty.clone(),
422            field: field.clone(),
423        };
424
425        print_status_msg(
426            StatusType::Added,
427            &format!(
428                "Field '{}' to Model '{}'",
429                &field.field_name, app_model.model.name
430            ),
431        );
432
433        op
434    }
435
436    #[must_use]
437    fn make_alter_field_operation(
438        &self,
439        _app_model: &ModelInSource,
440        app_field: &Field,
441        migration_model: &ModelInSource,
442        migration_field: &Field,
443    ) -> Option<DynOperation> {
444        if app_field == migration_field {
445            return None;
446        }
447        print_status_msg(
448            StatusType::Modifying,
449            &format!(
450                "Field '{}' from Model '{}'",
451                &migration_field.field_name, migration_model.model.name
452            ),
453        );
454
455        todo!();
456
457        // line below should be removed once todo is implemented
458        #[allow(unreachable_code)]
459        print_status_msg(
460            StatusType::Modified,
461            &format!(
462                "Field '{}' from Model '{}'",
463                &migration_field.field_name, migration_model.model.name
464            ),
465        );
466    }
467
468    #[must_use]
469    fn make_remove_field_operation(
470        &self,
471        migration_model: &ModelInSource,
472        migration_field: &Field,
473    ) -> DynOperation {
474        print_status_msg(
475            StatusType::Removing,
476            &format!(
477                "Field '{}' from Model '{}'",
478                &migration_field.field_name, migration_model.model.name
479            ),
480        );
481
482        todo!();
483        // line below should be removed once todo is implemented
484        #[allow(unreachable_code)]
485        print_status_msg(
486            StatusType::Removed,
487            &format!(
488                "Field '{}' from Model '{}'",
489                &migration_field.field_name, migration_model.model.name
490            ),
491        );
492    }
493
494    #[must_use]
495    fn make_remove_model_operation(&self, migration_model: &ModelInSource) -> DynOperation {
496        print_status_msg(
497            StatusType::Removing,
498            &format!("Model '{}'", &migration_model.model.name),
499        );
500
501        todo!();
502
503        // line below should be removed once todo is implemented
504        #[allow(unreachable_code)]
505        print_status_msg(
506            StatusType::Removed,
507            &format!("Model '{}'", &migration_model.model.name),
508        );
509    }
510
511    fn generate_migration_file_content(&self, migration: GeneratedMigration) -> String {
512        let operations: Vec<_> = migration
513            .operations
514            .into_iter()
515            .map(|operation| operation.repr())
516            .collect();
517        let dependencies: Vec<_> = migration
518            .dependencies
519            .into_iter()
520            .map(|dependency| dependency.repr())
521            .collect();
522
523        let app_name = self.options.app_name.as_ref().unwrap_or(&self.crate_name);
524        let migration_name = &migration.migration_name;
525        let migration_def = quote! {
526            #[derive(Debug, Copy, Clone)]
527            pub(super) struct Migration;
528
529            impl ::cot::db::migrations::Migration for Migration {
530                const APP_NAME: &'static str = #app_name;
531                const MIGRATION_NAME: &'static str = #migration_name;
532                const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = &[
533                    #(#dependencies,)*
534                ];
535                const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
536                    #(#operations,)*
537                ];
538            }
539        };
540
541        let models = migration
542            .modified_models
543            .iter()
544            .map(Self::model_to_migration_model)
545            .collect::<Vec<_>>();
546        let models_def = quote! {
547            #(#models)*
548        };
549
550        Self::generate_migration(migration_def, models_def)
551    }
552
553    fn write_migration(&self, migration: &MigrationAsSource) -> anyhow::Result<()> {
554        let src_path = self.get_src_path();
555        let migration_path = src_path.join(MIGRATIONS_MODULE_NAME);
556        let migration_file = migration_path.join(format!("{}.rs", migration.name));
557        print_status_msg(
558            StatusType::Creating,
559            &format!("Migration file '{}'", migration_file.display()),
560        );
561        std::fs::create_dir_all(&migration_path).with_context(|| {
562            format!(
563                "unable to create migrations directory: {}",
564                migration_path.display()
565            )
566        })?;
567
568        let mut file = File::create(&migration_file).with_context(|| {
569            format!(
570                "unable to create migration file: {}",
571                migration_file.display()
572            )
573        })?;
574        file.write_all(migration.content.as_bytes())
575            .with_context(|| "unable to write migration file")?;
576        print_status_msg(
577            StatusType::Created,
578            &format!("Migration file '{}'", migration_file.display()),
579        );
580        Ok(())
581    }
582
583    #[must_use]
584    fn generate_migration(migration: TokenStream, modified_models: TokenStream) -> String {
585        let migration = Self::format_tokens(migration);
586        let modified_models = Self::format_tokens(modified_models);
587
588        let header = Self::migration_header();
589
590        format!("{header}\n\n{migration}\n{modified_models}")
591    }
592
593    fn migration_header() -> String {
594        let version = env!("CARGO_PKG_VERSION");
595        let date_time = chrono::offset::Utc::now().format("%Y-%m-%d %H:%M:%S%:z");
596        let header = format!("//! Generated by cot CLI {version} on {date_time}");
597        header
598    }
599
600    #[must_use]
601    fn format_tokens(tokens: TokenStream) -> String {
602        let parsed: syn::File = syn::parse2(tokens).unwrap();
603        prettyplease::unparse(&parsed)
604    }
605
606    #[must_use]
607    fn model_to_migration_model(model: &ModelInSource) -> TokenStream {
608        let mut model_source = model.model_item.clone();
609        model_source.vis = syn::Visibility::Inherited;
610        model_source.ident = format_ident!("_{}", model_source.ident);
611        model_source.attrs.clear();
612        model_source
613            .attrs
614            .push(syn::parse_quote! {#[derive(::core::fmt::Debug)]});
615        model_source
616            .attrs
617            .push(syn::parse_quote! {#[::cot::db::model(model_type = "migration")]});
618        quote! {
619            #model_source
620        }
621    }
622
623    pub fn write_migrations_module(&self) -> anyhow::Result<()> {
624        let src_path = self.get_src_path();
625        let migrations_dir = src_path.join(MIGRATIONS_MODULE_NAME);
626
627        let migration_list = Self::get_migration_list(&migrations_dir)?;
628        let contents = Self::get_migration_module_contents(&migration_list);
629        let contents_string = Self::format_tokens(contents);
630
631        let header = Self::migration_header();
632        let migration_header = "//! List of migrations for the current app.\n//!";
633        let contents_with_header = format!("{migration_header}\n{header}\n\n{contents_string}");
634
635        let mut file = File::create(src_path.join(format!("{MIGRATIONS_MODULE_NAME}.rs")))?;
636        file.write_all(contents_with_header.as_bytes())?;
637
638        Ok(())
639    }
640
641    fn get_migration_list(migrations_dir: &PathBuf) -> anyhow::Result<Vec<String>> {
642        Ok(std::fs::read_dir(migrations_dir)
643            .with_context(|| {
644                format!(
645                    "unable to read migrations directory: {}",
646                    migrations_dir.display()
647                )
648            })?
649            .filter_map(|entry| {
650                let entry = entry.ok()?;
651                let path = entry.path();
652                let stem = path.file_stem();
653
654                if path.is_file()
655                    && stem
656                        .unwrap_or_default()
657                        .to_string_lossy()
658                        .starts_with(MIGRATIONS_MODULE_PREFIX)
659                    && path.extension() == Some("rs".as_ref())
660                {
661                    stem.map(|stem| stem.to_string_lossy().to_string())
662                } else {
663                    None
664                }
665            })
666            .collect())
667    }
668
669    #[must_use]
670    fn get_migration_module_contents(migration_list: &[String]) -> TokenStream {
671        let migration_mods = migration_list.iter().map(|migration| {
672            let migration = format_ident!("{}", migration);
673            quote! {
674                pub mod #migration;
675            }
676        });
677        let migration_refs = migration_list.iter().map(|migration| {
678            let migration = format_ident!("{}", migration);
679            quote! {
680                &#migration::Migration
681            }
682        });
683
684        quote! {
685            #(#migration_mods)*
686
687            /// The list of migrations for current app.
688            pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[
689                #(#migration_refs),*
690            ];
691        }
692    }
693
694    fn get_src_path(&self) -> PathBuf {
695        self.options
696            .output_dir
697            .clone()
698            .unwrap_or(self.cargo_toml_path.parent().unwrap().join("src"))
699    }
700}
701
702#[derive(Debug, Clone)]
703pub struct SourceFile {
704    path: PathBuf,
705    content: syn::File,
706}
707
708impl SourceFile {
709    #[must_use]
710    fn new(path: PathBuf, content: syn::File) -> Self {
711        assert!(
712            path.is_relative(),
713            "path must be relative to the src directory"
714        );
715        Self { path, content }
716    }
717
718    pub fn parse(path: PathBuf, content: &str) -> anyhow::Result<Self> {
719        Ok(Self::new(
720            path,
721            syn::parse_file(content).with_context(|| "unable to parse file")?,
722        ))
723    }
724}
725
726#[derive(Debug, Clone)]
727struct AppState {
728    /// All the application models found in the source
729    models: Vec<ModelInSource>,
730    /// All the migrations found in the source
731    migrations: Vec<Migration>,
732}
733
734impl AppState {
735    #[must_use]
736    fn new() -> Self {
737        Self {
738            models: Vec::new(),
739            migrations: Vec::new(),
740        }
741    }
742}
743
744/// Helper struct to process already existing migrations.
745#[derive(Debug, Clone)]
746struct MigrationProcessor {
747    migrations: Vec<Migration>,
748}
749
750impl MigrationProcessor {
751    fn new(mut migrations: Vec<Migration>) -> anyhow::Result<Self> {
752        MigrationEngine::sort_migrations(&mut migrations)?;
753        Ok(Self { migrations })
754    }
755
756    /// Returns the latest (in the order of applying migrations) versions of the
757    /// models that are marked as migration models, that means the latest
758    /// version of each migration model.
759    ///
760    /// This is useful for generating migrations - we can compare the latest
761    /// version of the model in the source code with the latest version of the
762    /// model in the migrations (returned by this method) and generate the
763    /// necessary operations.
764    #[must_use]
765    fn latest_models(&self) -> Vec<ModelInSource> {
766        let mut migration_models: HashMap<String, &ModelInSource> = HashMap::new();
767        for migration in &self.migrations {
768            for model in &migration.models {
769                migration_models.insert(model.model.table_name.clone(), model);
770            }
771        }
772
773        migration_models.into_values().cloned().collect()
774    }
775
776    fn next_migration_name(&self) -> anyhow::Result<String> {
777        if self.migrations.is_empty() {
778            return Ok(format!("{MIGRATIONS_MODULE_PREFIX}0001_initial"));
779        }
780
781        let last_migration = self.migrations.last().unwrap();
782        let last_migration_number = last_migration
783            .name
784            .split('_')
785            .nth(1)
786            .with_context(|| format!("migration number not found: {}", last_migration.name))?
787            .parse::<u32>()
788            .with_context(|| {
789                format!("unable to parse migration number: {}", last_migration.name)
790            })?;
791
792        let migration_number = last_migration_number + 1;
793        let now = chrono::Utc::now();
794        let date_time = now.format("%Y%m%d_%H%M%S");
795
796        Ok(format!(
797            "{MIGRATIONS_MODULE_PREFIX}{migration_number:04}_auto_{date_time}"
798        ))
799    }
800
801    /// Returns the list of dependencies for the next migration, based on the
802    /// already existing and processed migrations.
803    fn base_dependencies(&self) -> Vec<DynDependency> {
804        if self.migrations.is_empty() {
805            return Vec::new();
806        }
807
808        let last_migration = self.migrations.last().unwrap();
809        vec![DynDependency::Migration {
810            app: last_migration.app_name.clone(),
811            migration: last_migration.name.clone(),
812        }]
813    }
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Hash)]
817pub struct ModelInSource {
818    model_item: syn::ItemStruct,
819    model: Model,
820}
821
822impl ModelInSource {
823    fn from_item(
824        item: syn::ItemStruct,
825        args: &ModelArgs,
826        symbol_resolver: &SymbolResolver,
827    ) -> anyhow::Result<Self> {
828        let input: syn::DeriveInput = item.clone().into();
829        let opts = ModelOpts::new_from_derive_input(&input)
830            .map_err(|e| anyhow::anyhow!("cannot parse model: {}", e))?;
831        let model = opts.as_model(args, symbol_resolver)?;
832
833        Ok(Self {
834            model_item: item,
835            model,
836        })
837    }
838}
839
840/// A migration generated by the CLI and before converting to a Rust
841/// source code and writing to a file.
842#[derive(Debug, Clone)]
843pub struct GeneratedMigration {
844    pub migration_name: String,
845    pub modified_models: Vec<ModelInSource>,
846    pub dependencies: Vec<DynDependency>,
847    pub operations: Vec<DynOperation>,
848}
849
850impl GeneratedMigration {
851    #[must_use]
852    fn new(
853        migration_name: String,
854        modified_models: Vec<ModelInSource>,
855        mut dependencies: Vec<DynDependency>,
856        mut operations: Vec<DynOperation>,
857    ) -> Self {
858        Self::remove_cycles(&mut operations);
859        Self::toposort_operations(&mut operations);
860        dependencies.extend(Self::get_foreign_key_dependencies(&operations));
861
862        Self {
863            migration_name,
864            modified_models,
865            dependencies,
866            operations,
867        }
868    }
869
870    /// Get the list of [`DynDependency`] for all foreign keys that point
871    /// to models that are **not** created in this migration.
872    fn get_foreign_key_dependencies(operations: &[DynOperation]) -> Vec<DynDependency> {
873        let create_ops = Self::get_create_ops_map(operations);
874        let ops_adding_foreign_keys = Self::get_ops_adding_foreign_keys(operations);
875
876        let mut dependencies = Vec::new();
877        for (_index, dependency_ty) in &ops_adding_foreign_keys {
878            if !create_ops.contains_key(dependency_ty) {
879                dependencies.push(DynDependency::Model {
880                    model_type: dependency_ty.clone(),
881                });
882            }
883        }
884
885        dependencies
886    }
887
888    /// Removes dependency cycles by removing operations that create cycles.
889    ///
890    /// This method tries to minimize the number of operations added by
891    /// calculating the minimum feedback arc set of the dependency graph.
892    ///
893    /// This method modifies the `operations` parameter in place.
894    ///
895    /// # See also
896    ///
897    /// * [`Self::remove_dependency`]
898    fn remove_cycles(operations: &mut Vec<DynOperation>) {
899        let graph = Self::construct_dependency_graph(operations);
900
901        let cycle_edges = petgraph::algo::feedback_arc_set::greedy_feedback_arc_set(&graph);
902        for edge_id in cycle_edges {
903            let (from, to) = graph
904                .edge_endpoints(edge_id.id())
905                .expect("greedy_feedback_arc_set should always return valid edge refs");
906
907            let to_op = operations[to.index()].clone();
908            let from_op = &mut operations[from.index()];
909            debug!(
910                "Removing cycle by removing operation {:?} that depends on {:?}",
911                from_op, to_op
912            );
913
914            let to_add = Self::remove_dependency(from_op, &to_op);
915            operations.extend(to_add);
916        }
917    }
918
919    /// Remove a dependency between two operations.
920    ///
921    /// This is done by removing foreign keys from the `from` operation that
922    /// point to the model created by `to` operation, and creating a new
923    /// `AddField` operation for each removed foreign key.
924    #[must_use]
925    fn remove_dependency(from: &mut DynOperation, to: &DynOperation) -> Vec<DynOperation> {
926        match from {
927            DynOperation::CreateModel {
928                table_name,
929                model_ty,
930                fields,
931            } => {
932                let to_type = match to {
933                    DynOperation::CreateModel { model_ty, .. } => model_ty,
934                    DynOperation::AddField { .. } => {
935                        unreachable!(
936                            "AddField operation shouldn't be a dependency of CreateModel \
937                            because it doesn't create a new model"
938                        )
939                    }
940                };
941                trace!(
942                    "Removing foreign keys from {} to {}",
943                    model_ty.to_token_stream().to_string(),
944                    to_type.into_token_stream().to_string()
945                );
946
947                let mut result = Vec::new();
948                let (fields_to_remove, fields_to_retain): (Vec<_>, Vec<_>) = std::mem::take(fields)
949                    .into_iter()
950                    .partition(|field| is_field_foreign_key_to(field, to_type));
951                *fields = fields_to_retain;
952
953                for field in fields_to_remove {
954                    result.push(DynOperation::AddField {
955                        table_name: table_name.clone(),
956                        model_ty: model_ty.clone(),
957                        field,
958                    });
959                }
960
961                result
962            }
963            DynOperation::AddField { .. } => {
964                // AddField only links two already existing models together, so
965                // removing it shouldn't ever affect whether a graph is cyclic
966                unreachable!("AddField operation should never create cycles")
967            }
968        }
969    }
970
971    /// Topologically sort operations in this migration.
972    ///
973    /// This is to ensure that operations will be applied in the correct order.
974    /// If there are no dependencies between operations, the order of operations
975    /// will not be modified.
976    ///
977    /// This method modifies the `operations` field in place.
978    ///
979    /// # Panics
980    ///
981    /// This method should be called after removing cycles; otherwise it will
982    /// panic.
983    fn toposort_operations(operations: &mut [DynOperation]) {
984        let graph = Self::construct_dependency_graph(operations);
985
986        let sorted = petgraph::algo::toposort(&graph, None)
987            .expect("cycles shouldn't exist after removing them");
988        let mut sorted = sorted
989            .into_iter()
990            .map(petgraph::graph::NodeIndex::index)
991            .collect::<Vec<_>>();
992        cot::__private::apply_permutation(operations, &mut sorted);
993    }
994
995    /// Construct a graph that represents reverse dependencies between
996    /// given operations.
997    ///
998    /// The graph is directed and has an edge from operation A to operation B
999    /// if operation B creates a foreign key that points to a model created by
1000    /// operation A.
1001    #[must_use]
1002    fn construct_dependency_graph(operations: &[DynOperation]) -> DiGraph<usize, (), usize> {
1003        let create_ops = Self::get_create_ops_map(operations);
1004        let ops_adding_foreign_keys = Self::get_ops_adding_foreign_keys(operations);
1005
1006        let mut graph = DiGraph::with_capacity(operations.len(), 0);
1007
1008        for i in 0..operations.len() {
1009            graph.add_node(i);
1010        }
1011        for (i, dependency_ty) in &ops_adding_foreign_keys {
1012            if let Some(&dependency) = create_ops.get(dependency_ty) {
1013                graph.add_edge(
1014                    petgraph::graph::NodeIndex::new(dependency),
1015                    petgraph::graph::NodeIndex::new(*i),
1016                    (),
1017                );
1018            }
1019        }
1020
1021        graph
1022    }
1023
1024    /// Return a map of (resolved) model types to the index of the
1025    /// operation that creates given model.
1026    #[must_use]
1027    fn get_create_ops_map(operations: &[DynOperation]) -> HashMap<syn::Type, usize> {
1028        #[allow(clippy::match_wildcard_for_single_variants)] // we only care about CreateModel here
1029        operations
1030            .iter()
1031            .enumerate()
1032            .filter_map(|(i, op)| match op {
1033                DynOperation::CreateModel { model_ty, .. } => Some((model_ty.clone(), i)),
1034                _ => None,
1035            })
1036            .collect()
1037    }
1038
1039    /// Return a list of operations that add foreign keys as tuples of
1040    /// operation index and the type of the model that foreign key points to.
1041    #[must_use]
1042    fn get_ops_adding_foreign_keys(operations: &[DynOperation]) -> Vec<(usize, syn::Type)> {
1043        operations
1044            .iter()
1045            .enumerate()
1046            .flat_map(|(i, op)| match op {
1047                DynOperation::CreateModel { fields, .. } => fields
1048                    .iter()
1049                    .filter_map(foreign_key_for_field)
1050                    .map(|to_model| (i, to_model))
1051                    .collect::<Vec<(usize, syn::Type)>>(),
1052                DynOperation::AddField {
1053                    field, model_ty, ..
1054                } => {
1055                    let mut ops = vec![(i, model_ty.clone())];
1056
1057                    if let Some(to_type) = foreign_key_for_field(field) {
1058                        ops.push((i, to_type));
1059                    }
1060
1061                    ops
1062                }
1063            })
1064            .collect()
1065    }
1066}
1067
1068/// A migration represented as a generated and ready to write Rust source code.
1069#[derive(Debug, Clone)]
1070pub struct MigrationAsSource {
1071    pub name: String,
1072    pub content: String,
1073}
1074
1075impl MigrationAsSource {
1076    #[must_use]
1077    pub(crate) fn new(name: String, content: String) -> Self {
1078        Self { name, content }
1079    }
1080}
1081
1082#[must_use]
1083fn is_model_attr(attr: &syn::Attribute) -> bool {
1084    let path = attr.path();
1085
1086    let model_path: syn::Path = parse_quote!(cot::db::model);
1087    let model_path_prefixed: syn::Path = parse_quote!(::cot::db::model);
1088
1089    attr.style == syn::AttrStyle::Outer
1090        && (path.is_ident("model") || path == &model_path || path == &model_path_prefixed)
1091}
1092
1093trait Repr {
1094    fn repr(&self) -> TokenStream;
1095}
1096
1097impl Repr for Field {
1098    fn repr(&self) -> TokenStream {
1099        let column_name = &self.column_name;
1100        let ty = &self.ty;
1101        let mut tokens = quote! {
1102            ::cot::db::migrations::Field::new(::cot::db::Identifier::new(#column_name), <#ty as ::cot::db::DatabaseField>::TYPE)
1103        };
1104        if self.auto_value {
1105            tokens = quote! { #tokens.auto() }
1106        }
1107        if self.primary_key {
1108            tokens = quote! { #tokens.primary_key() }
1109        }
1110        if let Some(fk_spec) = self.foreign_key.clone() {
1111            let to_model = &fk_spec.to_model;
1112
1113            tokens = quote! {
1114                #tokens.foreign_key(
1115                    <#to_model as ::cot::db::Model>::TABLE_NAME,
1116                    <#to_model as ::cot::db::Model>::PRIMARY_KEY_NAME,
1117                    ::cot::db::ForeignKeyOnDeletePolicy::Restrict,
1118                    ::cot::db::ForeignKeyOnUpdatePolicy::Restrict,
1119                )
1120            }
1121        }
1122        tokens = quote! { #tokens.set_null(<#ty as ::cot::db::DatabaseField>::NULLABLE) };
1123        if self.unique {
1124            tokens = quote! { #tokens.unique() }
1125        }
1126        tokens
1127    }
1128}
1129
1130#[derive(Debug, Clone, PartialEq, Eq)]
1131struct Migration {
1132    app_name: String,
1133    name: String,
1134    models: Vec<ModelInSource>,
1135}
1136
1137impl DynMigration for Migration {
1138    fn app_name(&self) -> &str {
1139        &self.app_name
1140    }
1141
1142    fn name(&self) -> &str {
1143        &self.name
1144    }
1145
1146    fn dependencies(&self) -> &[cot::db::migrations::MigrationDependency] {
1147        &[]
1148    }
1149
1150    fn operations(&self) -> &[cot::db::migrations::Operation] {
1151        &[]
1152    }
1153}
1154
1155/// A version of [`cot::db::migrations::MigrationDependency`] that can be
1156/// created at runtime and is using codegen types.
1157///
1158/// This is used to generate migration files.
1159#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1160pub enum DynDependency {
1161    Migration { app: String, migration: String },
1162    Model { model_type: syn::Type },
1163}
1164
1165impl Repr for DynDependency {
1166    fn repr(&self) -> TokenStream {
1167        match self {
1168            Self::Migration { app, migration } => {
1169                quote! {
1170                    ::cot::db::migrations::MigrationDependency::migration(#app, #migration)
1171                }
1172            }
1173            Self::Model { model_type } => {
1174                quote! {
1175                    ::cot::db::migrations::MigrationDependency::model(
1176                        <#model_type as ::cot::db::Model>::APP_NAME,
1177                        <#model_type as ::cot::db::Model>::TABLE_NAME
1178                    )
1179                }
1180            }
1181        }
1182    }
1183}
1184
1185/// A version of [`cot::db::migrations::Operation`] that can be created at
1186/// runtime and is using codegen types.
1187///
1188/// This is used to generate migration files.
1189#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1190pub enum DynOperation {
1191    CreateModel {
1192        table_name: String,
1193        model_ty: syn::Type,
1194        fields: Vec<Field>,
1195    },
1196    AddField {
1197        table_name: String,
1198        model_ty: syn::Type,
1199        field: Field,
1200    },
1201}
1202
1203/// Returns whether given [`Field`] is a foreign key to given type.
1204fn is_field_foreign_key_to(field: &Field, ty: &syn::Type) -> bool {
1205    foreign_key_for_field(field).is_some_and(|to_model| &to_model == ty)
1206}
1207
1208/// Returns the type of the model that the given field is a foreign key to.
1209/// Returns [`None`] if the field is not a foreign key.
1210fn foreign_key_for_field(field: &Field) -> Option<syn::Type> {
1211    match field.foreign_key.clone() {
1212        None => None,
1213        Some(foreign_key_spec) => Some(foreign_key_spec.to_model),
1214    }
1215}
1216
1217impl Repr for DynOperation {
1218    fn repr(&self) -> TokenStream {
1219        match self {
1220            Self::CreateModel {
1221                table_name, fields, ..
1222            } => {
1223                let fields = fields.iter().map(Repr::repr).collect::<Vec<_>>();
1224                quote! {
1225                    ::cot::db::migrations::Operation::create_model()
1226                        .table_name(::cot::db::Identifier::new(#table_name))
1227                        .fields(&[
1228                            #(#fields,)*
1229                        ])
1230                        .build()
1231                }
1232            }
1233            Self::AddField {
1234                table_name, field, ..
1235            } => {
1236                let field = field.repr();
1237                quote! {
1238                    ::cot::db::migrations::Operation::add_field()
1239                        .table_name(::cot::db::Identifier::new(#table_name))
1240                        .field(#field)
1241                        .build()
1242                }
1243            }
1244        }
1245    }
1246}
1247
1248#[derive(Debug)]
1249struct ParsingError {
1250    message: String,
1251    path: PathBuf,
1252    location: String,
1253    source: Option<String>,
1254}
1255
1256impl ParsingError {
1257    fn from_darling(message: &str, path: PathBuf, error: &darling::Error) -> Self {
1258        let message = format!("{message}: {error}");
1259        let span = error.span();
1260        let location = format!("{}:{}", span.start().line, span.start().column);
1261
1262        Self {
1263            message,
1264            path,
1265            location,
1266            source: span.source_text().clone(),
1267        }
1268    }
1269}
1270
1271impl Display for ParsingError {
1272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1273        write!(f, "{}", self.message)?;
1274        if let Some(source) = &self.source {
1275            write!(f, "\n{source}")?;
1276        }
1277        write!(f, "\n    at {}:{}", self.path.display(), self.location)?;
1278        Ok(())
1279    }
1280}
1281
1282impl Error for ParsingError {}
1283
1284#[cfg(test)]
1285mod tests {
1286    use cot_codegen::model::ForeignKeySpec;
1287
1288    use super::*;
1289
1290    #[test]
1291    fn migration_processor_next_migration_name_empty() {
1292        let migrations = vec![];
1293        let processor = MigrationProcessor::new(migrations).unwrap();
1294
1295        let next_migration_name = processor.next_migration_name().unwrap();
1296        assert_eq!(next_migration_name, "m_0001_initial");
1297    }
1298
1299    #[test]
1300    fn migration_processor_dependencies_empty() {
1301        let migrations = vec![];
1302        let processor = MigrationProcessor::new(migrations).unwrap();
1303
1304        let next_migration_name = processor.base_dependencies();
1305        assert_eq!(next_migration_name, vec![]);
1306    }
1307
1308    #[test]
1309    fn migration_processor_dependencies_previous() {
1310        let migrations = vec![Migration {
1311            app_name: "app1".to_string(),
1312            name: "m0001_initial".to_string(),
1313            models: vec![],
1314        }];
1315        let processor = MigrationProcessor::new(migrations).unwrap();
1316
1317        let next_migration_name = processor.base_dependencies();
1318        assert_eq!(
1319            next_migration_name,
1320            vec![DynDependency::Migration {
1321                app: "app1".to_string(),
1322                migration: "m0001_initial".to_string(),
1323            }]
1324        );
1325    }
1326
1327    #[test]
1328    fn toposort_operations() {
1329        let mut operations = vec![
1330            DynOperation::AddField {
1331                table_name: "table2".to_string(),
1332                model_ty: parse_quote!(Table2),
1333                field: Field {
1334                    field_name: format_ident!("field1"),
1335                    column_name: "field1".to_string(),
1336                    ty: parse_quote!(i32),
1337                    auto_value: false,
1338                    primary_key: false,
1339                    unique: false,
1340                    foreign_key: Some(ForeignKeySpec {
1341                        to_model: parse_quote!(Table1),
1342                    }),
1343                },
1344            },
1345            DynOperation::CreateModel {
1346                table_name: "table1".to_string(),
1347                model_ty: parse_quote!(Table1),
1348                fields: vec![],
1349            },
1350        ];
1351
1352        GeneratedMigration::toposort_operations(&mut operations);
1353
1354        assert_eq!(operations.len(), 2);
1355        if let DynOperation::CreateModel { table_name, .. } = &operations[0] {
1356            assert_eq!(table_name, "table1");
1357        } else {
1358            panic!("Expected CreateModel operation");
1359        }
1360        if let DynOperation::AddField { table_name, .. } = &operations[1] {
1361            assert_eq!(table_name, "table2");
1362        } else {
1363            panic!("Expected AddField operation");
1364        }
1365    }
1366
1367    #[test]
1368    fn remove_cycles() {
1369        let mut operations = vec![
1370            DynOperation::CreateModel {
1371                table_name: "table1".to_string(),
1372                model_ty: parse_quote!(Table1),
1373                fields: vec![Field {
1374                    field_name: format_ident!("field1"),
1375                    column_name: "field1".to_string(),
1376                    ty: parse_quote!(ForeignKey<Table2>),
1377                    auto_value: false,
1378                    primary_key: false,
1379                    unique: false,
1380                    foreign_key: Some(ForeignKeySpec {
1381                        to_model: parse_quote!(Table2),
1382                    }),
1383                }],
1384            },
1385            DynOperation::CreateModel {
1386                table_name: "table2".to_string(),
1387                model_ty: parse_quote!(Table2),
1388                fields: vec![Field {
1389                    field_name: format_ident!("field1"),
1390                    column_name: "field1".to_string(),
1391                    ty: parse_quote!(ForeignKey<Table1>),
1392                    auto_value: false,
1393                    primary_key: false,
1394                    unique: false,
1395                    foreign_key: Some(ForeignKeySpec {
1396                        to_model: parse_quote!(Table1),
1397                    }),
1398                }],
1399            },
1400        ];
1401
1402        GeneratedMigration::remove_cycles(&mut operations);
1403
1404        assert_eq!(operations.len(), 3);
1405        if let DynOperation::CreateModel {
1406            table_name, fields, ..
1407        } = &operations[0]
1408        {
1409            assert_eq!(table_name, "table1");
1410            assert!(!fields.is_empty());
1411        } else {
1412            panic!("Expected CreateModel operation");
1413        }
1414        if let DynOperation::CreateModel {
1415            table_name, fields, ..
1416        } = &operations[1]
1417        {
1418            assert_eq!(table_name, "table2");
1419            assert!(fields.is_empty());
1420        } else {
1421            panic!("Expected CreateModel operation");
1422        }
1423        if let DynOperation::AddField { table_name, .. } = &operations[2] {
1424            assert_eq!(table_name, "table2");
1425        } else {
1426            panic!("Expected AddField operation");
1427        }
1428    }
1429
1430    #[test]
1431    fn remove_dependency() {
1432        let mut create_model_op = DynOperation::CreateModel {
1433            table_name: "table1".to_string(),
1434            model_ty: parse_quote!(Table1),
1435            fields: vec![Field {
1436                field_name: format_ident!("field1"),
1437                column_name: "field1".to_string(),
1438                ty: parse_quote!(ForeignKey<Table2>),
1439                auto_value: false,
1440                primary_key: false,
1441                unique: false,
1442                foreign_key: Some(ForeignKeySpec {
1443                    to_model: parse_quote!(Table2),
1444                }),
1445            }],
1446        };
1447
1448        let add_field_op = DynOperation::CreateModel {
1449            table_name: "table2".to_string(),
1450            model_ty: parse_quote!(Table2),
1451            fields: vec![],
1452        };
1453
1454        let additional_ops =
1455            GeneratedMigration::remove_dependency(&mut create_model_op, &add_field_op);
1456
1457        match create_model_op {
1458            DynOperation::CreateModel { fields, .. } => {
1459                assert_eq!(fields.len(), 0);
1460            }
1461            _ => {
1462                panic!("Expected from operation not to change type");
1463            }
1464        }
1465        assert_eq!(additional_ops.len(), 1);
1466        if let DynOperation::AddField { table_name, .. } = &additional_ops[0] {
1467            assert_eq!(table_name, "table1");
1468        } else {
1469            panic!("Expected AddField operation");
1470        }
1471    }
1472
1473    #[test]
1474    fn get_foreign_key_dependencies_no_foreign_keys() {
1475        let operations = vec![DynOperation::CreateModel {
1476            table_name: "table1".to_string(),
1477            model_ty: parse_quote!(Table1),
1478            fields: vec![],
1479        }];
1480
1481        let external_dependencies = GeneratedMigration::get_foreign_key_dependencies(&operations);
1482        assert!(external_dependencies.is_empty());
1483    }
1484
1485    #[test]
1486    fn get_foreign_key_dependencies_with_foreign_keys() {
1487        let operations = vec![DynOperation::CreateModel {
1488            table_name: "table1".to_string(),
1489            model_ty: parse_quote!(Table1),
1490            fields: vec![Field {
1491                field_name: format_ident!("field1"),
1492                column_name: "field1".to_string(),
1493                ty: parse_quote!(ForeignKey<Table2>),
1494                auto_value: false,
1495                primary_key: false,
1496                unique: false,
1497                foreign_key: Some(ForeignKeySpec {
1498                    to_model: parse_quote!(crate::Table2),
1499                }),
1500            }],
1501        }];
1502
1503        let external_dependencies = GeneratedMigration::get_foreign_key_dependencies(&operations);
1504        assert_eq!(external_dependencies.len(), 1);
1505        assert_eq!(
1506            external_dependencies[0],
1507            DynDependency::Model {
1508                model_type: parse_quote!(crate::Table2),
1509            }
1510        );
1511    }
1512
1513    #[test]
1514    fn get_foreign_key_dependencies_with_multiple_foreign_keys() {
1515        let operations = vec![
1516            DynOperation::CreateModel {
1517                table_name: "table1".to_string(),
1518                model_ty: parse_quote!(Table1),
1519                fields: vec![Field {
1520                    field_name: format_ident!("field1"),
1521                    column_name: "field1".to_string(),
1522                    ty: parse_quote!(ForeignKey<Table2>),
1523                    auto_value: false,
1524                    primary_key: false,
1525                    unique: false,
1526                    foreign_key: Some(ForeignKeySpec {
1527                        to_model: parse_quote!(my_crate::Table2),
1528                    }),
1529                }],
1530            },
1531            DynOperation::CreateModel {
1532                table_name: "table3".to_string(),
1533                model_ty: parse_quote!(Table3),
1534                fields: vec![Field {
1535                    field_name: format_ident!("field2"),
1536                    column_name: "field2".to_string(),
1537                    ty: parse_quote!(ForeignKey<Table4>),
1538                    auto_value: false,
1539                    primary_key: false,
1540                    unique: false,
1541                    foreign_key: Some(ForeignKeySpec {
1542                        to_model: parse_quote!(crate::Table4),
1543                    }),
1544                }],
1545            },
1546        ];
1547
1548        let external_dependencies = GeneratedMigration::get_foreign_key_dependencies(&operations);
1549        assert_eq!(external_dependencies.len(), 2);
1550        assert!(external_dependencies.contains(&DynDependency::Model {
1551            model_type: parse_quote!(my_crate::Table2),
1552        }));
1553        assert!(external_dependencies.contains(&DynDependency::Model {
1554            model_type: parse_quote!(crate::Table4),
1555        }));
1556    }
1557
1558    #[test]
1559    fn make_add_field_operation() {
1560        let app_model = ModelInSource {
1561            model_item: parse_quote! {
1562                struct TestModel {
1563                    #[model(primary_key)]
1564                    id: i32,
1565                    field1: i32,
1566                }
1567            },
1568            model: Model {
1569                name: format_ident!("TestModel"),
1570                vis: syn::Visibility::Inherited,
1571                original_name: "TestModel".to_string(),
1572                resolved_ty: parse_quote!(TestModel),
1573                model_type: Default::default(),
1574                table_name: "test_model".to_string(),
1575                pk_field: Field {
1576                    field_name: format_ident!("id"),
1577                    column_name: "id".to_string(),
1578                    ty: parse_quote!(i32),
1579                    auto_value: true,
1580                    primary_key: true,
1581                    unique: false,
1582                    foreign_key: None,
1583                },
1584                fields: vec![],
1585            },
1586        };
1587
1588        let field = Field {
1589            field_name: format_ident!("new_field"),
1590            column_name: "new_field".to_string(),
1591            ty: parse_quote!(i32),
1592            auto_value: false,
1593            primary_key: false,
1594            unique: false,
1595            foreign_key: None,
1596        };
1597
1598        let operation = MigrationGenerator::make_add_field_operation(&app_model, &field);
1599
1600        match operation {
1601            DynOperation::AddField {
1602                table_name,
1603                model_ty,
1604                field: op_field,
1605            } => {
1606                assert_eq!(table_name, "test_model");
1607                assert_eq!(model_ty, parse_quote!(TestModel));
1608                assert_eq!(op_field.column_name, "new_field");
1609                assert_eq!(op_field.ty, parse_quote!(i32));
1610            }
1611            _ => panic!("Expected AddField operation"),
1612        }
1613    }
1614
1615    #[test]
1616    fn make_create_model_operation() {
1617        let app_model = ModelInSource {
1618            model_item: parse_quote! {
1619                struct TestModel {
1620                    #[model(primary_key)]
1621                    id: i32,
1622                    field1: i32,
1623                }
1624            },
1625            model: Model {
1626                name: format_ident!("TestModel"),
1627                vis: syn::Visibility::Inherited,
1628                original_name: "TestModel".to_string(),
1629                resolved_ty: parse_quote!(TestModel),
1630                model_type: Default::default(),
1631                table_name: "test_model".to_string(),
1632                pk_field: Field {
1633                    field_name: format_ident!("id"),
1634                    column_name: "id".to_string(),
1635                    ty: parse_quote!(i32),
1636                    auto_value: true,
1637                    primary_key: true,
1638                    unique: false,
1639                    foreign_key: None,
1640                },
1641                fields: vec![Field {
1642                    field_name: format_ident!("field1"),
1643                    column_name: "field1".to_string(),
1644                    ty: parse_quote!(i32),
1645                    auto_value: false,
1646                    primary_key: false,
1647                    unique: false,
1648                    foreign_key: None,
1649                }],
1650            },
1651        };
1652
1653        let operation = MigrationGenerator::make_create_model_operation(&app_model);
1654
1655        match operation {
1656            DynOperation::CreateModel {
1657                table_name,
1658                model_ty,
1659                fields,
1660            } => {
1661                assert_eq!(table_name, "test_model");
1662                assert_eq!(model_ty, parse_quote!(TestModel));
1663                assert_eq!(fields.len(), 1);
1664                assert_eq!(fields[0].column_name, "field1");
1665            }
1666            _ => panic!("Expected CreateModel operation"),
1667        }
1668    }
1669
1670    #[test]
1671    fn generate_operations_with_new_model() {
1672        let app_model = ModelInSource {
1673            model_item: parse_quote! {
1674                struct NewModel {
1675                    #[model(primary_key)]
1676                    id: i32,
1677                    name: String,
1678                }
1679            },
1680            model: Model {
1681                name: format_ident!("NewModel"),
1682                vis: syn::Visibility::Inherited,
1683                original_name: "NewModel".to_string(),
1684                resolved_ty: parse_quote!(NewModel),
1685                model_type: Default::default(),
1686                table_name: "new_model".to_string(),
1687                pk_field: Field {
1688                    field_name: format_ident!("id"),
1689                    column_name: "id".to_string(),
1690                    ty: parse_quote!(i32),
1691                    auto_value: true,
1692                    primary_key: true,
1693                    unique: false,
1694                    foreign_key: None,
1695                },
1696                fields: vec![Field {
1697                    field_name: format_ident!("name"),
1698                    column_name: "name".to_string(),
1699                    ty: parse_quote!(String),
1700                    auto_value: false,
1701                    primary_key: false,
1702                    unique: false,
1703                    foreign_key: None,
1704                }],
1705            },
1706        };
1707
1708        let app_models = vec![app_model.clone()];
1709        let migration_models = vec![];
1710
1711        let generator = MigrationGenerator::new(
1712            PathBuf::from("/fake/path/Cargo.toml"),
1713            "test_crate".to_string(),
1714            MigrationGeneratorOptions::default(),
1715        );
1716
1717        let (modified_models, operations) =
1718            generator.generate_operations(&app_models, &migration_models);
1719
1720        assert_eq!(modified_models.len(), 1);
1721        assert_eq!(operations.len(), 1);
1722
1723        match &operations[0] {
1724            DynOperation::CreateModel { table_name, .. } => {
1725                assert_eq!(table_name, "new_model");
1726            }
1727            _ => panic!("Expected CreateModel operation"),
1728        }
1729    }
1730
1731    #[test]
1732    fn generate_operations_with_modified_model() {
1733        let app_model = ModelInSource {
1734            model_item: parse_quote! {
1735                struct UserModel {
1736                    #[model(primary_key)]
1737                    id: i32,
1738                    name: String,
1739                    email: String,
1740                }
1741            },
1742            model: Model {
1743                name: format_ident!("UserModel"),
1744                vis: syn::Visibility::Inherited,
1745                original_name: "UserModel".to_string(),
1746                resolved_ty: parse_quote!(UserModel),
1747                model_type: Default::default(),
1748                table_name: "user_model".to_string(),
1749                pk_field: Field {
1750                    field_name: format_ident!("id"),
1751                    column_name: "id".to_string(),
1752                    ty: parse_quote!(i32),
1753                    auto_value: true,
1754                    primary_key: true,
1755                    unique: false,
1756                    foreign_key: None,
1757                },
1758                fields: vec![
1759                    Field {
1760                        field_name: format_ident!("name"),
1761                        column_name: "name".to_string(),
1762                        ty: parse_quote!(String),
1763                        auto_value: false,
1764                        primary_key: false,
1765                        unique: false,
1766                        foreign_key: None,
1767                    },
1768                    Field {
1769                        field_name: format_ident!("email"),
1770                        column_name: "email".to_string(),
1771                        ty: parse_quote!(String),
1772                        auto_value: false,
1773                        primary_key: false,
1774                        unique: false,
1775                        foreign_key: None,
1776                    },
1777                ],
1778            },
1779        };
1780
1781        let migration_model = ModelInSource {
1782            model_item: parse_quote! {
1783                struct UserModel {
1784                    #[model(primary_key)]
1785                    id: i32,
1786                    name: String,
1787                }
1788            },
1789            model: Model {
1790                name: format_ident!("UserModel"),
1791                vis: syn::Visibility::Inherited,
1792                original_name: "UserModel".to_string(),
1793                resolved_ty: parse_quote!(UserModel),
1794                model_type: Default::default(),
1795                table_name: "user_model".to_string(),
1796                pk_field: Field {
1797                    field_name: format_ident!("id"),
1798                    column_name: "id".to_string(),
1799                    ty: parse_quote!(i32),
1800                    auto_value: true,
1801                    primary_key: true,
1802                    unique: false,
1803                    foreign_key: None,
1804                },
1805                fields: vec![Field {
1806                    field_name: format_ident!("name"),
1807                    column_name: "name".to_string(),
1808                    ty: parse_quote!(String),
1809                    auto_value: false,
1810                    primary_key: false,
1811                    unique: false,
1812                    foreign_key: None,
1813                }],
1814            },
1815        };
1816
1817        let app_models = vec![app_model.clone()];
1818        let migration_models = vec![migration_model.clone()];
1819
1820        let generator = MigrationGenerator::new(
1821            PathBuf::from("/fake/path/Cargo.toml"),
1822            "test_crate".to_string(),
1823            MigrationGeneratorOptions::default(),
1824        );
1825
1826        let (modified_models, operations) =
1827            generator.generate_operations(&app_models, &migration_models);
1828
1829        assert_eq!(modified_models.len(), 1);
1830        assert!(operations.len() > 0, "Expected at least one operation");
1831
1832        let has_add_field = operations.iter().any(|op| match op {
1833            DynOperation::AddField { field, .. } => field.column_name == "email",
1834            _ => false,
1835        });
1836
1837        assert!(has_add_field, "Expected an AddField operation for 'email'");
1838    }
1839
1840    #[test]
1841    fn get_migration_list() {
1842        let tempdir = tempfile::tempdir().unwrap();
1843        let migrations_dir = tempdir.path().join("migrations");
1844        std::fs::create_dir(&migrations_dir).unwrap();
1845
1846        File::create(migrations_dir.join("m_0001_initial.rs")).unwrap();
1847        File::create(migrations_dir.join("m_0002_auto.rs")).unwrap();
1848        File::create(migrations_dir.join("dummy.rs")).unwrap();
1849        File::create(migrations_dir.join("m_0003_not_rust_file.txt")).unwrap();
1850
1851        let migration_list = MigrationGenerator::get_migration_list(&migrations_dir).unwrap();
1852        assert_eq!(
1853            migration_list.len(),
1854            2,
1855            "Migration list: {migration_list:?}"
1856        );
1857        assert!(migration_list.contains(&"m_0001_initial".to_string()));
1858        assert!(migration_list.contains(&"m_0002_auto".to_string()));
1859    }
1860
1861    #[test]
1862    fn get_migration_module_contents() {
1863        let contents = MigrationGenerator::get_migration_module_contents(&[
1864            "m_0001_initial".to_string(),
1865            "m_0002_auto".to_string(),
1866        ]);
1867
1868        let expected = quote! {
1869            pub mod m_0001_initial;
1870            pub mod m_0002_auto;
1871
1872            /// The list of migrations for current app.
1873            pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[
1874                &m_0001_initial::Migration,
1875                &m_0002_auto::Migration
1876            ];
1877        };
1878
1879        assert_eq!(contents.to_string(), expected.to_string());
1880    }
1881}