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