Skip to main content

kegani_cli/commands/
db.rs

1//! `keg db` command — Database operations
2
3use anyhow::{Context, Result};
4use console::{style, Emoji};
5use std::process::Command;
6
7/// Run database actions
8pub fn run_db_action(action: DbAction) -> Result<()> {
9    let project_dir = std::env::current_dir().context("Failed to get current directory")?;
10
11    println!();
12    println!("{} {}", Emoji("🗄️", ""), style("Database Operations").bold());
13    println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
14
15    match action {
16        DbAction::Create => db_create(&project_dir)?,
17        DbAction::Drop => db_drop(&project_dir)?,
18        DbAction::Seed => db_seed(&project_dir)?,
19        DbAction::Psql => db_psql(&project_dir)?,
20        DbAction::Schema => db_schema(&project_dir)?,
21        DbAction::Reset => db_reset(&project_dir)?,
22    }
23
24    Ok(())
25}
26
27fn db_create(project_dir: &std::path::Path) -> Result<()> {
28    println!("  {} Creating database...", style("📦").cyan());
29
30    // Try using sqlx first, fall back to psql
31    let result = Command::new("sqlx")
32        .args(["database", "create"])
33        .current_dir(project_dir)
34        .status();
35
36    if result.map(|s| !s.success()).unwrap_or(true) {
37        // Fall back to psql
38        let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
39        let db_name = extract_db_name(&db_url)?;
40
41        println!("  {} Using psql to create database...", style("→").dim());
42
43        let status = Command::new("psql")
44            .args(["-c", &format!("CREATE DATABASE {};", db_name)])
45            .env("PGPASSWORD", extract_password(&db_url).unwrap_or_else(|| "".to_string()))
46            .current_dir(project_dir)
47            .status()
48            .context("Failed to run psql")?;
49
50        if !status.success() {
51            anyhow::bail!("Failed to create database");
52        }
53    }
54
55    println!();
56    println!("{} {}", Emoji("✅", ""), style("Database created successfully!").green());
57    Ok(())
58}
59
60fn db_drop(project_dir: &std::path::Path) -> Result<()> {
61    println!("  {} Dropping database...", style("🗑️").cyan());
62    println!("  {} {}", style("⚠️").yellow(), style("WARNING: This will delete all data!").red().bold());
63
64    // Try using sqlx first, fall back to psql
65    let result = Command::new("sqlx")
66        .args(["database", "drop"])
67        .current_dir(project_dir)
68        .status();
69
70    if result.map(|s| !s.success()).unwrap_or(true) {
71        // Fall back to psql
72        let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
73        let db_name = extract_db_name(&db_url)?;
74
75        println!("  {} Using psql to drop database...", style("→").dim());
76
77        let status = Command::new("psql")
78            .args(["-c", &format!("DROP DATABASE {};", db_name)])
79            .env("PGPASSWORD", extract_password(&db_url).unwrap_or_else(|| "".to_string()))
80            .current_dir(project_dir)
81            .status()
82            .context("Failed to run psql")?;
83
84        if !status.success() {
85            anyhow::bail!("Failed to drop database");
86        }
87    }
88
89    println!();
90    println!("{} {}", Emoji("✅", ""), style("Database dropped successfully!").green());
91    Ok(())
92}
93
94fn db_seed(project_dir: &std::path::Path) -> Result<()> {
95    println!("  {} Seeding database...", style("🌱").cyan());
96
97    // Check for seed script
98    let seed_file = project_dir.join("migrations").join("seed.sql");
99
100    if seed_file.exists() {
101        let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
102
103        let status = Command::new("psql")
104            .args([&db_url, "-f", seed_file.to_str().unwrap()])
105            .current_dir(project_dir)
106            .status()
107            .context("Failed to run seed script")?;
108
109        if !status.success() {
110            anyhow::bail!("Seed failed");
111        }
112    } else {
113        println!("  {} {}", style("ℹ").dim(), style("No seed.sql found in migrations/. Creating sample...").dim());
114
115        // Create a sample seed file
116        let seed_content = r#"-- Sample seed data
117-- Add your seed SQL here
118
119-- Example:
120-- INSERT INTO users (name, email) VALUES ('Admin', 'admin@example.com');
121"#;
122        std::fs::write(&seed_file, seed_content)?;
123        println!("  {} {}", style("✓").green(), style("Created migrations/seed.sql").dim());
124        println!("  {} {}", style("→").cyan(), style("Edit migrations/seed.sql and run `keg db seed` again").dim());
125        return Ok(());
126    }
127
128    println!();
129    println!("{} {}", Emoji("✅", ""), style("Database seeded successfully!").green());
130    Ok(())
131}
132
133fn db_psql(project_dir: &std::path::Path) -> Result<()> {
134    println!("  {} Opening psql shell...", style("🔗").cyan());
135
136    let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
137
138    let mut cmd = Command::new("psql");
139    cmd.arg(&db_url)
140        .current_dir(project_dir)
141        .status()
142        .context("Failed to open psql. Is psql installed?")?;
143
144    Ok(())
145}
146
147fn db_schema(project_dir: &std::path::Path) -> Result<()> {
148    println!("  {} Showing database schema...", style("📋").cyan());
149
150    let db_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
151
152    // Query to show tables
153    let tables_query = r#"
154SELECT table_name
155FROM information_schema.tables
156WHERE table_schema = 'public'
157ORDER BY table_name;
158"#;
159
160    let output = Command::new("psql")
161        .args(["-c", tables_query])
162        .arg(&db_url)
163        .current_dir(project_dir)
164        .output()
165        .context("Failed to run psql. Is psql installed?")?;
166
167    println!();
168    println!("{}", style("Tables:").bold());
169    println!("{}", String::from_utf8_lossy(&output.stdout));
170
171    Ok(())
172}
173
174fn db_reset(project_dir: &std::path::Path) -> Result<()> {
175    println!();
176    println!("{} {} This will reset the database!", style("⚠️").yellow(), style("WARNING").red().bold());
177    println!();
178
179    // Drop
180    db_drop(project_dir)?;
181
182    // Create
183    db_create(project_dir)?;
184
185    // Run migrations
186    println!();
187    println!("  {} Running migrations...", style("📦").cyan());
188    let migrate_result = Command::new("sqlx")
189        .args(["migrate", "run"])
190        .current_dir(project_dir)
191        .status();
192
193    if migrate_result.map(|s| !s.success()).unwrap_or(false) {
194        println!("  {} {}", style("⚠").yellow(), style("sqlx-cli not available, skipping migrations").dim());
195    }
196
197    // Seed
198    db_seed(project_dir)?;
199
200    println!();
201    println!("{} {}", Emoji("✅", ""), style("Database reset complete!").green());
202    Ok(())
203}
204
205// Helper functions
206fn extract_db_name(url: &str) -> Result<String> {
207    // Parse postgresql://user:pass@host:5432/dbname
208    if let Some(i) = url.rfind('/') {
209        let after_slash = &url[i+1..];
210        if let Some(j) = after_slash.find('?') {
211            Ok(after_slash[..j].to_string())
212        } else {
213            Ok(after_slash.to_string())
214        }
215    } else {
216        anyhow::bail!("Invalid DATABASE_URL format")
217    }
218}
219
220fn extract_password(url: &str) -> Option<String> {
221    // Extract password from postgresql://user:pass@host:5432/dbname
222    if let Some(i) = url.find(':') {
223        if let Some(j) = url.find('@') {
224            return Some(url[i+1..j].to_string());
225        }
226    }
227    None
228}
229
230#[derive(clap::Subcommand)]
231pub enum DbAction {
232    /// Create the database
233    Create,
234    /// Drop the database
235    Drop,
236    /// Seed the database with sample data
237    Seed,
238    /// Open psql shell
239    Psql,
240    /// Show database schema
241    Schema,
242    /// Reset the database (drop + create + seed)
243    Reset,
244}