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