1use std::env;
4use std::fmt::Write;
5use std::path::PathBuf;
6
7use proc_macro::TokenStream;
8use syn::parse::{Parse, ParseStream};
9use syn::{Ident, Token};
10use vespertide_loader::{load_config_or_default, load_migrations_at_compile_time};
11use vespertide_planner::apply_action;
12use vespertide_query::{DatabaseBackend, build_plan_queries};
13
14struct MacroInput {
15 pool: proc_macro2::TokenStream,
16 version_table: Option<String>,
17 verbose: bool,
18}
19
20impl Parse for MacroInput {
21 fn parse(input: ParseStream) -> syn::Result<Self> {
22 let mut pool_tokens = Vec::new();
23 while !input.is_empty() && !input.peek(Token![,]) {
24 pool_tokens.push(input.parse::<proc_macro2::TokenTree>()?);
25 }
26 let pool: proc_macro2::TokenStream = pool_tokens.into_iter().collect();
27 let mut version_table = None;
28 let mut verbose = false;
29
30 while !input.is_empty() {
31 input.parse::<Token![,]>()?;
32 if input.is_empty() {
33 break;
34 }
35
36 let key: Ident = input.parse()?;
37 if key == "version_table" {
38 input.parse::<Token![=]>()?;
39 let value: syn::LitStr = input.parse()?;
40 version_table = Some(value.value());
41 } else if key == "verbose" {
42 verbose = true;
43 } else {
44 return Err(syn::Error::new(
45 key.span(),
46 "unsupported option for vespertide_migration!",
47 ));
48 }
49 }
50
51 Ok(MacroInput {
52 pool,
53 version_table,
54 verbose,
55 })
56 }
57}
58
59#[derive(Debug)]
61pub(crate) struct MigrationBlock {
62 pub version: u32,
64 pub migration_id: String,
66 pub comment: String,
68 pub pg_sqls: Vec<String>,
70 pub mysql_sqls: Vec<String>,
72 pub sqlite_sqls: Vec<String>,
74}
75
76pub(crate) fn build_migration_block(
77 migration: &vespertide_core::MigrationPlan,
78 baseline_schema: &mut Vec<vespertide_core::TableDef>,
79) -> Result<MigrationBlock, String> {
80 let version = migration.version;
81
82 let queries = build_plan_queries(migration, baseline_schema).map_err(|e| {
84 format!(
85 "Failed to build queries for migration version {}: {}",
86 version, e
87 )
88 })?;
89
90 for action in &migration.actions {
92 let _ = apply_action(baseline_schema, action);
93 }
94
95 let mut pg_sqls = Vec::new();
97 let mut mysql_sqls = Vec::new();
98 let mut sqlite_sqls = Vec::new();
99
100 for q in &queries {
101 for stmt in &q.postgres {
102 pg_sqls.push(stmt.build(DatabaseBackend::Postgres));
103 }
104 for stmt in &q.mysql {
105 mysql_sqls.push(stmt.build(DatabaseBackend::MySql));
106 }
107 for stmt in &q.sqlite {
108 sqlite_sqls.push(stmt.build(DatabaseBackend::Sqlite));
109 }
110 }
111
112 let comment = migration.comment.as_deref().unwrap_or("").to_string();
113
114 Ok(MigrationBlock {
115 version,
116 migration_id: migration.id.clone(),
117 comment,
118 pg_sqls,
119 mysql_sqls,
120 sqlite_sqls,
121 })
122}
123
124fn generate_migration_code(
125 pool: &proc_macro2::TokenStream,
126 version_table: &str,
127 migration_blocks: Vec<MigrationBlock>,
128 verbose: bool,
129) -> proc_macro2::TokenStream {
130 let pool_str = pool.to_string();
133 let mut code = String::with_capacity(1_048_576);
134
135 code.push_str("{\n");
136
137 for b in &migration_blocks {
139 write_sql_blob(&mut code, &format!("__V{}_PG", b.version), &b.pg_sqls);
140 write_sql_blob(&mut code, &format!("__V{}_MYSQL", b.version), &b.mysql_sqls);
141 write_sql_blob(
142 &mut code,
143 &format!("__V{}_SQLITE", b.version),
144 &b.sqlite_sqls,
145 );
146 }
147
148 code.push_str(
149 "static __VESPERTIDE_MIGRATIONS: &[::vespertide::runtime::EmbeddedMigration] = &[\n",
150 );
151 for b in &migration_blocks {
152 writeln!(code, "::vespertide::runtime::EmbeddedMigration::new({}u32, {:?}, {:?}, __V{}_PG, __V{}_MYSQL, __V{}_SQLITE),", b.version, b.migration_id, b.comment, b.version, b.version, b.version).unwrap();
153 }
154 code.push_str("];\n");
155
156 code.push_str("async {\n");
157 writeln!(code, "let __pool = &{pool_str};").unwrap();
158 writeln!(code, "::vespertide::runtime::run_embedded_migrations(__pool, {version_table:?}, {verbose}, __VESPERTIDE_MIGRATIONS).await").unwrap();
159 code.push_str("}\n"); code.push_str("}\n"); code.parse().unwrap_or_else(|e| panic!("vespertide_migration codegen produced invalid Rust:\n{e}\n\nGenerated code (first 2000 chars):\n{}", &code[..code.len().min(2000)]))
164}
165
166fn write_sql_blob(code: &mut String, ident: &str, sqls: &[String]) {
168 let mut blob = String::new();
169 for sql in sqls {
170 blob.push_str(sql);
171 blob.push('\0');
172 }
173
174 writeln!(code, "static {ident}: &str = {blob:?};").unwrap();
175}
176
177pub(crate) fn vespertide_migration_impl(
179 input: proc_macro2::TokenStream,
180) -> proc_macro2::TokenStream {
181 let input: MacroInput = match syn::parse2(input) {
182 Ok(input) => input,
183 Err(e) => return e.to_compile_error(),
184 };
185 let pool = &input.pool;
186 let verbose = input.verbose;
187
188 let project_root = match env::var("CARGO_MANIFEST_DIR") {
190 Ok(dir) => Some(PathBuf::from(dir)),
191 Err(_) => None,
192 };
193
194 let config = match load_config_or_default(project_root) {
196 Ok(config) => config,
197 #[cfg(not(tarpaulin_include))]
198 Err(e) => {
199 return syn::Error::new(
200 proc_macro2::Span::call_site(),
201 format!("Failed to load config at compile time: {}", e),
202 )
203 .to_compile_error();
204 }
205 };
206 let prefix = config.prefix();
207
208 let version_table = input
210 .version_table
211 .map(|vt| config.apply_prefix(&vt))
212 .unwrap_or_else(|| config.apply_prefix("vespertide_version"));
213
214 let migrations = match load_migrations_at_compile_time() {
216 Ok(migrations) => migrations,
217 Err(e) => {
218 return syn::Error::new(
219 proc_macro2::Span::call_site(),
220 format!("Failed to load migrations at compile time: {}", e),
221 )
222 .to_compile_error();
223 }
224 };
225 let mut baseline_schema = Vec::new();
227 let mut migration_blocks = Vec::new();
228
229 #[cfg(not(tarpaulin_include))]
230 for migration in &migrations {
231 let prefixed_migration = migration.clone().with_prefix(prefix);
233 match build_migration_block(&prefixed_migration, &mut baseline_schema) {
234 Ok(block) => migration_blocks.push(block),
235 Err(e) => {
236 return syn::Error::new(proc_macro2::Span::call_site(), e).to_compile_error();
237 }
238 }
239 }
240
241 generate_migration_code(pool, &version_table, migration_blocks, verbose)
242}
243
244#[cfg(not(tarpaulin_include))]
246#[proc_macro]
247pub fn vespertide_migration(input: TokenStream) -> TokenStream {
248 vespertide_migration_impl(input.into()).into()
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use std::fs::File;
255 use std::io::Write;
256 use tempfile::tempdir;
257 use vespertide_core::{
258 ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, StrOrBoolOrArray,
259 };
260
261 #[test]
262 fn test_macro_expansion_with_runtime_macros() {
263 let dir = tempdir().unwrap();
265
266 let test_file_path = dir.path().join("test_macro.rs");
268 let mut test_file = File::create(&test_file_path).unwrap();
269 writeln!(
270 test_file,
271 r#"vespertide_migration!(pool, version_table = "test_versions");"#
272 )
273 .unwrap();
274
275 let file = File::open(&test_file_path).unwrap();
277 let result = runtime_macros::emulate_functionlike_macro_expansion(
278 file,
279 &[("vespertide_migration", vespertide_migration_impl)],
280 );
281
282 assert!(result.is_ok() || result.is_err());
286 }
287
288 #[test]
289 fn test_macro_with_simple_pool() {
290 let dir = tempdir().unwrap();
291 let test_file_path = dir.path().join("test_simple.rs");
292 let mut test_file = File::create(&test_file_path).unwrap();
293 writeln!(test_file, r#"vespertide_migration!(db_pool);"#).unwrap();
294
295 let file = File::open(&test_file_path).unwrap();
296 let result = runtime_macros::emulate_functionlike_macro_expansion(
297 file,
298 &[("vespertide_migration", vespertide_migration_impl)],
299 );
300
301 assert!(result.is_ok() || result.is_err());
302 }
303
304 #[test]
305 fn test_macro_parsing_invalid_option() {
306 let input: proc_macro2::TokenStream = "pool, invalid_option = \"value\"".parse().unwrap();
308 let output = vespertide_migration_impl(input);
309 let output_str = output.to_string();
310 assert!(output_str.contains("unsupported option"));
312 }
313
314 #[test]
315 fn test_macro_parsing_valid_input() {
316 let input: proc_macro2::TokenStream = "my_pool".parse().unwrap();
320 let output = vespertide_migration_impl(input);
321 let output_str = output.to_string();
322 assert!(!output_str.is_empty());
324 assert!(
327 output_str.contains("async") || output_str.contains("Failed to load"),
328 "Unexpected output: {}",
329 output_str
330 );
331 }
332
333 #[test]
334 fn test_macro_parsing_with_version_table() {
335 let input: proc_macro2::TokenStream =
336 r#"pool, version_table = "custom_versions""#.parse().unwrap();
337 let output = vespertide_migration_impl(input);
338 let output_str = output.to_string();
339 assert!(!output_str.is_empty());
340 }
341
342 #[test]
343 fn test_macro_parsing_trailing_comma() {
344 let input: proc_macro2::TokenStream = "pool,".parse().unwrap();
345 let output = vespertide_migration_impl(input);
346 let output_str = output.to_string();
347 assert!(!output_str.is_empty());
348 }
349
350 fn test_column(name: &str) -> ColumnDef {
351 ColumnDef {
352 name: name.into(),
353 r#type: ColumnType::Simple(SimpleColumnType::Integer),
354 nullable: false,
355 default: None,
356 comment: None,
357 primary_key: None,
358 unique: None,
359 index: None,
360 foreign_key: None,
361 }
362 }
363
364 fn block_to_string(block: &MigrationBlock) -> String {
366 let mut result = String::new();
367 for sql in &block.pg_sqls {
368 result.push_str(sql);
369 result.push(' ');
370 }
371 for sql in &block.mysql_sqls {
372 result.push_str(sql);
373 result.push(' ');
374 }
375 for sql in &block.sqlite_sqls {
376 result.push_str(sql);
377 result.push(' ');
378 }
379 result
380 }
381
382 #[test]
383 fn test_build_migration_block_create_table() {
384 let migration = MigrationPlan {
385 id: String::new(),
386 version: 1,
387 comment: None,
388 created_at: None,
389 actions: vec![MigrationAction::CreateTable {
390 table: "users".into(),
391 columns: vec![test_column("id")],
392 constraints: vec![],
393 }],
394 };
395
396 let mut baseline = Vec::new();
397 let result = build_migration_block(&migration, &mut baseline);
398
399 assert!(result.is_ok());
400 let block = result.unwrap();
401 let block_str = block_to_string(&block);
402
403 assert!(block_str.contains("CREATE TABLE"));
405 assert_eq!(block.version, 1);
406
407 assert_eq!(baseline.len(), 1);
409 assert_eq!(baseline[0].name, "users");
410 }
411
412 #[test]
413 fn test_build_migration_block_add_column() {
414 let create_migration = MigrationPlan {
416 id: String::new(),
417 version: 1,
418 comment: None,
419 created_at: None,
420 actions: vec![MigrationAction::CreateTable {
421 table: "users".into(),
422 columns: vec![test_column("id")],
423 constraints: vec![],
424 }],
425 };
426
427 let mut baseline = Vec::new();
428 let _ = build_migration_block(&create_migration, &mut baseline);
429
430 let add_column_migration = MigrationPlan {
432 id: String::new(),
433 version: 2,
434 comment: None,
435 created_at: None,
436 actions: vec![MigrationAction::AddColumn {
437 table: "users".into(),
438 column: Box::new(ColumnDef {
439 name: "email".into(),
440 r#type: ColumnType::Simple(SimpleColumnType::Text),
441 nullable: true,
442 default: None,
443 comment: None,
444 primary_key: None,
445 unique: None,
446 index: None,
447 foreign_key: None,
448 }),
449 fill_with: None,
450 }],
451 };
452
453 let result = build_migration_block(&add_column_migration, &mut baseline);
454 assert!(result.is_ok());
455 let block = result.unwrap();
456 let block_str = block_to_string(&block);
457
458 assert_eq!(block.version, 2);
459 assert!(block_str.contains("ALTER TABLE"));
460 assert!(block_str.contains("ADD COLUMN"));
461 }
462
463 #[test]
464 fn test_build_migration_block_multiple_actions() {
465 let migration = MigrationPlan {
466 id: String::new(),
467 version: 1,
468 comment: None,
469 created_at: None,
470 actions: vec![
471 MigrationAction::CreateTable {
472 table: "users".into(),
473 columns: vec![test_column("id")],
474 constraints: vec![],
475 },
476 MigrationAction::CreateTable {
477 table: "posts".into(),
478 columns: vec![test_column("id")],
479 constraints: vec![],
480 },
481 ],
482 };
483
484 let mut baseline = Vec::new();
485 let result = build_migration_block(&migration, &mut baseline);
486
487 assert!(result.is_ok());
488 assert_eq!(baseline.len(), 2);
489 }
490
491 #[test]
492 fn test_generate_migration_code() {
493 let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap();
494 let version_table = "test_versions";
495
496 let migration = MigrationPlan {
498 id: String::new(),
499 version: 1,
500 comment: None,
501 created_at: None,
502 actions: vec![MigrationAction::CreateTable {
503 table: "users".into(),
504 columns: vec![test_column("id")],
505 constraints: vec![],
506 }],
507 };
508
509 let mut baseline = Vec::new();
510 let block = build_migration_block(&migration, &mut baseline).unwrap();
511
512 let generated = generate_migration_code(&pool, version_table, vec![block], false);
513 let generated_str = generated.to_string();
514
515 assert!(generated_str.contains("async"));
517 assert!(generated_str.contains("db_pool"));
518 assert!(generated_str.contains("test_versions"));
519 assert!(generated_str.contains("run_embedded_migrations"));
520 assert!(generated_str.contains("EmbeddedMigration"));
521 assert!(generated_str.contains("1u32"));
522 }
523
524 #[test]
525 fn test_generate_migration_code_empty_migrations() {
526 let pool: proc_macro2::TokenStream = "pool".parse().unwrap();
527 let version_table = "vespertide_version";
528
529 let generated = generate_migration_code(&pool, version_table, vec![], false);
530 let generated_str = generated.to_string();
531
532 assert!(generated_str.contains("async"));
534 assert!(generated_str.contains("vespertide_version"));
535 }
536
537 #[test]
538 fn test_generate_migration_code_multiple_blocks() {
539 let pool: proc_macro2::TokenStream = "connection".parse().unwrap();
540
541 let mut baseline = Vec::new();
542
543 let migration1 = MigrationPlan {
544 id: String::new(),
545 version: 1,
546 comment: None,
547 created_at: None,
548 actions: vec![MigrationAction::CreateTable {
549 table: "users".into(),
550 columns: vec![test_column("id")],
551 constraints: vec![],
552 }],
553 };
554 let block1 = build_migration_block(&migration1, &mut baseline).unwrap();
555
556 let migration2 = MigrationPlan {
557 id: String::new(),
558 version: 2,
559 comment: None,
560 created_at: None,
561 actions: vec![MigrationAction::CreateTable {
562 table: "posts".into(),
563 columns: vec![test_column("id")],
564 constraints: vec![],
565 }],
566 };
567 let block2 = build_migration_block(&migration2, &mut baseline).unwrap();
568
569 let generated = generate_migration_code(&pool, "migrations", vec![block1, block2], false);
570 let generated_str = generated.to_string();
571
572 assert!(generated_str.contains("1u32"));
574 assert!(generated_str.contains("2u32"));
575 assert!(generated_str.contains("__VESPERTIDE_MIGRATIONS"));
576 }
577
578 #[test]
579 fn test_generate_migration_code_delegates_runtime_execution() {
580 let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap();
581
582 let migration = MigrationPlan {
583 id: String::new(),
584 version: 1,
585 comment: Some("initial".into()),
586 created_at: None,
587 actions: vec![MigrationAction::CreateTable {
588 table: "users".into(),
589 columns: vec![test_column("id")],
590 constraints: vec![],
591 }],
592 };
593
594 let mut baseline = Vec::new();
595 let block = build_migration_block(&migration, &mut baseline).unwrap();
596
597 let generated = generate_migration_code(&pool, "vespertide_version", vec![block], false);
598 let generated_str = generated.to_string();
599
600 assert!(generated_str.contains("run_embedded_migrations"));
601 assert!(generated_str.contains("EmbeddedMigration"));
602 assert!(!generated_str.contains("SELECT MAX"));
603 assert!(!generated_str.contains("execute_raw"));
604 }
605
606 #[test]
607 fn test_build_migration_block_generates_all_backends() {
608 let migration = MigrationPlan {
609 id: String::new(),
610 version: 1,
611 comment: None,
612 created_at: None,
613 actions: vec![MigrationAction::CreateTable {
614 table: "test_table".into(),
615 columns: vec![test_column("id")],
616 constraints: vec![],
617 }],
618 };
619
620 let mut baseline = Vec::new();
621 let result = build_migration_block(&migration, &mut baseline);
622 assert!(result.is_ok());
623
624 let block = result.unwrap();
625
626 assert!(
628 !block.pg_sqls.is_empty(),
629 "PostgreSQL SQL should not be empty"
630 );
631 assert!(
632 !block.mysql_sqls.is_empty(),
633 "MySQL SQL should not be empty"
634 );
635 assert!(
636 !block.sqlite_sqls.is_empty(),
637 "SQLite SQL should not be empty"
638 );
639
640 assert!(block.pg_sqls.iter().any(|s| s.contains("CREATE TABLE")));
642 assert!(block.mysql_sqls.iter().any(|s| s.contains("CREATE TABLE")));
643 assert!(block.sqlite_sqls.iter().any(|s| s.contains("CREATE TABLE")));
644 }
645
646 #[test]
647 fn test_build_migration_block_with_delete_table() {
648 let create_migration = MigrationPlan {
650 id: String::new(),
651 version: 1,
652 comment: None,
653 created_at: None,
654 actions: vec![MigrationAction::CreateTable {
655 table: "temp_table".into(),
656 columns: vec![test_column("id")],
657 constraints: vec![],
658 }],
659 };
660
661 let mut baseline = Vec::new();
662 let _ = build_migration_block(&create_migration, &mut baseline);
663 assert_eq!(baseline.len(), 1);
664
665 let delete_migration = MigrationPlan {
667 id: String::new(),
668 version: 2,
669 comment: None,
670 created_at: None,
671 actions: vec![MigrationAction::DeleteTable {
672 table: "temp_table".into(),
673 }],
674 };
675
676 let result = build_migration_block(&delete_migration, &mut baseline);
677 assert!(result.is_ok());
678 let block = result.unwrap();
679 assert!(block.pg_sqls.iter().any(|s| s.contains("DROP TABLE")));
680
681 assert_eq!(baseline.len(), 0);
683 }
684
685 #[test]
686 fn test_build_migration_block_with_index() {
687 let migration = MigrationPlan {
688 id: String::new(),
689 version: 1,
690 comment: None,
691 created_at: None,
692 actions: vec![MigrationAction::CreateTable {
693 table: "users".into(),
694 columns: vec![
695 test_column("id"),
696 ColumnDef {
697 name: "email".into(),
698 r#type: ColumnType::Simple(SimpleColumnType::Text),
699 nullable: true,
700 default: None,
701 comment: None,
702 primary_key: None,
703 unique: None,
704 index: Some(StrOrBoolOrArray::Bool(true)),
705 foreign_key: None,
706 },
707 ],
708 constraints: vec![],
709 }],
710 };
711
712 let mut baseline = Vec::new();
713 let result = build_migration_block(&migration, &mut baseline);
714 assert!(result.is_ok());
715
716 let table = &baseline[0];
718 let normalized = table.clone().normalize();
719 assert!(normalized.is_ok());
720 }
721
722 #[test]
723 fn test_build_migration_block_error_nonexistent_table() {
724 let migration = MigrationPlan {
726 id: String::new(),
727 version: 1,
728 comment: None,
729 created_at: None,
730 actions: vec![MigrationAction::AddColumn {
731 table: "nonexistent_table".into(),
732 column: Box::new(test_column("new_col")),
733 fill_with: None,
734 }],
735 };
736
737 let mut baseline = Vec::new();
738 let result = build_migration_block(&migration, &mut baseline);
739
740 assert!(result.is_err());
741 let err = result.unwrap_err();
742 assert!(err.contains("Failed to build queries for migration version 1"));
743 }
744
745 #[test]
746 fn test_vespertide_migration_impl_loading_error() {
747 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
749
750 unsafe {
752 std::env::remove_var("CARGO_MANIFEST_DIR");
753 }
754
755 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
756 let output = vespertide_migration_impl(input);
757 let output_str = output.to_string();
758
759 assert!(
761 output_str.contains("Failed to load migrations at compile time"),
762 "Expected loading error, got: {}",
763 output_str
764 );
765
766 if let Some(val) = original {
768 unsafe {
769 std::env::set_var("CARGO_MANIFEST_DIR", val);
770 }
771 }
772 }
773
774 #[test]
775 fn test_vespertide_migration_impl_with_valid_project() {
776 use std::fs;
777
778 let dir = tempdir().unwrap();
780 let project_dir = dir.path();
781
782 let config_content = r#"{
784 "modelsDir": "models",
785 "migrationsDir": "migrations",
786 "tableNamingCase": "snake",
787 "columnNamingCase": "snake",
788 "modelFormat": "json"
789 }"#;
790 fs::write(project_dir.join("vespertide.json"), config_content).unwrap();
791
792 fs::create_dir_all(project_dir.join("models")).unwrap();
794 fs::create_dir_all(project_dir.join("migrations")).unwrap();
795
796 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
798 unsafe {
799 std::env::set_var("CARGO_MANIFEST_DIR", project_dir);
800 }
801
802 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
803 let output = vespertide_migration_impl(input);
804 let output_str = output.to_string();
805
806 assert!(
808 output_str.contains("async"),
809 "Expected async block, got: {}",
810 output_str
811 );
812 assert!(
813 output_str.contains("run_embedded_migrations"),
814 "Expected runtime helper delegation, got: {}",
815 output_str
816 );
817
818 if let Some(val) = original {
820 unsafe {
821 std::env::set_var("CARGO_MANIFEST_DIR", val);
822 }
823 } else {
824 unsafe {
825 std::env::remove_var("CARGO_MANIFEST_DIR");
826 }
827 }
828 }
829
830 #[test]
831 fn test_build_migration_block_verbose_create_table() {
832 let migration = MigrationPlan {
833 id: String::new(),
834 version: 1,
835 comment: Some("initial setup".into()),
836 created_at: None,
837 actions: vec![MigrationAction::CreateTable {
838 table: "users".into(),
839 columns: vec![test_column("id")],
840 constraints: vec![],
841 }],
842 };
843
844 let mut baseline = Vec::new();
845 let result = build_migration_block(&migration, &mut baseline);
846
847 assert!(result.is_ok());
848 let block = result.unwrap();
849
850 assert_eq!(block.version, 1);
852 assert_eq!(block.comment, "initial setup");
853 assert!(block.pg_sqls.iter().any(|s| s.contains("CREATE TABLE")));
855 }
856
857 #[test]
858 fn test_build_migration_block_verbose_multiple_actions() {
859 let migration = MigrationPlan {
860 id: String::new(),
861 version: 1,
862 comment: None,
863 created_at: None,
864 actions: vec![
865 MigrationAction::CreateTable {
866 table: "users".into(),
867 columns: vec![test_column("id")],
868 constraints: vec![],
869 },
870 MigrationAction::CreateTable {
871 table: "posts".into(),
872 columns: vec![test_column("id")],
873 constraints: vec![],
874 },
875 ],
876 };
877
878 let mut baseline = Vec::new();
879 let result = build_migration_block(&migration, &mut baseline);
880
881 assert!(result.is_ok());
882 assert_eq!(baseline.len(), 2);
883 assert_eq!(result.as_ref().unwrap().version, 1);
885 }
886
887 #[test]
888 fn test_build_migration_block_verbose_add_column() {
889 let create = MigrationPlan {
891 id: String::new(),
892 version: 1,
893 comment: None,
894 created_at: None,
895 actions: vec![MigrationAction::CreateTable {
896 table: "users".into(),
897 columns: vec![test_column("id")],
898 constraints: vec![],
899 }],
900 };
901 let mut baseline = Vec::new();
902 let _ = build_migration_block(&create, &mut baseline);
903
904 let add_col = MigrationPlan {
906 id: String::new(),
907 version: 2,
908 comment: Some("add email".into()),
909 created_at: None,
910 actions: vec![MigrationAction::AddColumn {
911 table: "users".into(),
912 column: Box::new(ColumnDef {
913 name: "email".into(),
914 r#type: ColumnType::Simple(SimpleColumnType::Text),
915 nullable: true,
916 default: None,
917 comment: None,
918 primary_key: None,
919 unique: None,
920 index: None,
921 foreign_key: None,
922 }),
923 fill_with: None,
924 }],
925 };
926
927 let result = build_migration_block(&add_col, &mut baseline);
928 assert!(result.is_ok());
929 let block = result.unwrap();
930 assert_eq!(block.version, 2);
931 assert_eq!(block.comment, "add email");
932 assert!(block.pg_sqls.iter().any(|s| s.contains("ALTER TABLE")));
934 }
935
936 #[test]
937 fn test_generate_migration_code_verbose() {
938 let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap();
939 let version_table = "test_versions";
940
941 let migration = MigrationPlan {
942 id: String::new(),
943 version: 1,
944 comment: None,
945 created_at: None,
946 actions: vec![MigrationAction::CreateTable {
947 table: "users".into(),
948 columns: vec![test_column("id")],
949 constraints: vec![],
950 }],
951 };
952
953 let mut baseline = Vec::new();
954 let block = build_migration_block(&migration, &mut baseline).unwrap();
955
956 let generated = generate_migration_code(&pool, version_table, vec![block], true);
957 let generated_str = generated.to_string();
958
959 assert!(generated_str.contains("run_embedded_migrations"));
960 assert!(generated_str.contains("async"));
961 }
962
963 #[test]
964 fn test_macro_parsing_verbose_flag() {
965 let input: proc_macro2::TokenStream = "pool, verbose".parse().unwrap();
967 let output = vespertide_migration_impl(input);
968 let output_str = output.to_string();
969 assert!(!output_str.is_empty());
971 }
972
973 #[test]
974 fn test_vespertide_migration_impl_with_migrations() {
975 use std::fs;
976
977 let dir = tempdir().unwrap();
979 let project_dir = dir.path();
980
981 let config_content = r#"{
983 "modelsDir": "models",
984 "migrationsDir": "migrations",
985 "tableNamingCase": "snake",
986 "columnNamingCase": "snake",
987 "modelFormat": "json"
988 }"#;
989 fs::write(project_dir.join("vespertide.json"), config_content).unwrap();
990
991 fs::create_dir_all(project_dir.join("models")).unwrap();
993 fs::create_dir_all(project_dir.join("migrations")).unwrap();
994
995 let migration_content = r#"{
997 "version": 1,
998 "actions": [
999 {
1000 "type": "create_table",
1001 "table": "users",
1002 "columns": [
1003 {"name": "id", "type": "integer", "nullable": false}
1004 ],
1005 "constraints": []
1006 }
1007 ]
1008 }"#;
1009 fs::write(
1010 project_dir.join("migrations").join("0001_initial.json"),
1011 migration_content,
1012 )
1013 .unwrap();
1014
1015 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
1017 unsafe {
1018 std::env::set_var("CARGO_MANIFEST_DIR", project_dir);
1019 }
1020
1021 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
1022 let output = vespertide_migration_impl(input);
1023 let output_str = output.to_string();
1024
1025 assert!(
1027 output_str.contains("async"),
1028 "Expected async block, got: {}",
1029 output_str
1030 );
1031
1032 if let Some(val) = original {
1034 unsafe {
1035 std::env::set_var("CARGO_MANIFEST_DIR", val);
1036 }
1037 } else {
1038 unsafe {
1039 std::env::remove_var("CARGO_MANIFEST_DIR");
1040 }
1041 }
1042 }
1043
1044 #[test]
1045 fn test_vespertide_migration_impl_ignores_invalid_models() {
1046 use std::fs;
1047
1048 let dir = tempdir().unwrap();
1049 let project_dir = dir.path();
1050
1051 let config_content = r#"{
1052 "modelsDir": "models",
1053 "migrationsDir": "migrations",
1054 "tableNamingCase": "snake",
1055 "columnNamingCase": "snake",
1056 "modelFormat": "json"
1057 }"#;
1058 fs::write(project_dir.join("vespertide.json"), config_content).unwrap();
1059
1060 fs::create_dir_all(project_dir.join("models")).unwrap();
1061 fs::create_dir_all(project_dir.join("migrations")).unwrap();
1062
1063 fs::write(
1064 project_dir.join("models").join("broken.json"),
1065 r#"{
1066 "name": "broken",
1067 "columns": [
1068 {"name": "user_id", "type": "integer", "nullable": false, "foreign_key": "invalid_format"}
1069 ],
1070 "constraints": []
1071 }"#,
1072 )
1073 .unwrap();
1074
1075 fs::write(
1076 project_dir.join("migrations").join("0001_initial.json"),
1077 r#"{
1078 "version": 1,
1079 "actions": [
1080 {
1081 "type": "create_table",
1082 "table": "users",
1083 "columns": [
1084 {"name": "id", "type": "integer", "nullable": false}
1085 ],
1086 "constraints": []
1087 }
1088 ]
1089 }"#,
1090 )
1091 .unwrap();
1092
1093 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
1094 unsafe {
1095 std::env::set_var("CARGO_MANIFEST_DIR", project_dir);
1096 }
1097
1098 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
1099 let output = vespertide_migration_impl(input);
1100 let output_str = output.to_string();
1101
1102 assert!(
1103 output_str.contains("async"),
1104 "Expected migration code generation to ignore invalid models, got: {}",
1105 output_str
1106 );
1107
1108 if let Some(val) = original {
1109 unsafe {
1110 std::env::set_var("CARGO_MANIFEST_DIR", val);
1111 }
1112 } else {
1113 unsafe {
1114 std::env::remove_var("CARGO_MANIFEST_DIR");
1115 }
1116 }
1117 }
1118}