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