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