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