1use std::path::PathBuf;
4
5use crate::cli::DbArgs;
6use crate::commands::seed::{find_seed_file, get_database_url, SeedRunner};
7use crate::config::{Config, CONFIG_FILE_NAME, SCHEMA_FILE_NAME};
8use crate::error::{CliError, CliResult};
9use crate::output::{self, success, warn};
10
11pub async fn run(args: DbArgs) -> CliResult<()> {
13 match args.command {
14 crate::cli::DbSubcommand::Push(push_args) => run_push(push_args).await,
15 crate::cli::DbSubcommand::Pull(pull_args) => run_pull(pull_args).await,
16 crate::cli::DbSubcommand::Seed(seed_args) => run_seed(seed_args).await,
17 crate::cli::DbSubcommand::Execute(exec_args) => run_execute(exec_args).await,
18 }
19}
20
21async fn run_push(args: crate::cli::DbPushArgs) -> CliResult<()> {
23 output::header("Database Push");
24
25 let cwd = std::env::current_dir()?;
26 let config = load_config(&cwd)?;
27 let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_NAME));
28
29 output::kv("Schema", &schema_path.display().to_string());
30 output::kv("Database", config.database.url.as_deref().unwrap_or("env(DATABASE_URL)"));
31 output::newline();
32
33 output::step(1, 4, "Parsing schema...");
35 let schema_content = std::fs::read_to_string(&schema_path)?;
36 let schema = parse_schema(&schema_content)?;
37
38 output::step(2, 4, "Introspecting database...");
40 output::step(3, 4, "Calculating changes...");
44 let changes = calculate_schema_changes(&schema)?;
45
46 if changes.is_empty() {
47 output::newline();
48 success("Database is already in sync with schema!");
49 return Ok(());
50 }
51
52 let destructive = changes.iter().any(|c| c.is_destructive);
54 if destructive && !args.accept_data_loss && !args.force {
55 output::newline();
56 warn("This push would cause data loss!");
57 output::section("Destructive changes");
58 for change in changes.iter().filter(|c| c.is_destructive) {
59 output::list_item(&format!("⚠️ {}", change.description));
60 }
61 output::newline();
62 output::info("Use --accept-data-loss to proceed, or --force to skip confirmation.");
63 return Ok(());
64 }
65
66 output::step(4, 4, "Applying changes...");
68 for change in &changes {
69 output::list_item(&change.description);
70 }
72
73 output::newline();
74 success(&format!("Applied {} changes to database!", changes.len()));
75
76 Ok(())
77}
78
79async fn run_pull(args: crate::cli::DbPullArgs) -> CliResult<()> {
81 output::header("Database Pull");
82
83 let cwd = std::env::current_dir()?;
84 let config = load_config(&cwd)?;
85
86 output::kv("Database", config.database.url.as_deref().unwrap_or("env(DATABASE_URL)"));
87 output::newline();
88
89 output::step(1, 3, "Introspecting database...");
91 let schema = introspect_database(&config).await?;
92
93 output::step(2, 3, "Generating schema...");
95 let schema_content = generate_schema_file(&schema)?;
96
97 output::step(3, 3, "Writing schema file...");
99 let output_path = args.output.unwrap_or_else(|| cwd.join(SCHEMA_FILE_NAME));
100
101 if output_path.exists() && !args.force {
102 warn(&format!("{} already exists!", output_path.display()));
103 if !output::confirm("Overwrite existing schema?") {
104 output::newline();
105 output::info("Pull cancelled.");
106 return Ok(());
107 }
108 }
109
110 std::fs::write(&output_path, &schema_content)?;
111
112 output::newline();
113 success(&format!(
114 "Schema written to {}",
115 output_path.display()
116 ));
117
118 output::newline();
119 output::section("Introspected");
120 output::kv("Models", &schema.models.len().to_string());
121 output::kv("Enums", &schema.enums.len().to_string());
122
123 Ok(())
124}
125
126async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
128 output::header("Database Seed");
129
130 let cwd = std::env::current_dir()?;
131 let config = load_config(&cwd)?;
132
133 if !args.force && !config.seed.should_seed(&args.environment) {
135 warn(&format!(
136 "Seeding is disabled for environment '{}'. Use --force to override.",
137 args.environment
138 ));
139 return Ok(());
140 }
141
142 let seed_path = args
144 .seed_file
145 .or_else(|| config.seed.script.clone())
146 .or_else(|| find_seed_file(&cwd, &config))
147 .ok_or_else(|| {
148 CliError::Config(
149 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
150 or specify with --seed-file".to_string()
151 )
152 })?;
153
154 if !seed_path.exists() {
155 return Err(CliError::Config(format!(
156 "Seed file not found: {}. Create a seed file or specify with --seed-file",
157 seed_path.display()
158 )));
159 }
160
161 let database_url = get_database_url(&config)?;
163
164 output::kv("Seed file", &seed_path.display().to_string());
165 output::kv("Database", &mask_database_url(&database_url));
166 output::kv("Provider", &config.database.provider);
167 output::kv("Environment", &args.environment);
168 output::newline();
169
170 if args.reset {
172 warn("Resetting database before seeding...");
173 output::newline();
175 }
176
177 let runner = SeedRunner::new(
179 seed_path,
180 database_url,
181 config.database.provider.clone(),
182 cwd,
183 )?
184 .with_environment(&args.environment)
185 .with_reset(args.reset);
186
187 let result = runner.run().await?;
188
189 output::newline();
190 success("Database seeded successfully!");
191
192 output::newline();
194 output::section("Summary");
195 output::kv("Records affected", &result.records_affected.to_string());
196 if !result.tables_seeded.is_empty() {
197 output::kv("Tables seeded", &result.tables_seeded.join(", "));
198 }
199
200 Ok(())
201}
202
203fn mask_database_url(url: &str) -> String {
205 if let Ok(parsed) = url::Url::parse(url) {
206 let mut masked = parsed.clone();
207 if parsed.password().is_some() {
208 let _ = masked.set_password(Some("****"));
209 }
210 masked.to_string()
211 } else {
212 if url.len() > 30 {
214 format!("{}...", &url[..30])
215 } else {
216 url.to_string()
217 }
218 }
219}
220
221async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
223 output::header("Execute SQL");
224
225 let cwd = std::env::current_dir()?;
226 let config = load_config(&cwd)?;
227
228 let sql = if let Some(sql) = args.sql {
230 sql
231 } else if let Some(file) = args.file {
232 std::fs::read_to_string(&file)?
233 } else if args.stdin {
234 let mut sql = String::new();
235 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
236 sql
237 } else {
238 return Err(CliError::Command(
239 "Must provide SQL via --sql, --file, or --stdin".to_string()
240 ).into());
241 };
242
243 output::kv("Database", config.database.url.as_deref().unwrap_or("env(DATABASE_URL)"));
244 output::newline();
245
246 output::section("SQL");
247 output::code(&sql, "sql");
248 output::newline();
249
250 if !args.force {
252 if !output::confirm("Execute this SQL?") {
253 output::newline();
254 output::info("Execution cancelled.");
255 return Ok(());
256 }
257 }
258
259 output::step(1, 1, "Executing SQL...");
261 output::newline();
264 success("SQL executed successfully!");
265
266 Ok(())
267}
268
269#[derive(Debug)]
274struct SchemaChange {
275 description: String,
276 #[allow(dead_code)]
277 sql: String,
278 is_destructive: bool,
279}
280
281fn load_config(cwd: &PathBuf) -> CliResult<Config> {
282 let config_path = cwd.join(CONFIG_FILE_NAME);
283 if config_path.exists() {
284 Config::load(&config_path)
285 } else {
286 Ok(Config::default())
287 }
288}
289
290fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
291 prax_schema::parse_schema(content)
292 .map_err(|e| CliError::Schema(format!("Failed to parse schema: {}", e)))
293}
294
295fn calculate_schema_changes(
296 _schema: &prax_schema::ast::Schema,
297) -> CliResult<Vec<SchemaChange>> {
298 Ok(Vec::new())
301}
302
303async fn introspect_database(_config: &Config) -> CliResult<prax_schema::ast::Schema> {
304 Ok(prax_schema::ast::Schema::default())
307}
308
309fn generate_schema_file(schema: &prax_schema::ast::Schema) -> CliResult<String> {
310 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
311
312 let mut output = String::new();
313
314 output.push_str("// Generated by `prax db pull`\n");
315 output.push_str("// Edit this file to customize your schema\n\n");
316
317 output.push_str("datasource db {\n");
318 output.push_str(" provider = \"postgresql\"\n");
319 output.push_str(" url = env(\"DATABASE_URL\")\n");
320 output.push_str("}\n\n");
321
322 output.push_str("generator client {\n");
323 output.push_str(" provider = \"prax-client-rust\"\n");
324 output.push_str(" output = \"./src/generated\"\n");
325 output.push_str("}\n\n");
326
327 for model in schema.models.values() {
329 output.push_str(&format!("model {} {{\n", model.name()));
330 for field in model.fields.values() {
331 let field_type = format_field_type(&field.field_type, field.modifier);
332 output.push_str(&format!(" {} {}\n", field.name(), field_type));
333 }
334 output.push_str("}\n\n");
335 }
336
337 for enum_def in schema.enums.values() {
339 output.push_str(&format!("enum {} {{\n", enum_def.name()));
340 for variant in &enum_def.variants {
341 output.push_str(&format!(" {}\n", variant.name()));
342 }
343 output.push_str("}\n\n");
344 }
345
346 return Ok(output);
347
348 fn format_field_type(field_type: &FieldType, modifier: TypeModifier) -> String {
349 let base = match field_type {
350 FieldType::Scalar(scalar) => match scalar {
351 ScalarType::Int => "Int",
352 ScalarType::BigInt => "BigInt",
353 ScalarType::Float => "Float",
354 ScalarType::String => "String",
355 ScalarType::Boolean => "Boolean",
356 ScalarType::DateTime => "DateTime",
357 ScalarType::Date => "Date",
358 ScalarType::Time => "Time",
359 ScalarType::Json => "Json",
360 ScalarType::Bytes => "Bytes",
361 ScalarType::Decimal => "Decimal",
362 ScalarType::Uuid => "Uuid",
363 ScalarType::Cuid => "Cuid",
364 ScalarType::Cuid2 => "Cuid2",
365 ScalarType::NanoId => "NanoId",
366 ScalarType::Ulid => "Ulid",
367 }
368 .to_string(),
369 FieldType::Model(name) => name.to_string(),
370 FieldType::Enum(name) => name.to_string(),
371 FieldType::Composite(name) => name.to_string(),
372 FieldType::Unsupported(name) => format!("Unsupported(\"{}\")", name),
373 };
374
375 match modifier {
376 TypeModifier::Optional => format!("{}?", base),
377 TypeModifier::List => format!("{}[]", base),
378 TypeModifier::OptionalList => format!("{}[]?", base),
379 TypeModifier::Required => base,
380 }
381 }
382}