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_PATH};
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_PATH));
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::Introspector;
124 use crate::commands::introspect::postgres::PostgresIntrospector;
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"
142 .to_string(),
143 ));
144 };
145
146 output::step(2, 3, "Generating schema...");
148 let schema_content = match args.format {
149 OutputFormat::Prax => format_as_prax(&db_schema, &config),
150 OutputFormat::Json => format_as_json(&db_schema)?,
151 OutputFormat::Sql => format_as_sql(&db_schema, db_type),
152 };
153
154 output::step(3, 3, "Writing output...");
156
157 if args.print {
158 output::newline();
159 output::section("Generated Schema");
160 println!("{}", schema_content);
161 } else {
162 let output_path = args.output.unwrap_or_else(|| {
163 let ext = match args.format {
164 OutputFormat::Prax => "prax",
165 OutputFormat::Json => "json",
166 OutputFormat::Sql => "sql",
167 };
168 cwd.join(format!("schema.{}", ext))
169 });
170
171 if output_path.exists() && !args.force {
172 warn(&format!("{} already exists!", output_path.display()));
173 if !output::confirm("Overwrite existing file?") {
174 output::newline();
175 output::info("Pull cancelled.");
176 return Ok(());
177 }
178 }
179
180 std::fs::write(&output_path, &schema_content)?;
181
182 output::newline();
183 success(&format!("Schema written to {}", output_path.display()));
184 }
185
186 output::newline();
187 output::section("Summary");
188 output::kv("Tables", &db_schema.tables.len().to_string());
189 output::kv("Enums", &db_schema.enums.len().to_string());
190 output::kv("Views", &db_schema.views.len().to_string());
191
192 if !db_schema.tables.is_empty() {
194 output::newline();
195 output::section("Tables Introspected");
196 for table in &db_schema.tables {
197 output::list_item(&format!("{} ({} columns)", table.name, table.columns.len()));
198 }
199 }
200
201 Ok(())
202}
203
204async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
206 output::header("Database Seed");
207
208 let cwd = std::env::current_dir()?;
209 let config = load_config(&cwd)?;
210
211 if !args.force && !config.seed.should_seed(&args.environment) {
213 warn(&format!(
214 "Seeding is disabled for environment '{}'. Use --force to override.",
215 args.environment
216 ));
217 return Ok(());
218 }
219
220 let seed_path = args
222 .seed_file
223 .or_else(|| config.seed.script.clone())
224 .or_else(|| find_seed_file(&cwd, &config))
225 .ok_or_else(|| {
226 CliError::Config(
227 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
228 or specify with --seed-file".to_string()
229 )
230 })?;
231
232 if !seed_path.exists() {
233 return Err(CliError::Config(format!(
234 "Seed file not found: {}. Create a seed file or specify with --seed-file",
235 seed_path.display()
236 )));
237 }
238
239 let database_url = get_database_url(&config)?;
241
242 output::kv("Seed file", &seed_path.display().to_string());
243 output::kv("Database", &mask_database_url(&database_url));
244 output::kv("Provider", &config.database.provider);
245 output::kv("Environment", &args.environment);
246 output::newline();
247
248 if args.reset {
250 warn("Resetting database before seeding...");
251 output::newline();
253 }
254
255 let runner = SeedRunner::new(
257 seed_path,
258 database_url,
259 config.database.provider.clone(),
260 cwd,
261 )?
262 .with_environment(&args.environment)
263 .with_reset(args.reset);
264
265 let result = runner.run().await?;
266
267 output::newline();
268 success("Database seeded successfully!");
269
270 output::newline();
272 output::section("Summary");
273 output::kv("Records affected", &result.records_affected.to_string());
274 if !result.tables_seeded.is_empty() {
275 output::kv("Tables seeded", &result.tables_seeded.join(", "));
276 }
277
278 Ok(())
279}
280
281fn mask_database_url(url: &str) -> String {
283 if let Ok(parsed) = url::Url::parse(url) {
284 let mut masked = parsed.clone();
285 if parsed.password().is_some() {
286 let _ = masked.set_password(Some("****"));
287 }
288 masked.to_string()
289 } else {
290 if url.len() > 30 {
292 format!("{}...", &url[..30])
293 } else {
294 url.to_string()
295 }
296 }
297}
298
299async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
301 output::header("Execute SQL");
302
303 let cwd = std::env::current_dir()?;
304 let config = load_config(&cwd)?;
305
306 let sql = if let Some(sql) = args.sql {
308 sql
309 } else if let Some(file) = args.file {
310 std::fs::read_to_string(&file)?
311 } else if args.stdin {
312 let mut sql = String::new();
313 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
314 sql
315 } else {
316 return Err(CliError::Command(
317 "Must provide SQL via --sql, --file, or --stdin".to_string(),
318 )
319 .into());
320 };
321
322 output::kv(
323 "Database",
324 config
325 .database
326 .url
327 .as_deref()
328 .unwrap_or("env(DATABASE_URL)"),
329 );
330 output::newline();
331
332 output::section("SQL");
333 output::code(&sql, "sql");
334 output::newline();
335
336 if !args.force {
338 if !output::confirm("Execute this SQL?") {
339 output::newline();
340 output::info("Execution cancelled.");
341 return Ok(());
342 }
343 }
344
345 output::step(1, 1, "Executing SQL...");
347 output::newline();
350 success("SQL executed successfully!");
351
352 Ok(())
353}
354
355#[derive(Debug)]
360struct SchemaChange {
361 description: String,
362 #[allow(dead_code)]
363 sql: String,
364 is_destructive: bool,
365}
366
367fn load_config(cwd: &PathBuf) -> CliResult<Config> {
368 let config_path = cwd.join(CONFIG_FILE_NAME);
369 if config_path.exists() {
370 Config::load(&config_path)
371 } else {
372 Ok(Config::default())
373 }
374}
375
376fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
377 prax_schema::validate_schema(content)
380 .map_err(|e| CliError::Schema(format!("Failed to parse/validate schema: {}", e)))
381}
382
383fn calculate_schema_changes(_schema: &prax_schema::ast::Schema) -> CliResult<Vec<SchemaChange>> {
384 Ok(Vec::new())
387}