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