Skip to main content

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