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