1use std::path::Path;
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
31 let display_path = args
32 .schema
33 .as_deref()
34 .map(|p| p.display().to_string())
35 .unwrap_or_else(|| SCHEMA_FILE_PATH.to_string());
36 output::kv("Schema", &display_path);
37 output::kv(
38 "Database",
39 config
40 .database
41 .url
42 .as_deref()
43 .unwrap_or("env(DATABASE_URL)"),
44 );
45 output::newline();
46
47 output::step(1, 4, "Parsing schema...");
49 let loaded = crate::schema_loader::load_schema(args.schema.as_deref())?;
50 let schema = loaded.schema;
51
52 output::step(2, 4, "Introspecting database...");
54 output::step(3, 4, "Calculating changes...");
58 let changes = calculate_schema_changes(&schema)?;
59
60 if changes.is_empty() {
61 output::newline();
62 success("Database is already in sync with schema!");
63 return Ok(());
64 }
65
66 let destructive = changes.iter().any(|c| c.is_destructive);
68 if destructive && !args.accept_data_loss && !args.force {
69 output::newline();
70 warn("This push would cause data loss!");
71 output::section("Destructive changes");
72 for change in changes.iter().filter(|c| c.is_destructive) {
73 output::list_item(&format!("⚠️ {}", change.description));
74 }
75 output::newline();
76 output::info("Use --accept-data-loss to proceed, or --force to skip confirmation.");
77 return Ok(());
78 }
79
80 output::step(4, 4, "Applying changes...");
82 for change in &changes {
83 output::list_item(&change.description);
84 }
86
87 output::newline();
88 success(&format!("Applied {} changes to database!", changes.len()));
89
90 Ok(())
91}
92
93async fn run_pull(args: crate::cli::DbPullArgs) -> CliResult<()> {
95 output::header("Database Pull (Introspection)");
96
97 let cwd = std::env::current_dir()?;
98 let config = load_config(&cwd)?;
99
100 let database_url = get_database_url(&config)?;
102 let db_type = get_database_type(&config.database.provider)?;
103
104 output::kv("Provider", &config.database.provider);
105 output::kv("Database", &mask_database_url(&database_url));
106 if let Some(ref schema) = args.schema {
107 output::kv("Schema", schema);
108 }
109 output::newline();
110
111 let options = IntrospectionOptions {
113 schema: args.schema.clone(),
114 include_views: args.include_views,
115 include_materialized_views: args.include_materialized_views,
116 table_filter: args.tables.clone(),
117 exclude_pattern: args.exclude.clone(),
118 include_comments: args.comments,
119 sample_size: args.sample_size,
120 };
121
122 output::step(1, 3, "Introspecting database...");
124
125 #[cfg(feature = "postgres")]
126 let db_schema = {
127 use crate::commands::introspect::Introspector;
128 use crate::commands::introspect::postgres::PostgresIntrospector;
129
130 if config.database.provider.to_lowercase().contains("postgres") {
131 let introspector = PostgresIntrospector::new(database_url.clone());
132 introspector.introspect(&options).await?
133 } else {
134 return Err(CliError::Config(format!(
135 "Introspection for {} requires the corresponding feature. Compile with --features {}",
136 config.database.provider,
137 config.database.provider.to_lowercase()
138 )));
139 }
140 };
141
142 #[cfg(not(feature = "postgres"))]
143 let db_schema = {
144 return Err(CliError::Config(
145 "No database driver enabled. Compile with --features postgres, mysql, sqlite, or mssql"
146 .to_string(),
147 ));
148 };
149
150 output::step(2, 3, "Generating schema...");
152 let schema_content = match args.format {
153 OutputFormat::Prax => format_as_prax(&db_schema, &config),
154 OutputFormat::Json => format_as_json(&db_schema)?,
155 OutputFormat::Sql => format_as_sql(&db_schema, db_type),
156 };
157
158 output::step(3, 3, "Writing output...");
160
161 if args.print {
162 output::newline();
163 output::section("Generated Schema");
164 println!("{}", schema_content);
165 } else {
166 let output_path = args.output.unwrap_or_else(|| {
167 let ext = match args.format {
168 OutputFormat::Prax => "prax",
169 OutputFormat::Json => "json",
170 OutputFormat::Sql => "sql",
171 };
172 cwd.join(format!("schema.{}", ext))
173 });
174
175 if output_path.exists() && !args.force {
176 warn(&format!("{} already exists!", output_path.display()));
177 if !output::confirm("Overwrite existing file?") {
178 output::newline();
179 output::info("Pull cancelled.");
180 return Ok(());
181 }
182 }
183
184 std::fs::write(&output_path, &schema_content)?;
185
186 output::newline();
187 success(&format!("Schema written to {}", output_path.display()));
188 }
189
190 output::newline();
191 output::section("Summary");
192 output::kv("Tables", &db_schema.tables.len().to_string());
193 output::kv("Enums", &db_schema.enums.len().to_string());
194 output::kv("Views", &db_schema.views.len().to_string());
195
196 if !db_schema.tables.is_empty() {
198 output::newline();
199 output::section("Tables Introspected");
200 for table in &db_schema.tables {
201 output::list_item(&format!("{} ({} columns)", table.name, table.columns.len()));
202 }
203 }
204
205 Ok(())
206}
207
208async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
210 output::header("Database Seed");
211
212 let cwd = std::env::current_dir()?;
213 let config = load_config(&cwd)?;
214
215 if !args.force && !config.seed.should_seed(&args.environment) {
217 warn(&format!(
218 "Seeding is disabled for environment '{}'. Use --force to override.",
219 args.environment
220 ));
221 return Ok(());
222 }
223
224 let seed_path = args
226 .seed_file
227 .or_else(|| config.seed.script.clone())
228 .or_else(|| find_seed_file(&cwd, &config))
229 .ok_or_else(|| {
230 CliError::Config(
231 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
232 or specify with --seed-file".to_string()
233 )
234 })?;
235
236 if !seed_path.exists() {
237 return Err(CliError::Config(format!(
238 "Seed file not found: {}. Create a seed file or specify with --seed-file",
239 seed_path.display()
240 )));
241 }
242
243 let database_url = get_database_url(&config)?;
245
246 output::kv("Seed file", &seed_path.display().to_string());
247 output::kv("Database", &mask_database_url(&database_url));
248 output::kv("Provider", &config.database.provider);
249 output::kv("Environment", &args.environment);
250 output::newline();
251
252 if args.reset {
254 warn("Resetting database before seeding...");
255 output::newline();
257 }
258
259 let runner = SeedRunner::new(
261 seed_path,
262 database_url,
263 config.database.provider.clone(),
264 cwd,
265 )?
266 .with_environment(&args.environment)
267 .with_reset(args.reset);
268
269 let result = runner.run().await?;
270
271 output::newline();
272 success("Database seeded successfully!");
273
274 output::newline();
276 output::section("Summary");
277 output::kv("Records affected", &result.records_affected.to_string());
278 if !result.tables_seeded.is_empty() {
279 output::kv("Tables seeded", &result.tables_seeded.join(", "));
280 }
281
282 Ok(())
283}
284
285fn mask_database_url(url: &str) -> String {
287 if let Ok(parsed) = url::Url::parse(url) {
288 let mut masked = parsed.clone();
289 if parsed.password().is_some() {
290 let _ = masked.set_password(Some("****"));
291 }
292 masked.to_string()
293 } else {
294 if url.len() > 30 {
296 format!("{}...", &url[..30])
297 } else {
298 url.to_string()
299 }
300 }
301}
302
303async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
305 output::header("Execute SQL");
306
307 let cwd = std::env::current_dir()?;
308 let config = load_config(&cwd)?;
309
310 let sql = if let Some(sql) = args.sql {
312 sql
313 } else if let Some(file) = args.file {
314 std::fs::read_to_string(&file)?
315 } else if args.stdin {
316 let mut sql = String::new();
317 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
318 sql
319 } else {
320 return Err(CliError::Command(
321 "Must provide SQL via --sql, --file, or --stdin".to_string(),
322 ));
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 && !output::confirm("Execute this SQL?") {
341 output::newline();
342 output::info("Execution cancelled.");
343 return Ok(());
344 }
345
346 output::step(1, 1, "Executing SQL...");
348 output::newline();
351 success("SQL executed successfully!");
352
353 Ok(())
354}
355
356#[derive(Debug)]
361struct SchemaChange {
362 description: String,
363 #[allow(dead_code)]
364 sql: String,
365 is_destructive: bool,
366}
367
368fn load_config(cwd: &Path) -> CliResult<Config> {
369 let config_path = cwd.join(CONFIG_FILE_NAME);
370 if config_path.exists() {
371 Config::load(&config_path)
372 } else {
373 Ok(Config::default())
374 }
375}
376
377fn calculate_schema_changes(_schema: &prax_schema::ast::Schema) -> CliResult<Vec<SchemaChange>> {
378 Ok(Vec::new())
381}