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