1use std::env;
4use std::path::PathBuf;
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::parse::{Parse, ParseStream};
9use syn::{Expr, Ident, Token};
10use vespertide_loader::{
11 load_config_or_default, load_migrations_at_compile_time, load_models_at_compile_time,
12};
13use vespertide_planner::apply_action;
14use vespertide_query::{DatabaseBackend, build_plan_queries};
15
16struct MacroInput {
17 pool: Expr,
18 version_table: Option<String>,
19 verbose: bool,
20}
21
22impl Parse for MacroInput {
23 fn parse(input: ParseStream) -> syn::Result<Self> {
24 let pool = input.parse()?;
25 let mut version_table = None;
26 let mut verbose = false;
27
28 while !input.is_empty() {
29 input.parse::<Token![,]>()?;
30 if input.is_empty() {
31 break;
32 }
33
34 let key: Ident = input.parse()?;
35 if key == "version_table" {
36 input.parse::<Token![=]>()?;
37 let value: syn::LitStr = input.parse()?;
38 version_table = Some(value.value());
39 } else if key == "verbose" {
40 verbose = true;
41 } else {
42 return Err(syn::Error::new(
43 key.span(),
44 "unsupported option for vespertide_migration!",
45 ));
46 }
47 }
48
49 Ok(MacroInput {
50 pool,
51 version_table,
52 verbose,
53 })
54 }
55}
56
57pub(crate) fn build_migration_block(
58 migration: &vespertide_core::MigrationPlan,
59 baseline_schema: &mut Vec<vespertide_core::TableDef>,
60 verbose: bool,
61) -> Result<proc_macro2::TokenStream, String> {
62 let version = migration.version;
63
64 let queries = build_plan_queries(migration, baseline_schema).map_err(|e| {
66 format!(
67 "Failed to build queries for migration version {}: {}",
68 version, e
69 )
70 })?;
71
72 for action in &migration.actions {
74 let _ = apply_action(baseline_schema, action);
75 }
76
77 let version_str = format!("v{}", version);
79 let comment_str = migration.comment.as_deref().unwrap_or("").to_string();
80
81 let block = if verbose {
82 let total_sql_count: usize = queries
84 .iter()
85 .map(|q| q.postgres.len().max(q.mysql.len()).max(q.sqlite.len()))
86 .sum();
87 let total_sql_count_lit = total_sql_count;
88
89 let mut action_blocks = Vec::new();
90 let mut global_idx: usize = 0;
91
92 for (action_idx, q) in queries.iter().enumerate() {
93 let action_desc = format!("{}", q.action);
94 let action_num = action_idx + 1;
95 let total_actions = queries.len();
96
97 let pg: Vec<String> = q
98 .postgres
99 .iter()
100 .map(|s| s.build(DatabaseBackend::Postgres))
101 .collect();
102 let mysql: Vec<String> = q
103 .mysql
104 .iter()
105 .map(|s| s.build(DatabaseBackend::MySql))
106 .collect();
107 let sqlite: Vec<String> = q
108 .sqlite
109 .iter()
110 .map(|s| s.build(DatabaseBackend::Sqlite))
111 .collect();
112
113 let sql_count = pg.len().max(mysql.len()).max(sqlite.len());
115 let mut sql_exec_blocks = Vec::new();
116
117 for i in 0..sql_count {
118 let idx = global_idx + i + 1;
119 let pg_sql = pg.get(i).cloned().unwrap_or_default();
120 let mysql_sql = mysql.get(i).cloned().unwrap_or_default();
121 let sqlite_sql = sqlite.get(i).cloned().unwrap_or_default();
122
123 sql_exec_blocks.push(quote! {
124 {
125 let sql: &str = match backend {
126 sea_orm::DatabaseBackend::Postgres => #pg_sql,
127 sea_orm::DatabaseBackend::MySql => #mysql_sql,
128 sea_orm::DatabaseBackend::Sqlite => #sqlite_sql,
129 _ => #pg_sql,
130 };
131 if !sql.is_empty() {
132 eprintln!("[vespertide] [{}/{}] {}", #idx, #total_sql_count_lit, sql);
133 let stmt = sea_orm::Statement::from_string(backend, sql);
134 __txn.execute_raw(stmt).await.map_err(|e| {
135 ::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e))
136 })?;
137 }
138 }
139 });
140 }
141 global_idx += sql_count;
142
143 action_blocks.push(quote! {
144 eprintln!("[vespertide] Action {}/{}: {}", #action_num, #total_actions, #action_desc);
145 #(#sql_exec_blocks)*
146 });
147 }
148
149 quote! {
150 if __version < #version {
151 eprintln!("[vespertide] Applying migration {} ({})", #version_str, #comment_str);
152 #(#action_blocks)*
153
154 let insert_sql = format!("INSERT INTO {q}{}{q} (version) VALUES ({})", __version_table, #version);
155 let stmt = sea_orm::Statement::from_string(backend, insert_sql);
156 __txn.execute_raw(stmt).await.map_err(|e| {
157 ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e))
158 })?;
159
160 eprintln!("[vespertide] Migration {} applied successfully", #version_str);
161 }
162 }
163 } else {
164 let mut pg_sqls = Vec::new();
166 let mut mysql_sqls = Vec::new();
167 let mut sqlite_sqls = Vec::new();
168
169 for q in &queries {
170 for stmt in &q.postgres {
171 pg_sqls.push(stmt.build(DatabaseBackend::Postgres));
172 }
173 for stmt in &q.mysql {
174 mysql_sqls.push(stmt.build(DatabaseBackend::MySql));
175 }
176 for stmt in &q.sqlite {
177 sqlite_sqls.push(stmt.build(DatabaseBackend::Sqlite));
178 }
179 }
180
181 quote! {
182 if __version < #version {
183 let sqls: &[&str] = match backend {
184 sea_orm::DatabaseBackend::Postgres => &[#(#pg_sqls),*],
185 sea_orm::DatabaseBackend::MySql => &[#(#mysql_sqls),*],
186 sea_orm::DatabaseBackend::Sqlite => &[#(#sqlite_sqls),*],
187 _ => &[#(#pg_sqls),*],
188 };
189
190 for sql in sqls {
191 if !sql.is_empty() {
192 let stmt = sea_orm::Statement::from_string(backend, *sql);
193 __txn.execute_raw(stmt).await.map_err(|e| {
194 ::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e))
195 })?;
196 }
197 }
198
199 let insert_sql = format!("INSERT INTO {q}{}{q} (version) VALUES ({})", __version_table, #version);
200 let stmt = sea_orm::Statement::from_string(backend, insert_sql);
201 __txn.execute_raw(stmt).await.map_err(|e| {
202 ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e))
203 })?;
204 }
205 }
206 };
207
208 Ok(block)
209}
210
211fn generate_migration_code(
212 pool: &Expr,
213 version_table: &str,
214 migration_blocks: Vec<proc_macro2::TokenStream>,
215 verbose: bool,
216) -> proc_macro2::TokenStream {
217 let verbose_current_version = if verbose {
218 quote! {
219 eprintln!("[vespertide] Current database version: {}", __version);
220 }
221 } else {
222 quote! {}
223 };
224
225 quote! {
226 async {
227 use sea_orm::{ConnectionTrait, TransactionTrait};
228 let __pool = &#pool;
229 let __version_table = #version_table;
230 let backend = __pool.get_database_backend();
231 let q = if matches!(backend, sea_orm::DatabaseBackend::MySql) { '`' } else { '"' };
232
233 let create_table_sql = format!(
235 "CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)",
236 __version_table
237 );
238 let stmt = sea_orm::Statement::from_string(backend, create_table_sql);
239 __pool.execute_raw(stmt).await.map_err(|e| {
240 ::vespertide::MigrationError::DatabaseError(format!("Failed to create version table: {}", e))
241 })?;
242
243 let __txn = __pool.begin().await.map_err(|e| {
247 ::vespertide::MigrationError::DatabaseError(format!("Failed to begin transaction: {}", e))
248 })?;
249
250 let select_sql = format!("SELECT MAX(version) as version FROM {q}{}{q}", __version_table);
252 let stmt = sea_orm::Statement::from_string(backend, select_sql);
253 let version_result = __txn.query_one_raw(stmt).await.map_err(|e| {
254 ::vespertide::MigrationError::DatabaseError(format!("Failed to read version: {}", e))
255 })?;
256
257 let __version = version_result
258 .and_then(|row| row.try_get::<i32>("", "version").ok())
259 .unwrap_or(0) as u32;
260
261 #verbose_current_version
262
263 #(#migration_blocks)*
265
266 __txn.commit().await.map_err(|e| {
268 ::vespertide::MigrationError::DatabaseError(format!("Failed to commit transaction: {}", e))
269 })?;
270
271 Ok::<(), ::vespertide::MigrationError>(())
272 }
273 }
274}
275
276pub(crate) fn vespertide_migration_impl(
278 input: proc_macro2::TokenStream,
279) -> proc_macro2::TokenStream {
280 let input: MacroInput = match syn::parse2(input) {
281 Ok(input) => input,
282 Err(e) => return e.to_compile_error(),
283 };
284 let pool = &input.pool;
285 let verbose = input.verbose;
286
287 let project_root = match env::var("CARGO_MANIFEST_DIR") {
289 Ok(dir) => Some(PathBuf::from(dir)),
290 Err(_) => None,
291 };
292
293 let config = match load_config_or_default(project_root) {
295 Ok(config) => config,
296 #[cfg(not(tarpaulin_include))]
297 Err(e) => {
298 return syn::Error::new(
299 proc_macro2::Span::call_site(),
300 format!("Failed to load config at compile time: {}", e),
301 )
302 .to_compile_error();
303 }
304 };
305 let prefix = config.prefix();
306
307 let version_table = input
309 .version_table
310 .map(|vt| config.apply_prefix(&vt))
311 .unwrap_or_else(|| config.apply_prefix("vespertide_version"));
312
313 let migrations = match load_migrations_at_compile_time() {
315 Ok(migrations) => migrations,
316 Err(e) => {
317 return syn::Error::new(
318 proc_macro2::Span::call_site(),
319 format!("Failed to load migrations at compile time: {}", e),
320 )
321 .to_compile_error();
322 }
323 };
324 let _models = match load_models_at_compile_time() {
325 Ok(models) => models,
326 #[cfg(not(tarpaulin_include))]
327 Err(e) => {
328 return syn::Error::new(
329 proc_macro2::Span::call_site(),
330 format!("Failed to load models at compile time: {}", e),
331 )
332 .to_compile_error();
333 }
334 };
335
336 let mut baseline_schema = Vec::new();
338 let mut migration_blocks = Vec::new();
339
340 #[cfg(not(tarpaulin_include))]
341 for migration in &migrations {
342 let prefixed_migration = migration.clone().with_prefix(prefix);
344 match build_migration_block(&prefixed_migration, &mut baseline_schema, verbose) {
345 Ok(block) => migration_blocks.push(block),
346 Err(e) => {
347 return syn::Error::new(proc_macro2::Span::call_site(), e).to_compile_error();
348 }
349 }
350 }
351
352 generate_migration_code(pool, &version_table, migration_blocks, verbose)
353}
354
355#[cfg(not(tarpaulin_include))]
357#[proc_macro]
358pub fn vespertide_migration(input: TokenStream) -> TokenStream {
359 vespertide_migration_impl(input.into()).into()
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use std::fs::File;
366 use std::io::Write;
367 use tempfile::tempdir;
368 use vespertide_core::{
369 ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, StrOrBoolOrArray,
370 };
371
372 #[test]
373 fn test_macro_expansion_with_runtime_macros() {
374 let dir = tempdir().unwrap();
376
377 let test_file_path = dir.path().join("test_macro.rs");
379 let mut test_file = File::create(&test_file_path).unwrap();
380 writeln!(
381 test_file,
382 r#"vespertide_migration!(pool, version_table = "test_versions");"#
383 )
384 .unwrap();
385
386 let file = File::open(&test_file_path).unwrap();
388 let result = runtime_macros::emulate_functionlike_macro_expansion(
389 file,
390 &[("vespertide_migration", vespertide_migration_impl)],
391 );
392
393 assert!(result.is_ok() || result.is_err());
397 }
398
399 #[test]
400 fn test_macro_with_simple_pool() {
401 let dir = tempdir().unwrap();
402 let test_file_path = dir.path().join("test_simple.rs");
403 let mut test_file = File::create(&test_file_path).unwrap();
404 writeln!(test_file, r#"vespertide_migration!(db_pool);"#).unwrap();
405
406 let file = File::open(&test_file_path).unwrap();
407 let result = runtime_macros::emulate_functionlike_macro_expansion(
408 file,
409 &[("vespertide_migration", vespertide_migration_impl)],
410 );
411
412 assert!(result.is_ok() || result.is_err());
413 }
414
415 #[test]
416 fn test_macro_parsing_invalid_option() {
417 let input: proc_macro2::TokenStream = "pool, invalid_option = \"value\"".parse().unwrap();
419 let output = vespertide_migration_impl(input);
420 let output_str = output.to_string();
421 assert!(output_str.contains("unsupported option"));
423 }
424
425 #[test]
426 fn test_macro_parsing_valid_input() {
427 let input: proc_macro2::TokenStream = "my_pool".parse().unwrap();
431 let output = vespertide_migration_impl(input);
432 let output_str = output.to_string();
433 assert!(!output_str.is_empty());
435 assert!(
438 output_str.contains("async") || output_str.contains("Failed to load"),
439 "Unexpected output: {}",
440 output_str
441 );
442 }
443
444 #[test]
445 fn test_macro_parsing_with_version_table() {
446 let input: proc_macro2::TokenStream =
447 r#"pool, version_table = "custom_versions""#.parse().unwrap();
448 let output = vespertide_migration_impl(input);
449 let output_str = output.to_string();
450 assert!(!output_str.is_empty());
451 }
452
453 #[test]
454 fn test_macro_parsing_trailing_comma() {
455 let input: proc_macro2::TokenStream = "pool,".parse().unwrap();
456 let output = vespertide_migration_impl(input);
457 let output_str = output.to_string();
458 assert!(!output_str.is_empty());
459 }
460
461 fn test_column(name: &str) -> ColumnDef {
462 ColumnDef {
463 name: name.into(),
464 r#type: ColumnType::Simple(SimpleColumnType::Integer),
465 nullable: false,
466 default: None,
467 comment: None,
468 primary_key: None,
469 unique: None,
470 index: None,
471 foreign_key: None,
472 }
473 }
474
475 #[test]
476 fn test_build_migration_block_create_table() {
477 let migration = MigrationPlan {
478 version: 1,
479 comment: None,
480 created_at: None,
481 actions: vec![MigrationAction::CreateTable {
482 table: "users".into(),
483 columns: vec![test_column("id")],
484 constraints: vec![],
485 }],
486 };
487
488 let mut baseline = Vec::new();
489 let result = build_migration_block(&migration, &mut baseline, false);
490
491 assert!(result.is_ok());
492 let block = result.unwrap();
493 let block_str = block.to_string();
494
495 assert!(block_str.contains("version < 1u32"));
497 assert!(block_str.contains("CREATE TABLE"));
498
499 assert_eq!(baseline.len(), 1);
501 assert_eq!(baseline[0].name, "users");
502 }
503
504 #[test]
505 fn test_build_migration_block_add_column() {
506 let create_migration = MigrationPlan {
508 version: 1,
509 comment: None,
510 created_at: None,
511 actions: vec![MigrationAction::CreateTable {
512 table: "users".into(),
513 columns: vec![test_column("id")],
514 constraints: vec![],
515 }],
516 };
517
518 let mut baseline = Vec::new();
519 let _ = build_migration_block(&create_migration, &mut baseline, false);
520
521 let add_column_migration = MigrationPlan {
523 version: 2,
524 comment: None,
525 created_at: None,
526 actions: vec![MigrationAction::AddColumn {
527 table: "users".into(),
528 column: Box::new(ColumnDef {
529 name: "email".into(),
530 r#type: ColumnType::Simple(SimpleColumnType::Text),
531 nullable: true,
532 default: None,
533 comment: None,
534 primary_key: None,
535 unique: None,
536 index: None,
537 foreign_key: None,
538 }),
539 fill_with: None,
540 }],
541 };
542
543 let result = build_migration_block(&add_column_migration, &mut baseline, false);
544 assert!(result.is_ok());
545 let block = result.unwrap();
546 let block_str = block.to_string();
547
548 assert!(block_str.contains("version < 2u32"));
549 assert!(block_str.contains("ALTER TABLE"));
550 assert!(block_str.contains("ADD COLUMN"));
551 }
552
553 #[test]
554 fn test_build_migration_block_multiple_actions() {
555 let migration = MigrationPlan {
556 version: 1,
557 comment: None,
558 created_at: None,
559 actions: vec![
560 MigrationAction::CreateTable {
561 table: "users".into(),
562 columns: vec![test_column("id")],
563 constraints: vec![],
564 },
565 MigrationAction::CreateTable {
566 table: "posts".into(),
567 columns: vec![test_column("id")],
568 constraints: vec![],
569 },
570 ],
571 };
572
573 let mut baseline = Vec::new();
574 let result = build_migration_block(&migration, &mut baseline, false);
575
576 assert!(result.is_ok());
577 assert_eq!(baseline.len(), 2);
578 }
579
580 #[test]
581 fn test_generate_migration_code() {
582 let pool: Expr = syn::parse_str("db_pool").unwrap();
583 let version_table = "test_versions";
584
585 let migration = MigrationPlan {
587 version: 1,
588 comment: None,
589 created_at: None,
590 actions: vec![MigrationAction::CreateTable {
591 table: "users".into(),
592 columns: vec![test_column("id")],
593 constraints: vec![],
594 }],
595 };
596
597 let mut baseline = Vec::new();
598 let block = build_migration_block(&migration, &mut baseline, false).unwrap();
599
600 let generated = generate_migration_code(&pool, version_table, vec![block], false);
601 let generated_str = generated.to_string();
602
603 assert!(generated_str.contains("async"));
605 assert!(generated_str.contains("db_pool"));
606 assert!(generated_str.contains("test_versions"));
607 assert!(generated_str.contains("CREATE TABLE IF NOT EXISTS"));
608 assert!(generated_str.contains("SELECT MAX"));
609 }
610
611 #[test]
612 fn test_generate_migration_code_empty_migrations() {
613 let pool: Expr = syn::parse_str("pool").unwrap();
614 let version_table = "vespertide_version";
615
616 let generated = generate_migration_code(&pool, version_table, vec![], false);
617 let generated_str = generated.to_string();
618
619 assert!(generated_str.contains("async"));
621 assert!(generated_str.contains("vespertide_version"));
622 }
623
624 #[test]
625 fn test_generate_migration_code_multiple_blocks() {
626 let pool: Expr = syn::parse_str("connection").unwrap();
627
628 let mut baseline = Vec::new();
629
630 let migration1 = MigrationPlan {
631 version: 1,
632 comment: None,
633 created_at: None,
634 actions: vec![MigrationAction::CreateTable {
635 table: "users".into(),
636 columns: vec![test_column("id")],
637 constraints: vec![],
638 }],
639 };
640 let block1 = build_migration_block(&migration1, &mut baseline, false).unwrap();
641
642 let migration2 = MigrationPlan {
643 version: 2,
644 comment: None,
645 created_at: None,
646 actions: vec![MigrationAction::CreateTable {
647 table: "posts".into(),
648 columns: vec![test_column("id")],
649 constraints: vec![],
650 }],
651 };
652 let block2 = build_migration_block(&migration2, &mut baseline, false).unwrap();
653
654 let generated = generate_migration_code(&pool, "migrations", vec![block1, block2], false);
655 let generated_str = generated.to_string();
656
657 assert!(generated_str.contains("version < 1u32"));
659 assert!(generated_str.contains("version < 2u32"));
660 }
661
662 #[test]
663 fn test_build_migration_block_generates_all_backends() {
664 let migration = MigrationPlan {
665 version: 1,
666 comment: None,
667 created_at: None,
668 actions: vec![MigrationAction::CreateTable {
669 table: "test_table".into(),
670 columns: vec![test_column("id")],
671 constraints: vec![],
672 }],
673 };
674
675 let mut baseline = Vec::new();
676 let result = build_migration_block(&migration, &mut baseline, false);
677 assert!(result.is_ok());
678
679 let block_str = result.unwrap().to_string();
680
681 assert!(block_str.contains("DatabaseBackend :: Postgres"));
683 assert!(block_str.contains("DatabaseBackend :: MySql"));
684 assert!(block_str.contains("DatabaseBackend :: Sqlite"));
685 }
686
687 #[test]
688 fn test_build_migration_block_with_delete_table() {
689 let create_migration = MigrationPlan {
691 version: 1,
692 comment: None,
693 created_at: None,
694 actions: vec![MigrationAction::CreateTable {
695 table: "temp_table".into(),
696 columns: vec![test_column("id")],
697 constraints: vec![],
698 }],
699 };
700
701 let mut baseline = Vec::new();
702 let _ = build_migration_block(&create_migration, &mut baseline, false);
703 assert_eq!(baseline.len(), 1);
704
705 let delete_migration = MigrationPlan {
707 version: 2,
708 comment: None,
709 created_at: None,
710 actions: vec![MigrationAction::DeleteTable {
711 table: "temp_table".into(),
712 }],
713 };
714
715 let result = build_migration_block(&delete_migration, &mut baseline, false);
716 assert!(result.is_ok());
717 let block_str = result.unwrap().to_string();
718 assert!(block_str.contains("DROP TABLE"));
719
720 assert_eq!(baseline.len(), 0);
722 }
723
724 #[test]
725 fn test_build_migration_block_with_index() {
726 let migration = MigrationPlan {
727 version: 1,
728 comment: None,
729 created_at: None,
730 actions: vec![MigrationAction::CreateTable {
731 table: "users".into(),
732 columns: vec![
733 test_column("id"),
734 ColumnDef {
735 name: "email".into(),
736 r#type: ColumnType::Simple(SimpleColumnType::Text),
737 nullable: true,
738 default: None,
739 comment: None,
740 primary_key: None,
741 unique: None,
742 index: Some(StrOrBoolOrArray::Bool(true)),
743 foreign_key: None,
744 },
745 ],
746 constraints: vec![],
747 }],
748 };
749
750 let mut baseline = Vec::new();
751 let result = build_migration_block(&migration, &mut baseline, false);
752 assert!(result.is_ok());
753
754 let table = &baseline[0];
756 let normalized = table.clone().normalize();
757 assert!(normalized.is_ok());
758 }
759
760 #[test]
761 fn test_build_migration_block_error_nonexistent_table() {
762 let migration = MigrationPlan {
764 version: 1,
765 comment: None,
766 created_at: None,
767 actions: vec![MigrationAction::AddColumn {
768 table: "nonexistent_table".into(),
769 column: Box::new(test_column("new_col")),
770 fill_with: None,
771 }],
772 };
773
774 let mut baseline = Vec::new();
775 let result = build_migration_block(&migration, &mut baseline, false);
776
777 assert!(result.is_err());
778 let err = result.unwrap_err();
779 assert!(err.contains("Failed to build queries for migration version 1"));
780 }
781
782 #[test]
783 fn test_vespertide_migration_impl_loading_error() {
784 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
786
787 unsafe {
789 std::env::remove_var("CARGO_MANIFEST_DIR");
790 }
791
792 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
793 let output = vespertide_migration_impl(input);
794 let output_str = output.to_string();
795
796 assert!(
798 output_str.contains("Failed to load migrations at compile time"),
799 "Expected loading error, got: {}",
800 output_str
801 );
802
803 if let Some(val) = original {
805 unsafe {
806 std::env::set_var("CARGO_MANIFEST_DIR", val);
807 }
808 }
809 }
810
811 #[test]
812 fn test_vespertide_migration_impl_with_valid_project() {
813 use std::fs;
814
815 let dir = tempdir().unwrap();
817 let project_dir = dir.path();
818
819 let config_content = r#"{
821 "modelsDir": "models",
822 "migrationsDir": "migrations",
823 "tableNamingCase": "snake",
824 "columnNamingCase": "snake",
825 "modelFormat": "json"
826 }"#;
827 fs::write(project_dir.join("vespertide.json"), config_content).unwrap();
828
829 fs::create_dir_all(project_dir.join("models")).unwrap();
831 fs::create_dir_all(project_dir.join("migrations")).unwrap();
832
833 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
835 unsafe {
836 std::env::set_var("CARGO_MANIFEST_DIR", project_dir);
837 }
838
839 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
840 let output = vespertide_migration_impl(input);
841 let output_str = output.to_string();
842
843 assert!(
845 output_str.contains("async"),
846 "Expected async block, got: {}",
847 output_str
848 );
849 assert!(
850 output_str.contains("CREATE TABLE IF NOT EXISTS"),
851 "Expected version table creation, got: {}",
852 output_str
853 );
854
855 if let Some(val) = original {
857 unsafe {
858 std::env::set_var("CARGO_MANIFEST_DIR", val);
859 }
860 } else {
861 unsafe {
862 std::env::remove_var("CARGO_MANIFEST_DIR");
863 }
864 }
865 }
866
867 #[test]
868 fn test_build_migration_block_verbose_create_table() {
869 let migration = MigrationPlan {
870 version: 1,
871 comment: Some("initial setup".into()),
872 created_at: None,
873 actions: vec![MigrationAction::CreateTable {
874 table: "users".into(),
875 columns: vec![test_column("id")],
876 constraints: vec![],
877 }],
878 };
879
880 let mut baseline = Vec::new();
881 let result = build_migration_block(&migration, &mut baseline, true);
882
883 assert!(result.is_ok());
884 let block_str = result.unwrap().to_string();
885
886 assert!(block_str.contains("vespertide"));
888 assert!(block_str.contains("Action"));
889 assert!(block_str.contains("version < 1u32"));
890 }
891
892 #[test]
893 fn test_build_migration_block_verbose_multiple_actions() {
894 let migration = MigrationPlan {
895 version: 1,
896 comment: None,
897 created_at: None,
898 actions: vec![
899 MigrationAction::CreateTable {
900 table: "users".into(),
901 columns: vec![test_column("id")],
902 constraints: vec![],
903 },
904 MigrationAction::CreateTable {
905 table: "posts".into(),
906 columns: vec![test_column("id")],
907 constraints: vec![],
908 },
909 ],
910 };
911
912 let mut baseline = Vec::new();
913 let result = build_migration_block(&migration, &mut baseline, true);
914
915 assert!(result.is_ok());
916 let block_str = result.unwrap().to_string();
917
918 assert!(block_str.contains("Action"));
920 assert_eq!(baseline.len(), 2);
921 }
922
923 #[test]
924 fn test_build_migration_block_verbose_add_column() {
925 let create = MigrationPlan {
927 version: 1,
928 comment: None,
929 created_at: None,
930 actions: vec![MigrationAction::CreateTable {
931 table: "users".into(),
932 columns: vec![test_column("id")],
933 constraints: vec![],
934 }],
935 };
936 let mut baseline = Vec::new();
937 let _ = build_migration_block(&create, &mut baseline, true);
938
939 let add_col = MigrationPlan {
941 version: 2,
942 comment: Some("add email".into()),
943 created_at: None,
944 actions: vec![MigrationAction::AddColumn {
945 table: "users".into(),
946 column: Box::new(ColumnDef {
947 name: "email".into(),
948 r#type: ColumnType::Simple(SimpleColumnType::Text),
949 nullable: true,
950 default: None,
951 comment: None,
952 primary_key: None,
953 unique: None,
954 index: None,
955 foreign_key: None,
956 }),
957 fill_with: None,
958 }],
959 };
960
961 let result = build_migration_block(&add_col, &mut baseline, true);
962 assert!(result.is_ok());
963 let block_str = result.unwrap().to_string();
964 assert!(block_str.contains("vespertide"));
965 assert!(block_str.contains("version < 2u32"));
966 }
967
968 #[test]
969 fn test_generate_migration_code_verbose() {
970 let pool: Expr = syn::parse_str("db_pool").unwrap();
971 let version_table = "test_versions";
972
973 let migration = MigrationPlan {
974 version: 1,
975 comment: None,
976 created_at: None,
977 actions: vec![MigrationAction::CreateTable {
978 table: "users".into(),
979 columns: vec![test_column("id")],
980 constraints: vec![],
981 }],
982 };
983
984 let mut baseline = Vec::new();
985 let block = build_migration_block(&migration, &mut baseline, true).unwrap();
986
987 let generated = generate_migration_code(&pool, version_table, vec![block], true);
988 let generated_str = generated.to_string();
989
990 assert!(generated_str.contains("Current database version"));
992 assert!(generated_str.contains("async"));
993 }
994
995 #[test]
996 fn test_macro_parsing_verbose_flag() {
997 let input: proc_macro2::TokenStream = "pool, verbose".parse().unwrap();
999 let output = vespertide_migration_impl(input);
1000 let output_str = output.to_string();
1001 assert!(!output_str.is_empty());
1003 }
1004
1005 #[test]
1006 fn test_vespertide_migration_impl_with_migrations() {
1007 use std::fs;
1008
1009 let dir = tempdir().unwrap();
1011 let project_dir = dir.path();
1012
1013 let config_content = r#"{
1015 "modelsDir": "models",
1016 "migrationsDir": "migrations",
1017 "tableNamingCase": "snake",
1018 "columnNamingCase": "snake",
1019 "modelFormat": "json"
1020 }"#;
1021 fs::write(project_dir.join("vespertide.json"), config_content).unwrap();
1022
1023 fs::create_dir_all(project_dir.join("models")).unwrap();
1025 fs::create_dir_all(project_dir.join("migrations")).unwrap();
1026
1027 let migration_content = r#"{
1029 "version": 1,
1030 "actions": [
1031 {
1032 "type": "create_table",
1033 "table": "users",
1034 "columns": [
1035 {"name": "id", "type": "integer", "nullable": false}
1036 ],
1037 "constraints": []
1038 }
1039 ]
1040 }"#;
1041 fs::write(
1042 project_dir.join("migrations").join("0001_initial.json"),
1043 migration_content,
1044 )
1045 .unwrap();
1046
1047 let original = std::env::var("CARGO_MANIFEST_DIR").ok();
1049 unsafe {
1050 std::env::set_var("CARGO_MANIFEST_DIR", project_dir);
1051 }
1052
1053 let input: proc_macro2::TokenStream = "pool".parse().unwrap();
1054 let output = vespertide_migration_impl(input);
1055 let output_str = output.to_string();
1056
1057 assert!(
1059 output_str.contains("async"),
1060 "Expected async block, got: {}",
1061 output_str
1062 );
1063
1064 if let Some(val) = original {
1066 unsafe {
1067 std::env::set_var("CARGO_MANIFEST_DIR", val);
1068 }
1069 } else {
1070 unsafe {
1071 std::env::remove_var("CARGO_MANIFEST_DIR");
1072 }
1073 }
1074 }
1075}