kegani_cli/commands/
db.rs1use anyhow::{Context, Result};
4use console::{style, Emoji};
5use std::process::Command;
6
7pub 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 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 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 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 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 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 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 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 db_drop(project_dir)?;
181
182 db_create(project_dir)?;
184
185 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 db_seed(project_dir)?;
199
200 println!();
201 println!("{} {}", Emoji("✅", ""), style("Database reset complete!").green());
202 Ok(())
203}
204
205fn extract_db_name(url: &str) -> Result<String> {
207 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 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,
234 Drop,
236 Seed,
238 Psql,
240 Schema,
242 Reset,
244}