Skip to main content

cargo_hammerwork/commands/
migration.rs

1use anyhow::Result;
2use clap::Subcommand;
3use sqlx::Row;
4use tracing::info;
5
6use crate::config::Config;
7use crate::utils::database::DatabasePool;
8
9#[derive(Subcommand)]
10pub enum MigrationCommand {
11    #[command(about = "Run database migrations")]
12    Run {
13        #[arg(short = 'u', long, help = "Database connection URL")]
14        database_url: Option<String>,
15        #[arg(short, long, help = "Drop existing tables before migration")]
16        drop: bool,
17    },
18    #[command(about = "Check migration status")]
19    Status {
20        #[arg(short = 'u', long, help = "Database connection URL")]
21        database_url: Option<String>,
22    },
23}
24
25impl MigrationCommand {
26    pub async fn execute(&self, config: &Config) -> Result<()> {
27        match self {
28            MigrationCommand::Run { database_url, drop } => {
29                let db_url = database_url
30                    .as_ref()
31                    .map(|s| s.as_str())
32                    .or(config.get_database_url())
33                    .ok_or_else(|| anyhow::anyhow!("Database URL is required"))?;
34
35                info!("Running migrations for: {}", db_url);
36                let pool = DatabasePool::connect(db_url, config.get_connection_pool_size()).await?;
37                pool.migrate(*drop).await?;
38                info!("Migrations completed successfully");
39            }
40            MigrationCommand::Status { database_url } => {
41                let db_url = database_url
42                    .as_ref()
43                    .map(|s| s.as_str())
44                    .or(config.get_database_url())
45                    .ok_or_else(|| anyhow::anyhow!("Database URL is required"))?;
46
47                info!("Checking migration status for: {}", db_url);
48                check_migration_status(db_url).await?;
49            }
50        }
51        Ok(())
52    }
53}
54
55async fn check_migration_status(database_url: &str) -> Result<()> {
56    let pool = DatabasePool::connect(database_url, 1).await?;
57
58    match pool {
59        DatabasePool::Postgres(ref pg_pool) => {
60            let result = sqlx::query(
61                "SELECT table_name FROM information_schema.tables 
62                 WHERE table_schema = 'public' AND table_name = 'hammerwork_jobs'",
63            )
64            .fetch_optional(pg_pool)
65            .await?;
66
67            if result.is_some() {
68                println!("✅ PostgreSQL tables exist");
69
70                // Check for specific columns to verify schema version
71                let columns = sqlx::query(
72                    "SELECT column_name FROM information_schema.columns 
73                     WHERE table_name = 'hammerwork_jobs' ORDER BY column_name",
74                )
75                .fetch_all(pg_pool)
76                .await?;
77
78                println!("📊 Schema columns: {}", columns.len());
79                for col in columns {
80                    let column_name: String = col.try_get("column_name")?;
81                    println!("  - {}", column_name);
82                }
83            } else {
84                println!("❌ PostgreSQL tables do not exist. Run 'migrate run' to create them.");
85            }
86        }
87        DatabasePool::MySQL(ref mysql_pool) => {
88            let result = sqlx::query(
89                "SELECT TABLE_NAME FROM information_schema.tables 
90                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hammerwork_jobs'",
91            )
92            .fetch_optional(mysql_pool)
93            .await?;
94
95            if result.is_some() {
96                println!("✅ MySQL tables exist");
97
98                // Check for specific columns to verify schema version
99                let columns = sqlx::query(
100                    "SELECT COLUMN_NAME FROM information_schema.columns 
101                     WHERE TABLE_NAME = 'hammerwork_jobs' AND TABLE_SCHEMA = DATABASE() 
102                     ORDER BY COLUMN_NAME",
103                )
104                .fetch_all(mysql_pool)
105                .await?;
106
107                println!("📊 Schema columns: {}", columns.len());
108                for col in columns {
109                    let column_name: String = col.try_get("COLUMN_NAME")?;
110                    println!("  - {}", column_name);
111                }
112            } else {
113                println!("❌ MySQL tables do not exist. Run 'migrate run' to create them.");
114            }
115        }
116    }
117
118    Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123
124    #[test]
125    fn test_migration_command_structure() {
126        // Test that migration commands are properly structured
127        let commands = vec!["run", "status"];
128
129        for cmd in commands {
130            assert!(!cmd.is_empty());
131            assert!(cmd.chars().all(|c| c.is_ascii_lowercase()));
132        }
133    }
134
135    #[test]
136    fn test_migration_args_validation() {
137        // Test database URL validation for migration commands
138        let valid_urls = vec![
139            "postgres://localhost/test",
140            "mysql://localhost/test",
141            "postgres://user:pass@localhost:5432/db",
142        ];
143
144        for url in valid_urls {
145            assert!(is_valid_database_url(url));
146        }
147
148        let invalid_urls = vec!["", "invalid", "http://localhost/db", "file:///path/to/db"];
149
150        for url in invalid_urls {
151            assert!(!is_valid_database_url(url));
152        }
153    }
154
155    fn is_valid_database_url(url: &str) -> bool {
156        !url.is_empty() && (url.starts_with("postgres://") || url.starts_with("mysql://"))
157    }
158}