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...");
95
96 #[cfg(feature = "postgres")]
97 let db_schema = {
98 use crate::commands::introspect::Introspector;
99 use crate::commands::introspect::postgres::PostgresIntrospector;
100
101 if config.database.provider.to_lowercase().contains("postgres") {
102 let introspector = PostgresIntrospector::new(database_url.clone());
103 introspector.introspect(&options).await?
104 } else {
105 return Err(CliError::Config(format!(
106 "Introspection (`prax db pull`) currently supports PostgreSQL only; provider \
107 '{}' is not supported yet.",
108 config.database.provider
109 )));
110 }
111 };
112
113 #[cfg(not(feature = "postgres"))]
114 let db_schema = {
115 return Err(CliError::Config(
116 "Introspection (`prax db pull`) currently supports PostgreSQL only and requires \
117 the `postgres` feature: recompile with --features postgres."
118 .to_string(),
119 ));
120 };
121
122 output::step(2, 3, "Generating schema...");
124 let schema_content = match args.format {
125 OutputFormat::Prax => format_as_prax(&db_schema, &config),
126 OutputFormat::Json => format_as_json(&db_schema)?,
127 OutputFormat::Sql => format_as_sql(&db_schema, db_type),
128 };
129
130 output::step(3, 3, "Writing output...");
132
133 if args.print {
134 output::newline();
135 output::section("Generated Schema");
136 println!("{}", schema_content);
137 } else {
138 let output_path = args.output.unwrap_or_else(|| {
139 let ext = match args.format {
140 OutputFormat::Prax => "prax",
141 OutputFormat::Json => "json",
142 OutputFormat::Sql => "sql",
143 };
144 cwd.join(format!("schema.{}", ext))
145 });
146
147 if output_path.exists() && !args.force {
148 warn(&format!("{} already exists!", output_path.display()));
149 if !output::confirm("Overwrite existing file?") {
150 output::newline();
151 output::info("Pull cancelled.");
152 return Ok(());
153 }
154 }
155
156 std::fs::write(&output_path, &schema_content)?;
157
158 output::newline();
159 success(&format!("Schema written to {}", output_path.display()));
160 }
161
162 output::newline();
163 output::section("Summary");
164 output::kv("Tables", &db_schema.tables.len().to_string());
165 output::kv("Enums", &db_schema.enums.len().to_string());
166 output::kv("Views", &db_schema.views.len().to_string());
167
168 if !db_schema.tables.is_empty() {
170 output::newline();
171 output::section("Tables Introspected");
172 for table in &db_schema.tables {
173 output::list_item(&format!("{} ({} columns)", table.name, table.columns.len()));
174 }
175 }
176
177 Ok(())
178}
179
180async fn run_seed(args: crate::cli::DbSeedArgs) -> CliResult<()> {
182 output::header("Database Seed");
183
184 let cwd = std::env::current_dir()?;
185 let config = load_config(&cwd)?;
186
187 if !args.force && !config.seed.should_seed(&args.environment) {
189 warn(&format!(
190 "Seeding is disabled for environment '{}'. Use --force to override.",
191 args.environment
192 ));
193 return Ok(());
194 }
195
196 let seed_path = args
198 .seed_file
199 .or_else(|| config.seed.script.clone())
200 .or_else(|| find_seed_file(&cwd, &config))
201 .ok_or_else(|| {
202 CliError::Config(
203 "Seed file not found. Create a seed file (seed.rs, seed.sql, seed.json, or seed.toml) \
204 or specify with --seed-file".to_string()
205 )
206 })?;
207
208 if !seed_path.exists() {
209 return Err(CliError::Config(format!(
210 "Seed file not found: {}. Create a seed file or specify with --seed-file",
211 seed_path.display()
212 )));
213 }
214
215 let database_url = get_database_url(&config)?;
217
218 output::kv("Seed file", &seed_path.display().to_string());
219 output::kv("Database", &mask_database_url(&database_url));
220 output::kv("Provider", &config.database.provider);
221 output::kv("Environment", &args.environment);
222 output::newline();
223
224 let runner = SeedRunner::new(
226 seed_path,
227 database_url,
228 config.database.provider.clone(),
229 cwd,
230 )?
231 .with_environment(&args.environment)
232 .with_reset(args.reset);
233
234 let result = runner.run().await?;
235
236 output::newline();
237 success("Database seeded successfully!");
238
239 output::newline();
241 output::section("Summary");
242 output::kv("Records affected", &result.records_affected.to_string());
243 if !result.tables_seeded.is_empty() {
244 output::kv("Tables seeded", &result.tables_seeded.join(", "));
245 }
246
247 Ok(())
248}
249
250fn mask_database_url(url: &str) -> String {
252 if let Ok(parsed) = url::Url::parse(url) {
253 let mut masked = parsed.clone();
254 if parsed.password().is_some() {
255 let _ = masked.set_password(Some("****"));
256 }
257 masked.to_string()
258 } else {
259 if url.len() > 30 {
261 format!("{}...", &url[..30])
262 } else {
263 url.to_string()
264 }
265 }
266}
267
268async fn run_execute(args: crate::cli::DbExecuteArgs) -> CliResult<()> {
270 output::header("Execute SQL");
271
272 let cwd = std::env::current_dir()?;
273 let config = load_config(&cwd)?;
274
275 let sql = if let Some(sql) = args.sql {
277 sql
278 } else if let Some(file) = args.file {
279 std::fs::read_to_string(&file)?
280 } else if args.stdin {
281 let mut sql = String::new();
282 std::io::Read::read_to_string(&mut std::io::stdin(), &mut sql)?;
283 sql
284 } else {
285 return Err(CliError::Command(
286 "Must provide SQL via --sql, --file, or --stdin".to_string(),
287 ));
288 };
289
290 output::kv(
291 "Database",
292 config
293 .database
294 .url
295 .as_deref()
296 .unwrap_or("env(DATABASE_URL)"),
297 );
298 output::newline();
299
300 output::section("SQL");
301 output::code(&sql, "sql");
302 output::newline();
303
304 Err(CliError::Command(
309 "`prax db execute` is not yet implemented: the CLI has no SQL execution path wired \
310 up yet. Run this SQL with your database's native client (psql, mysql, sqlite3)."
311 .to_string(),
312 ))
313}
314
315fn load_config(cwd: &Path) -> CliResult<Config> {
320 let config_path = cwd.join(CONFIG_FILE_NAME);
321 if config_path.exists() {
322 Config::load(&config_path)
323 } else {
324 Ok(Config::default())
325 }
326}