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