Skip to main content

vespertide_macro/
lib.rs

1// MigrationOptions and MigrationError are now in vespertide-core
2
3use 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    // Use the current baseline schema (from all previous migrations)
65    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    // Update baseline schema incrementally by applying each action
73    for action in &migration.actions {
74        let _ = apply_action(baseline_schema, action);
75    }
76
77    // Generate version guard and SQL execution block
78    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        // Verbose mode: preserve per-action grouping with action descriptions
83        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            // Build per-SQL execution with global index
114            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        // Non-verbose: flatten all SQL into one array (minimal overhead)
165        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            // Create version table if it does not exist (outside transaction)
234            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            // Single transaction for the entire migration process.
244            // This prevents race conditions when multiple connections exist
245            // (e.g. SQLite with max_connections > 1).
246            let __txn = __pool.begin().await.map_err(|e| {
247                ::vespertide::MigrationError::DatabaseError(format!("Failed to begin transaction: {}", e))
248            })?;
249
250            // Read current maximum version inside the transaction (holds lock)
251            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            // Execute each migration block within the same transaction
264            #(#migration_blocks)*
265
266            // Commit the entire migration
267            __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
276/// Inner implementation that works with proc_macro2::TokenStream for testability.
277pub(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    // Get project root from CARGO_MANIFEST_DIR (same as load_migrations_at_compile_time)
288    let project_root = match env::var("CARGO_MANIFEST_DIR") {
289        Ok(dir) => Some(PathBuf::from(dir)),
290        Err(_) => None,
291    };
292
293    // Load config to get prefix
294    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    // Apply prefix to version_table if not explicitly provided
308    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    // Load migration files and build SQL at compile time
314    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    // Apply prefix to migrations and build SQL using incremental baseline schema
337    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        // Apply prefix to migration table names
343        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/// Zero-runtime migration entry point.
356#[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        // Create a temporary directory with test files
375        let dir = tempdir().unwrap();
376
377        // Create a test file that uses the macro
378        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        // Use runtime-macros to emulate macro expansion
387        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        // The macro will fail because there's no vespertide config, but
394        // the important thing is that it runs and covers the macro code
395        // We expect an error due to missing config
396        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        // Test that invalid options produce a compile error
418        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        // Should contain an error message about unsupported option
422        assert!(output_str.contains("unsupported option"));
423    }
424
425    #[test]
426    fn test_macro_parsing_valid_input() {
427        // Test that valid input is parsed correctly
428        // The macro will either succeed (if migrations dir exists and is empty)
429        // or fail with a migration loading error
430        let input: proc_macro2::TokenStream = "my_pool".parse().unwrap();
431        let output = vespertide_migration_impl(input);
432        let output_str = output.to_string();
433        // Should produce output (either success or migration loading error)
434        assert!(!output_str.is_empty());
435        // If error, it should mention "Failed to load"
436        // If success, it should contain "async"
437        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        // Verify the generated block contains expected elements
496        assert!(block_str.contains("version < 1u32"));
497        assert!(block_str.contains("CREATE TABLE"));
498
499        // Verify baseline schema was updated
500        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        // First create the table
507        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        // Now add a column
522        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        // Create a simple migration block
586        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        // Verify the generated code structure
604        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        // Should still generate the wrapper code
620        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        // Both version checks should be present
658        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        // The generated block should have backend matching
682        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        // First create the table
690        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        // Now delete it
706        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        // Baseline should be empty after delete
721        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        // Table should be normalized with index
755        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        // Try to add column to a table that doesn't exist - should fail
763        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        // Save original CARGO_MANIFEST_DIR
785        let original = std::env::var("CARGO_MANIFEST_DIR").ok();
786
787        // Remove CARGO_MANIFEST_DIR to trigger loading error
788        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        // Should contain error about failed loading
797        assert!(
798            output_str.contains("Failed to load migrations at compile time"),
799            "Expected loading error, got: {}",
800            output_str
801        );
802
803        // Restore CARGO_MANIFEST_DIR
804        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        // Create a temporary directory with a valid vespertide project
816        let dir = tempdir().unwrap();
817        let project_dir = dir.path();
818
819        // Create vespertide.json config
820        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        // Create empty models and migrations directories
830        fs::create_dir_all(project_dir.join("models")).unwrap();
831        fs::create_dir_all(project_dir.join("migrations")).unwrap();
832
833        // Save original CARGO_MANIFEST_DIR and set to temp dir
834        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        // Should produce valid async code since there are no migrations
844        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        // Restore CARGO_MANIFEST_DIR
856        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        // Verbose mode should contain eprintln statements with action descriptions
887        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        // Should have action numbering for both actions
919        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        // Create table first
926        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        // Add column in verbose mode
940        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        // Verbose mode should include current version eprintln
991        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        // Test parsing the "verbose" keyword
998        let input: proc_macro2::TokenStream = "pool, verbose".parse().unwrap();
999        let output = vespertide_migration_impl(input);
1000        let output_str = output.to_string();
1001        // Should produce output (either success or migration loading error)
1002        assert!(!output_str.is_empty());
1003    }
1004
1005    #[test]
1006    fn test_vespertide_migration_impl_with_migrations() {
1007        use std::fs;
1008
1009        // Create a temporary directory with a valid vespertide project and migrations
1010        let dir = tempdir().unwrap();
1011        let project_dir = dir.path();
1012
1013        // Create vespertide.json config
1014        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        // Create models and migrations directories
1024        fs::create_dir_all(project_dir.join("models")).unwrap();
1025        fs::create_dir_all(project_dir.join("migrations")).unwrap();
1026
1027        // Create a migration file
1028        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        // Save original CARGO_MANIFEST_DIR and set to temp dir
1048        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        // Should produce valid async code with migration
1058        assert!(
1059            output_str.contains("async"),
1060            "Expected async block, got: {}",
1061            output_str
1062        );
1063
1064        // Restore CARGO_MANIFEST_DIR
1065        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}