cargo_hammerwork/commands/
migration.rs1use 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 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 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 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 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}