1use std::path::PathBuf;
4
5use crate::cli::{DbArgs, OutputFormat};
6use crate::commands::introspect::{
7 IntrospectionOptions, format_as_json, format_as_prax, format_as_sql, get_database_type,
8};
9use crate::commands::seed::{SeedRunner, find_seed_file, get_database_url};
10use crate::config::{CONFIG_FILE_NAME, Config, SCHEMA_FILE_NAME};
11use crate::error::{CliError, CliResult};
12use crate::output::{self, success, warn};
13
14pub async fn run(args: DbArgs) -> CliResult<()> {
16 match args.command {
17 crate::cli::DbSubcommand::Push(push_args) => run_push(push_args).await,
18 crate::cli::DbSubcommand::Pull(pull_args) => run_pull(pull_args).await,
19 crate::cli::DbSubcommand::Seed(seed_args) => run_seed(seed_args).await,
20 crate::cli::DbSubcommand::Execute(exec_args) => run_execute(exec_args).await,
21 }
22}
23
24async fn run_push(args: crate::cli::DbPushArgs) -> CliResult<()> {
26 output::header("Database Push");
27
28 let cwd = std::env::current_dir()?;
29 let config = load_config(&cwd)?;
30 let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_NAME));
31
32 output::kv("Schema", &schema_path.display().to_string());
33 output::kv(
34 "Database",
35 config
36 .database
37 .url
38 .as_deref()
39 .unwrap_or("env(DATABASE_URL)"),
40 );
41 output::newline();
42
43 output::step(1, 4, "Parsing schema...");
45 let schema_content = std::fs::read_to_string(&schema_path)?;
46 let schema = parse_schema(&schema_content)?;
47
48 output::step(2, 4, "Introspecting database...");
50 output::step(3, 4, "Calculating changes...");
54 let changes = calculate_schema_changes(&schema)?;
55
56 if changes.is_empty() {
57 output::newline();
58 success("Database is already in sync with schema!");
59 return Ok(());
60 }
61
62 let destructive = changes.iter().any(|c| c.is_destructive);
64 if destructive && !args.accept_data_loss && !args.force {
65 output::newline();
66 warn("This push would cause data loss!");
67 output::section("Destructive changes");
68 for change in changes.iter().filter(|c| c.is_destructive) {
69 output::list_item(&format!("⚠️ {}", change.description));
70 }
71 output::newline();
72 output::info("Use --accept-data-loss to proceed, or --force to skip confirmation.");
73 return Ok(());
74 }
75
76 output::step(4, 4, "Applying changes...");
78 for change in &changes {
79 output::list_item(&change.description);
80 }
82
83 output::newline();
84 success(&format!("Applied {} changes to database!", changes.len()));
85
86 Ok(())
87}
88
89async fn run_pull(args: crate::cli::DbPullArgs) -> CliResult<()> {
91 output::header("Database Pull (Introspection)");
92
93 let cwd = std::env::current_dir()?;
94 let config = load_config(&cwd)?;
95
96 let database_url = get_database_url(&config)?;
98 let db_type = get_database_type(&config.database.provider)?;
99
100 output::kv("Provider", &config.database.provider);
101 output::kv("Database", &mask_database_url(&database_url));
102 if let Some(ref schema) = args.schema {
103 output::kv("Schema", schema);
104 }
105 output::newline();
106
107 let options = IntrospectionOptions {
109 schema: args.schema.clone(),
110 include_views: args.include_views,
111 include_materialized_views: args.include_materialized_views,
112 table_filter: args.tables.clone(),
113 exclude_pattern: args.exclude.clone(),
114 include_comments: args.comments,
115 sample_size: args.sample_size,
116 };
117
118 output::step(1, 3, "Introspecting database...");
120
121 #[cfg(feature = "postgres")]
122 let db_schema = {
123 use crate::commands::introspect::postgres::PostgresIntrospector;
124 use crate::commands::introspect::Introspector;
125
126 if config.database.provider.to_lowercase().contains("postgres") {
127 let introspector = PostgresIntrospector::new(database_url.clone());
128 introspector.introspect(&options).await?
129 } else {
130 return Err(CliError::Config(format!(
131 "Introspection for {} requires the corresponding feature. Compile with --features {}",
132 config.database.provider,
133 config.database.provider.to_lowercase()
134 )));
135 }
136 };
137
138 #[cfg(not(feature = "postgres"))]
139 let db_schema = {
140 return Err(CliError::Config(
141 "No database driver enabled. Compile with --features postgres, mysql, sqlite, or mssql".to_string()
142 ));
143 };
144
145 output::step(2, 3, "Generating schema...");
147 let schema_content = match args.format {
148 OutputFormat::Prax => format_as_prax(&db_schema, &config),
149 OutputFormat::Json => format_as_json(&db_schema)?,
150 OutputFormat::Sql => format_as_sql(&db_schema, db_type),
151 };
152
153 output::step(3, 3, "Writing output...");
155
156 if args.print {
157 output::newline();
158 output::section("Generated Schema");
159 println!("{}", schema_content);
160 } else {
161 let output_path = args.output.unwrap_or_else(|| {
162 let ext = match args.format {
163 OutputFormat::Prax => "prax",
164 OutputFormat::Json => "json",
165 OutputFormat::Sql => "sql",
166 };
167 cwd.join(format!("schema.{}", ext))
168 });
169
170 if output_path.exists() && !args.force {
171 warn(&format!("{} already exists!", output_path.display()));
172 if !output::confirm("Overwrite existing file?") {
173 output::newline();
174 output::info("Pull cancelled.");
175 return Ok(());
176 }
177 }
178
179 std::fs::write(&output_path, &schema_content)?;
180
181 output::newline();
182 success(&format!("Schema written to {}", output_path.display()));
183 }
184
185 output::newline();
186 output::section("Summary");
187 output::kv("Tables", &db_schema.tables.len().to_string());
188 output::kv("Enums", &db_schema.enums.len().to_string());
189 output::kv("Views", &db_schema.views.len().to_string());
190
191 if !db_schema.tables.is_empty() {
193 output::newline();
194 output::section("Tables Introspected");
195 for table in &db_schema.tables {
196 output::list_item(&format!(
197 "{} ({} columns)",
198 table.name,
199 table.columns.len()
200 ));
201 }
202 }
203
204 Ok(())
205}
206
207async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
209 output::header("Database Seed");
210
211 let cwd = std::env::current_dir()?;
212 let config = load_config(&cwd)?;
213
214 if !args.force && !config.seed.should_seed(&args.environment) {
216 warn(&format!(
217 "Seeding is disabled for environment '{}'. Use --force to override.",
218 args.environment
219 ));
220 return Ok(());
221 }
222
223 let seed_path = args
225 .seed_file
226 .or_else(|| config.seed.script.clone())
227 .or_else(|| find_seed_file(&cwd, &config))
228 .ok_or_else(|| {
229 CliError::Config(
230 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
231 or specify with --seed-file".to_string()
232 )
233 })?;
234
235 if !seed_path.exists() {
236 return Err(CliError::Config(format!(
237 "Seed file not found: {}. Create a seed file or specify with --seed-file",
238 seed_path.display()
239 )));
240 }
241
242 let database_url = get_database_url(&config)?;
244
245 output::kv("Seed file", &seed_path.display().to_string());
246 output::kv("Database", &mask_database_url(&database_url));
247 output::kv("Provider", &config.database.provider);
248 output::kv("Environment", &args.environment);
249 output::newline();
250
251 if args.reset {
253 warn("Resetting database before seeding...");
254 output::newline();
256 }
257
258 let runner = SeedRunner::new(
260 seed_path,
261 database_url,
262 config.database.provider.clone(),
263 cwd,
264 )?
265 .with_environment(&args.environment)
266 .with_reset(args.reset);
267
268 let result = runner.run().await?;
269
270 output::newline();
271 success("Database seeded successfully!");
272
273 output::newline();
275 output::section("Summary");
276 output::kv("Records affected", &result.records_affected.to_string());
277 if !result.tables_seeded.is_empty() {
278 output::kv("Tables seeded", &result.tables_seeded.join(", "));
279 }
280
281 Ok(())
282}
283
284fn mask_database_url(url: &str) -> String {
286 if let Ok(parsed) = url::Url::parse(url) {
287 let mut masked = parsed.clone();
288 if parsed.password().is_some() {
289 let _ = masked.set_password(Some("****"));
290 }
291 masked.to_string()
292 } else {
293 if url.len() > 30 {
295 format!("{}...", &url[..30])
296 } else {
297 url.to_string()
298 }
299 }
300}
301
302async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
304 output::header("Execute SQL");
305
306 let cwd = std::env::current_dir()?;
307 let config = load_config(&cwd)?;
308
309 let sql = if let Some(sql) = args.sql {
311 sql
312 } else if let Some(file) = args.file {
313 std::fs::read_to_string(&file)?
314 } else if args.stdin {
315 let mut sql = String::new();
316 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
317 sql
318 } else {
319 return Err(CliError::Command(
320 "Must provide SQL via --sql, --file, or --stdin".to_string(),
321 )
322 .into());
323 };
324
325 output::kv(
326 "Database",
327 config
328 .database
329 .url
330 .as_deref()
331 .unwrap_or("env(DATABASE_URL)"),
332 );
333 output::newline();
334
335 output::section("SQL");
336 output::code(&sql, "sql");
337 output::newline();
338
339 if !args.force {
341 if !output::confirm("Execute this SQL?") {
342 output::newline();
343 output::info("Execution cancelled.");
344 return Ok(());
345 }
346 }
347
348 output::step(1, 1, "Executing SQL...");
350 output::newline();
353 success("SQL executed successfully!");
354
355 Ok(())
356}
357
358#[derive(Debug)]
363struct SchemaChange {
364 description: String,
365 #[allow(dead_code)]
366 sql: String,
367 is_destructive: bool,
368}
369
370fn load_config(cwd: &PathBuf) -> CliResult<Config> {
371 let config_path = cwd.join(CONFIG_FILE_NAME);
372 if config_path.exists() {
373 Config::load(&config_path)
374 } else {
375 Ok(Config::default())
376 }
377}
378
379fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
380 prax_schema::parse_schema(content)
381 .map_err(|e| CliError::Schema(format!("Failed to parse schema: {}", e)))
382}
383
384fn calculate_schema_changes(_schema: &prax_schema::ast::Schema) -> CliResult<Vec<SchemaChange>> {
385 Ok(Vec::new())
388}
389