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, 1, "Parsing schema...");
49 crate::schema_loader::load_schema(args.schema.as_deref())?;
50
51 Err(CliError::Command(
56 "`prax db push` is not yet implemented: computing schema changes requires database \
57 introspection and diffing, which are not available yet. Use \
58 `prax migrate dev --create-only` to generate migration SQL, then apply it with an \
59 external tool (psql, mysql, sqlite3)."
60 .to_string(),
61 ))
62}
63
64async fn run_pull(args: crate::cli::DbPullArgs) -> CliResult<()> {
66 output::header("Database Pull (Introspection)");
67
68 let cwd = std::env::current_dir()?;
69 let config = load_config(&cwd)?;
70
71 let database_url = get_database_url(&config)?;
73 let db_type = get_database_type(&config.database.provider)?;
74
75 output::kv("Provider", &config.database.provider);
76 output::kv("Database", &mask_database_url(&database_url));
77 if let Some(ref schema) = args.schema {
78 output::kv("Schema", schema);
79 }
80 output::newline();
81
82 let options = IntrospectionOptions {
84 schema: args.schema.clone(),
85 include_views: args.include_views,
86 include_materialized_views: args.include_materialized_views,
87 table_filter: args.tables.clone(),
88 exclude_pattern: args.exclude.clone(),
89 include_comments: args.comments,
90 sample_size: args.sample_size,
91 };
92
93 output::step(1, 3, "Introspecting database...");
99
100 let db_schema = crate::commands::introspect::introspect_database(
101 &config.database.provider,
102 &database_url,
103 &options,
104 )
105 .await?;
106
107 output::step(2, 3, "Generating schema...");
109 let schema_content = match args.format {
110 OutputFormat::Prax => format_as_prax(&db_schema, &config),
111 OutputFormat::Json => format_as_json(&db_schema)?,
112 OutputFormat::Sql => format_as_sql(&db_schema, db_type),
113 };
114
115 output::step(3, 3, "Writing output...");
117
118 if args.print {
119 output::newline();
120 output::section("Generated Schema");
121 println!("{}", schema_content);
122 } else {
123 let output_path = args.output.unwrap_or_else(|| {
124 let ext = match args.format {
125 OutputFormat::Prax => "prax",
126 OutputFormat::Json => "json",
127 OutputFormat::Sql => "sql",
128 };
129 cwd.join(format!("schema.{}", ext))
130 });
131
132 if output_path.exists() && !args.force {
133 warn(&format!("{} already exists!", output_path.display()));
134 if !output::confirm("Overwrite existing file?") {
135 output::newline();
136 output::info("Pull cancelled.");
137 return Ok(());
138 }
139 }
140
141 std::fs::write(&output_path, &schema_content)?;
142
143 output::newline();
144 success(&format!("Schema written to {}", output_path.display()));
145 }
146
147 output::newline();
148 output::section("Summary");
149 output::kv("Tables", &db_schema.tables.len().to_string());
150 output::kv("Enums", &db_schema.enums.len().to_string());
151 output::kv("Views", &db_schema.views.len().to_string());
152
153 if !db_schema.tables.is_empty() {
155 output::newline();
156 output::section("Tables Introspected");
157 for table in &db_schema.tables {
158 output::list_item(&format!("{} ({} columns)", table.name, table.columns.len()));
159 }
160 }
161
162 Ok(())
163}
164
165async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
167 output::header("Database Seed");
168
169 let cwd = std::env::current_dir()?;
170 let config = load_config(&cwd)?;
171
172 if !args.force && !config.seed.should_seed(&args.environment) {
174 warn(&format!(
175 "Seeding is disabled for environment '{}'. Use --force to override.",
176 args.environment
177 ));
178 return Ok(());
179 }
180
181 let seed_path = args
183 .seed_file
184 .or_else(|| config.seed.script.clone())
185 .or_else(|| find_seed_file(&cwd, &config))
186 .ok_or_else(|| {
187 CliError::Config(
188 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
189 or specify with --seed-file".to_string()
190 )
191 })?;
192
193 if !seed_path.exists() {
194 return Err(CliError::Config(format!(
195 "Seed file not found: {}. Create a seed file or specify with --seed-file",
196 seed_path.display()
197 )));
198 }
199
200 let database_url = get_database_url(&config)?;
202
203 output::kv("Seed file", &seed_path.display().to_string());
204 output::kv("Database", &mask_database_url(&database_url));
205 output::kv("Provider", &config.database.provider);
206 output::kv("Environment", &args.environment);
207 output::newline();
208
209 let runner = SeedRunner::new(
211 seed_path,
212 database_url,
213 config.database.provider.clone(),
214 cwd,
215 )?
216 .with_environment(&args.environment)
217 .with_reset(args.reset);
218
219 let result = runner.run().await?;
220
221 output::newline();
222 success("Database seeded successfully!");
223
224 output::newline();
226 output::section("Summary");
227 output::kv("Records affected", &result.records_affected.to_string());
228 if !result.tables_seeded.is_empty() {
229 output::kv("Tables seeded", &result.tables_seeded.join(", "));
230 }
231
232 Ok(())
233}
234
235fn mask_database_url(url: &str) -> String {
237 if let Ok(parsed) = url::Url::parse(url) {
238 let mut masked = parsed.clone();
239 if parsed.password().is_some() {
240 let _ = masked.set_password(Some("****"));
241 }
242 masked.to_string()
243 } else {
244 if url.len() > 30 {
246 format!("{}...", &url[..30])
247 } else {
248 url.to_string()
249 }
250 }
251}
252
253async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
255 output::header("Execute SQL");
256
257 let cwd = std::env::current_dir()?;
258 let config = load_config(&cwd)?;
259
260 let sql = if let Some(sql) = args.sql {
262 sql
263 } else if let Some(file) = args.file {
264 std::fs::read_to_string(&file)?
265 } else if args.stdin {
266 let mut sql = String::new();
267 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
268 sql
269 } else {
270 return Err(CliError::Command(
271 "Must provide SQL via --sql, --file, or --stdin".to_string(),
272 ));
273 };
274
275 output::kv(
276 "Database",
277 config
278 .database
279 .url
280 .as_deref()
281 .unwrap_or("env(DATABASE_URL)"),
282 );
283 output::newline();
284
285 output::section("SQL");
286 output::code(&sql, "sql");
287 output::newline();
288
289 Err(CliError::Command(
294 "`prax db execute` is not yet implemented: the CLI has no SQL execution path wired \
295 up yet. Run this SQL with your database's native client (psql, mysql, sqlite3)."
296 .to_string(),
297 ))
298}
299
300fn load_config(cwd: &Path) -> CliResult<Config> {
305 let config_path = cwd.join(CONFIG_FILE_NAME);
306 if config_path.exists() {
307 Config::load(&config_path)
308 } else {
309 Ok(Config::default())
310 }
311}