1use anyhow::Result;
2use std::path::Path;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use super::InitArgs;
6use super::import::{ImportSource, import_schema};
7use super::project::{create_project_structure, generate_config_file};
8use super::prompts::{gather_init_options_with_args, prompt_baseline_creation};
9use crate::baseline::operations::{
10 BaselineCreationRequest, create_baseline, display_baseline_summary,
11};
12use crate::catalog::Catalog;
13use crate::config::load_config;
14use crate::constants::CONFIG_FILENAME;
15use crate::db::connection::mask_url_password;
16use crate::migration_tracking;
17use crate::prompts::ShadowDatabaseInput;
18
19#[derive(Debug)]
21pub enum ExistingConfigResult {
22 NotFound,
24 Update(Box<crate::config::types::ConfigInput>),
26 Fresh,
28 Cancelled,
30}
31
32pub fn check_existing_config(
34 project_dir: &Path,
35 force_fresh: bool,
36) -> Result<ExistingConfigResult> {
37 let config_path = project_dir.join(CONFIG_FILENAME);
38
39 if !config_path.exists() {
40 return Ok(ExistingConfigResult::NotFound);
41 }
42
43 if force_fresh {
45 println!(
46 "ā ļø Existing {} will be overwritten (--fresh flag)\n",
47 CONFIG_FILENAME
48 );
49 return Ok(ExistingConfigResult::Fresh);
50 }
51
52 let config_path_str = config_path.to_string_lossy();
54 let (existing_config, _) = load_config(&config_path_str)?;
55
56 let databases = existing_config.databases.as_ref();
58 let directories = existing_config.directories.as_ref();
59 println!("š Existing configuration found:\n");
60 if let Some(url) = databases.and_then(|d| d.dev_url.as_ref()) {
61 println!(" Database: {}", mask_url_password(url));
62 }
63 if let Some(schema_dir) = directories.and_then(|d| d.schema_dir.as_ref()) {
64 println!(" Schema dir: {}", schema_dir);
65 }
66 if let Some(migrations_dir) = directories.and_then(|d| d.migrations_dir.as_ref()) {
67 println!(" Migrations: {}", migrations_dir);
68 }
69 if let Some(baselines_dir) = directories.and_then(|d| d.baselines_dir.as_ref()) {
70 println!(" Baselines: {}", baselines_dir);
71 }
72 if let Some(pg_version) = databases
73 .and_then(|d| d.shadow.as_ref())
74 .and_then(|s| s.docker.as_ref())
75 .and_then(|d| d.version.as_ref())
76 {
77 println!(" Shadow PG: {}", pg_version);
78 }
79 println!();
80
81 let choices = vec![
83 "Update - modify existing configuration",
84 "Fresh - start over with new configuration",
85 "Cancel - keep current configuration",
86 ];
87
88 let selection = dialoguer::Select::new()
89 .with_prompt("What would you like to do?")
90 .items(&choices)
91 .default(0)
92 .interact()?;
93
94 match selection {
95 0 => {
96 println!("\nāļø Update mode: existing values will be shown as defaults\n");
97 Ok(ExistingConfigResult::Update(Box::new(existing_config)))
98 }
99 1 => {
100 println!("\nš Fresh mode: creating new configuration\n");
101 Ok(ExistingConfigResult::Fresh)
102 }
103 _ => {
104 println!("\nā Keeping existing configuration");
105 Ok(ExistingConfigResult::Cancelled)
106 }
107 }
108}
109
110#[derive(Debug)]
112pub struct InitOptions {
113 pub project_dir: std::path::PathBuf,
114 pub dev_database_url: String,
115 pub shadow_config: ShadowDatabaseInput,
116 pub shadow_pg_version: Option<String>,
119 pub detected_pg_version: Option<String>,
121 pub schema_dir: std::path::PathBuf,
122 pub migrations_dir: String,
123 pub baselines_dir: String,
124 pub import_source: Option<ImportSource>,
125 pub object_config: ObjectManagementConfig,
126 pub baseline_config: BaselineCreationConfig,
127 #[allow(dead_code)]
128 pub tracking_table: crate::config::types::TrackingTable,
129 pub roles_file: Option<String>,
131 pub objects: crate::config::types::Objects,
134 pub substrate_exclusions: Vec<String>,
137}
138
139#[derive(Debug, Clone)]
141pub struct ObjectManagementConfig {
142 pub comments: bool,
143 pub grants: bool,
144 pub triggers: bool,
145 pub extensions: bool,
146}
147
148impl Default for ObjectManagementConfig {
149 fn default() -> Self {
150 Self {
151 comments: true,
152 grants: true,
153 triggers: true,
154 extensions: true,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Default)]
161pub struct BaselineCreationConfig {
162 pub create_baseline: Option<bool>,
164 pub description: Option<String>,
166}
167
168pub async fn cmd_init_with_args(args: &InitArgs) -> Result<()> {
170 println!("š Welcome to pgmt! Let's set up your PostgreSQL migration project.\n");
171
172 let project_dir = std::env::current_dir()?;
174 let existing_config = check_existing_config(&project_dir, args.fresh)?;
175
176 let existing_input = match existing_config {
178 ExistingConfigResult::NotFound | ExistingConfigResult::Fresh => None,
179 ExistingConfigResult::Update(config) => Some(*config),
180 ExistingConfigResult::Cancelled => {
181 return Ok(());
182 }
183 };
184
185 let mut options = gather_init_options_with_args(args, existing_input.as_ref()).await?;
187
188 if !args.defaults {
190 let confirmed = super::prompts::prompt_project_confirmation(&options)?;
191 if !confirmed {
192 println!("ā Project initialization cancelled by user.");
193 return Ok(());
194 }
195 }
196
197 println!("šļø Creating project structure...");
199 create_project_structure(&options)?;
200 println!("ā
Project directories created");
201
202 let catalog = if let Some(import_source) = options.import_source.clone() {
204 match import_catalog_from_source(&import_source, &options).await? {
205 Some((catalog, substrate_exclusions)) => {
206 if !substrate_exclusions.is_empty() {
207 options
208 .objects
209 .exclude
210 .schemas
211 .extend(substrate_exclusions.iter().cloned());
212 options.substrate_exclusions = substrate_exclusions;
213 }
214 let filter = crate::config::filter::ObjectFilter::new(
218 &options.objects,
219 &options.tracking_table,
220 );
221 Some(filter.filter_catalog(catalog))
222 }
223 None => None,
224 }
225 } else {
226 None
227 };
228
229 if let Some(ref cat) = catalog {
231 show_catalog_preview(cat);
233
234 if !args.defaults {
236 options.object_config =
237 super::prompts::prompt_object_management_config_with_context(cat)?;
238 }
239 } else if !args.defaults {
240 options.object_config = super::prompts::prompt_object_management_config()?;
242 }
243
244 let baseline_result = if let Some(ref cat) = catalog {
246 process_imported_catalog(cat, &options).await?
247 } else {
248 BaselineResult::NotRequested
249 };
250
251 println!("š Generating configuration file...");
253 generate_config_file(&options, existing_input.as_ref(), &options.project_dir)?;
254 println!("ā
pgmt.yaml created");
255
256 print_success_summary(&options, &baseline_result);
258
259 Ok(())
260}
261
262fn resolve_shadow_database(
264 shadow_config: &ShadowDatabaseInput,
265 shadow_pg_version: Option<&String>,
266 detected_pg_version: Option<&String>,
267) -> crate::config::types::ShadowDatabase {
268 use crate::config::types::{ShadowDatabase, ShadowDockerConfig};
269
270 match shadow_config {
271 ShadowDatabaseInput::Auto => {
272 let version = shadow_pg_version.or(detected_pg_version);
274 if let Some(v) = version {
275 let major_version = crate::prompts::extract_major_version(v);
276 ShadowDatabase::Docker(ShadowDockerConfig {
277 version: Some(major_version),
278 ..Default::default()
279 })
280 } else {
281 ShadowDatabase::Auto
282 }
283 }
284 ShadowDatabaseInput::Docker { image, platform } => {
285 ShadowDatabase::Docker(ShadowDockerConfig {
286 image: image.clone(),
287 platform: platform.clone(),
288 ..Default::default()
289 })
290 }
291 ShadowDatabaseInput::Manual(url) => ShadowDatabase::Url {
292 url: url.clone(),
293 reset: crate::config::types::ShadowResetMode::default(),
294 },
295 }
296}
297
298async fn import_catalog_from_source(
301 import_source: &ImportSource,
302 options: &InitOptions,
303) -> Result<Option<(Catalog, Vec<String>)>> {
304 use crate::config::types::{ShadowDatabase, ShadowResetMode};
305
306 println!("š„ Importing existing schema...");
307 println!(" Source: {}", import_source.description());
308
309 let shadow_database = resolve_shadow_database(
311 &options.shadow_config,
312 options.shadow_pg_version.as_ref(),
313 options.detected_pg_version.as_ref(),
314 );
315
316 let sql_source = matches!(
317 import_source,
318 ImportSource::SqlFile(_) | ImportSource::Directory(_)
319 );
320
321 if let ShadowDatabase::Url {
325 url,
326 reset: ShadowResetMode::Clean,
327 } = &shadow_database
328 && sql_source
329 {
330 println!(
331 "ā ļø The shadow database at {} will be reset: every schema pgmt manages will be dropped before the import.",
332 crate::db::connection::mask_url_password(url)
333 );
334 let confirmed = dialoguer::Confirm::new()
335 .with_prompt(" Reset this database and continue?")
336 .default(false)
337 .interact()?;
338 if !confirmed {
339 return Err(anyhow::anyhow!(
340 "import cancelled ā shadow database left untouched"
341 ));
342 }
343 }
344
345 let (shadow_url, substrate_exclusions) = if sql_source {
349 let shadow_url = shadow_database.get_connection_string().await?;
350
351 let branch_backed = !matches!(
354 shadow_database,
355 ShadowDatabase::Url {
356 reset: ShadowResetMode::Clean,
357 ..
358 }
359 );
360 let exclusions = if branch_backed {
361 let substrate = fetch_substrate_schemas(&shadow_url).await?;
362 if substrate.is_empty() {
363 Vec::new()
364 } else {
365 super::prompts::prompt_substrate_exclusions(&substrate)?
366 }
367 } else {
368 Vec::new()
369 };
370 (shadow_url, exclusions)
371 } else {
372 (String::new(), Vec::new())
373 };
374
375 let roles_path = options
377 .roles_file
378 .as_ref()
379 .map(|f| options.project_dir.join(f));
380
381 match import_schema(
382 import_source.clone(),
383 &shadow_url,
384 roles_path.as_deref(),
385 &options.objects,
386 )
387 .await
388 {
389 Ok(catalog) => {
390 println!("ā
Schema import completed");
391 Ok(Some((catalog, substrate_exclusions)))
392 }
393 Err(e) => {
394 eprintln!("\nā ļø Schema import failed:\n{:#}", e);
396 eprintln!("\nš§ What would you like to do?");
397
398 let recovery_options = vec![
399 "Skip import and continue with empty project",
400 "Exit setup (you can run 'pgmt init' again later)",
401 ];
402
403 let choice = dialoguer::Select::new()
404 .with_prompt("Choose an option")
405 .items(&recovery_options)
406 .default(0)
407 .interact()?;
408
409 match choice {
410 0 => {
411 println!(
412 "ā ļø Skipping schema import. You can add schema files manually later."
413 );
414 println!(
415 " š” Tip: You can also try importing again with 'pgmt apply' after setup."
416 );
417 eprintln!(" Continuing with empty project setup...");
418 Ok(None)
419 }
420 1 => {
421 println!("ā Setup cancelled. Run 'pgmt init' again when ready.");
422 std::process::exit(1);
423 }
424 _ => Ok(None),
425 }
426 }
427 }
428}
429
430pub async fn fetch_substrate_schemas(shadow_url: &str) -> Result<Vec<String>> {
435 let pool = crate::db::connection::connect_with_retry_quiet(shadow_url).await?;
436 let rows: Vec<(String,)> = sqlx::query_as(
437 "SELECT nspname FROM pg_namespace
438 WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public')
439 AND nspname NOT LIKE 'pg_temp_%'
440 AND nspname NOT LIKE 'pg_toast_temp_%'
441 ORDER BY nspname",
442 )
443 .fetch_all(&pool)
444 .await?;
445 pool.close().await;
446 Ok(rows.into_iter().map(|(n,)| n).collect())
447}
448
449#[derive(Debug, Clone)]
451pub enum BaselineResult {
452 NotRequested,
454 Created,
456 NeedsAttention { reason: String },
458 Failed(String),
460}
461
462async fn process_imported_catalog(
464 catalog: &Catalog,
465 options: &InitOptions,
466) -> Result<BaselineResult> {
467 let total_objects = count_catalog_objects(catalog);
468
469 if total_objects == 0 {
470 println!("ā ļø No database objects found in the imported schema.");
471 println!(" Continuing with empty schema directory...");
472 return Ok(BaselineResult::NotRequested);
473 }
474
475 println!("\nš Generating schema files from your database...");
477 let file_count = match generate_schema_files(catalog, options).await {
478 Ok(count) => count,
479 Err(e) => {
480 eprintln!("ā Schema file generation failed: {}", e);
481 return Ok(BaselineResult::Failed(e.to_string()));
482 }
483 };
484 println!("ā
Generated {} schema files", file_count);
485
486 println!("\nš Validating schema files...");
488 let schema_dir = options.project_dir.join(&options.schema_dir);
489 let roles_path = options
490 .roles_file
491 .as_ref()
492 .map(|f| options.project_dir.join(f));
493
494 match validate_schema_files(
495 &schema_dir,
496 roles_path.as_deref(),
497 &options.shadow_config,
498 options.shadow_pg_version.as_ref(),
499 options.detected_pg_version.as_ref(),
500 )
501 .await
502 {
503 Ok(_) => {
504 println!("ā
Schema validation passed");
505 }
506 Err(e) => {
507 let error_str = format!("{:#}", e);
508
509 if error_str.contains("Circular dependency") {
511 println!("\nš Circular dependency detected in schema files");
512 if let Some(cycle_info) = extract_circular_dep_info(&error_str) {
513 println!(" {}", cycle_info);
514 }
515 println!();
516 println!(" This is common in complex databases with bidirectional foreign keys.");
517 println!(" To fix: move one foreign key to a separate file (e.g., constraints/)");
518 println!(" so the tables can be created before the constraint is added.");
519 return Ok(BaselineResult::NeedsAttention {
520 reason: "Circular dependency detected".to_string(),
521 });
522 }
523
524 println!("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
526 println!("ā ļø SCHEMA VALIDATION FAILED");
527 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
528 println!("{}\n", error_str);
529 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
530 println!("Next steps:");
531 println!(" 1. Fix dependencies in schema files (add '-- require:' statements)");
532 println!(" 2. Test with: pgmt apply --dry-run");
533 println!(" 3. Repeat until validation passes");
534 println!(" 4. Create baseline: pgmt migrate baseline\n");
535 return Ok(BaselineResult::Failed(e.to_string()));
536 }
537 }
538
539 let database_state = analyze_database_state(catalog);
541 let should_create_baseline = match &options.baseline_config.create_baseline {
542 Some(true) => true, Some(false) => false, None => {
545 prompt_baseline_creation(&database_state)?
547 }
548 };
549
550 if should_create_baseline {
552 let version = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
553 match create_baseline_with_migration_sync(catalog, options, version).await {
554 Ok((_baseline_path, _baseline_content)) => Ok(BaselineResult::Created),
555 Err(e) => {
556 handle_baseline_failure(&e);
557 Ok(BaselineResult::Failed(e.to_string()))
558 }
559 }
560 } else {
561 Ok(BaselineResult::NotRequested)
562 }
563}
564
565fn count_catalog_objects(catalog: &Catalog) -> usize {
567 catalog.tables.len()
568 + catalog.views.len()
569 + catalog.functions.len()
570 + catalog.types.len()
571 + catalog.sequences.len()
572 + catalog.indexes.len()
573 + catalog.constraints.len()
574 + catalog.triggers.len()
575 + catalog.extensions.len()
576 + catalog.grants.len()
577}
578
579#[derive(Debug)]
581pub enum DatabaseState {
582 Empty,
584 Existing { object_count: usize },
586}
587
588fn analyze_database_state(catalog: &Catalog) -> DatabaseState {
590 let total_objects = count_catalog_objects(catalog);
591
592 if total_objects <= 1 {
595 DatabaseState::Empty
596 } else {
597 DatabaseState::Existing {
598 object_count: total_objects,
599 }
600 }
601}
602
603fn show_catalog_preview(catalog: &Catalog) {
605 let total_objects = count_catalog_objects(catalog);
606
607 println!("\nš Schema Import Preview:");
608 println!(" š {} tables", catalog.tables.len());
609 println!(" š {} views", catalog.views.len());
610 println!(" āļø {} functions", catalog.functions.len());
611 println!(" š·ļø {} custom types", catalog.types.len());
612 println!(" š¢ {} sequences", catalog.sequences.len());
613 println!(" š {} indexes", catalog.indexes.len());
614 println!(" š {} constraints", catalog.constraints.len());
615 println!(" ā” {} triggers", catalog.triggers.len());
616 println!(" š§© {} extensions", catalog.extensions.len());
617 println!(" š {} grants", catalog.grants.len());
618 println!(" āāāāāāāāāāāāāāāāāāā");
619 println!(" š¦ {} total objects", total_objects);
620}
621
622fn extract_circular_dep_info(error_str: &str) -> Option<String> {
625 if let Some(start) = error_str.find("Circular dependency detected:") {
628 let after_prefix = &error_str[start + "Circular dependency detected:".len()..];
629 let cycle = after_prefix
631 .lines()
632 .next()
633 .map(|s| s.trim().to_string())
634 .filter(|s| !s.is_empty());
635 return cycle;
636 }
637 None
638}
639
640fn handle_baseline_failure(error: &anyhow::Error) {
642 println!("\nā Baseline creation failed: {}", error);
643
644 if error.to_string().contains("relation") && error.to_string().contains("does not exist") {
645 println!("\nš This error often indicates missing function dependencies.");
646 println!(" Some functions may reference tables that haven't been loaded yet.");
647 println!(" This is a known limitation - see README for details.");
648 println!("\nš” Common fixes:");
649 println!(" ⢠Add '-- require: tables/table_name.sql' to function files");
650 println!(" ⢠Check function bodies for table references");
651 println!(" ⢠Ensure proper loading order in your schema files");
652 } else {
653 println!("\nš Baseline creation encountered an error.");
654 println!(" This might be due to:");
655 println!(" ⢠Missing dependencies between schema objects");
656 println!(" ⢠Permission issues");
657 println!(" ⢠Database connection problems");
658 }
659
660 println!("\nā ļø Skipping baseline creation due to errors.");
661 println!("š” After fixing the dependency issues, run: pgmt migrate baseline");
662}
663
664async fn create_baseline_with_migration_sync(
666 catalog: &Catalog,
667 options: &InitOptions,
668 version: u64,
669) -> Result<(std::path::PathBuf, String)> {
670 println!("š¾ Creating baseline from current database state...");
671
672 let request = BaselineCreationRequest {
674 catalog: catalog.clone(),
675 version,
676 description: options
677 .baseline_config
678 .description
679 .clone()
680 .unwrap_or_else(|| "baseline".to_string()),
681 baselines_dir: options.project_dir.join(&options.baselines_dir),
682 verbose: false, };
684
685 let result = create_baseline(request).await?;
686
687 println!(
689 "ā
Created baseline: {}",
690 result.path.file_name().unwrap().to_str().unwrap()
691 );
692
693 display_baseline_summary(&result);
695
696 println!("š Marking baseline as applied in migration tracking...");
698
699 use sqlx::PgPool;
701 let dev_pool = PgPool::connect(&options.dev_database_url).await?;
702
703 let tracking_table = crate::config::types::TrackingTable {
705 schema: "public".to_string(),
706 name: "pgmt_migrations".to_string(),
707 };
708
709 let checksum = migration_tracking::calculate_checksum(&result.baseline_sql);
711
712 migration_tracking::record_baseline_as_applied(
714 &dev_pool,
715 &tracking_table,
716 version,
717 &options
718 .baseline_config
719 .description
720 .clone()
721 .unwrap_or_else(|| "baseline".to_string()),
722 &checksum,
723 )
724 .await?;
725
726 println!("ā
Baseline marked as applied in migration tracking");
727 println!("š” Future migrations will only contain NEW changes");
728
729 Ok((result.path, result.baseline_sql))
730}
731
732pub fn print_success_summary(options: &InitOptions, baseline_result: &BaselineResult) {
734 match baseline_result {
735 BaselineResult::Created => {
736 println!("\nš Project initialized successfully!");
737 println!("\nš Created:");
738 println!(" ā
pgmt.yaml (configuration)");
739 println!(
740 " ā
{} directory with modular files",
741 options.schema_dir.display()
742 );
743 println!(" ā
migrations/ directory");
744 println!(" ā
schema_baselines/ directory");
745 println!(" ā
Initial baseline from existing database");
746
747 println!("\nNext steps:");
748 println!(" š Run 'pgmt migrate new \"description\"' to create new migrations");
749 println!(" š” Future migrations will only contain NEW changes");
750 }
751 BaselineResult::NeedsAttention { reason } => {
752 println!("\nš Project initialized successfully!");
753 println!("\nš Created:");
754 println!(" ā
pgmt.yaml (configuration)");
755 println!(
756 " ā
{} directory with modular files",
757 options.schema_dir.display()
758 );
759 println!(" ā
migrations/ directory");
760 println!(" ā
schema_baselines/ directory");
761
762 println!("\nš {}", reason);
763 println!("\nNext steps:");
764 println!(
765 " 1. Move one foreign key from the cycle to a separate file (e.g., schema/constraints/)"
766 );
767 println!(" 2. Test with: pgmt apply --dry-run");
768 println!(" 3. Create baseline: pgmt migrate baseline");
769 println!(" š» Run 'pgmt apply' to sync your dev database");
770 println!(" š Run 'pgmt migrate new \"description\"' to create migrations");
771 }
772 BaselineResult::Failed(error) => {
773 if error.contains("relation") || error.contains("does not exist") {
775 println!("\nā ļø Project initialized - schema validation failed\n");
776 println!("š Created:");
777 println!(" ā
pgmt.yaml");
778 println!(
779 " ā
{} (needs dependency fixes)",
780 options.schema_dir.display()
781 );
782 println!(" ā
migrations/");
783 println!("\nš§ Next steps:");
784 println!(" 1. Fix schema dependencies (see error above)");
785 println!(" 2. Test with: pgmt apply --dry-run");
786 println!(" 3. Repeat until validation passes");
787 println!(" 4. Create baseline: pgmt migrate baseline");
788 } else {
789 let was_explicit_request =
791 matches!(options.baseline_config.create_baseline, Some(true));
792
793 if was_explicit_request {
794 println!("\nā ļø Project partially initialized - baseline creation failed!");
795 println!("\nš Created:");
796 println!(" ā
pgmt.yaml (configuration)");
797 println!(
798 " ā
{} directory with modular files",
799 options.schema_dir.display()
800 );
801 println!(" ā
migrations/ directory");
802 println!(" ā
schema_baselines/ directory");
803 println!(" ā Initial baseline creation failed: {}", error);
804
805 println!("\nNext steps:");
806 println!(" š§ Fix the baseline creation issue:");
807 println!(" ⢠Check database connectivity and permissions");
808 println!(" ⢠Review schema file dependencies");
809 println!(" ⢠Consider running 'pgmt migrate baseline' manually");
810 println!(" š» Run 'pgmt apply' to sync your dev database");
811 println!(" š Run 'pgmt migrate new \"description\"' to create migrations");
812 } else {
813 println!("\nš Project initialized successfully!");
815 println!("\nš Created:");
816 println!(" ā
pgmt.yaml (configuration)");
817 println!(
818 " ā
{} directory with modular files",
819 options.schema_dir.display()
820 );
821 println!(" ā
migrations/ directory");
822 println!(" ā
schema_baselines/ directory");
823 println!(" ā ļø Baseline creation failed (see error above)");
824
825 println!("\nNext steps:");
826 println!(" š” Fix the issue and create baseline: pgmt migrate baseline");
827 println!(" š» Run 'pgmt apply' to sync your dev database");
828 println!(" š Run 'pgmt migrate new \"description\"' to create migrations");
829 }
830 }
831 }
832 BaselineResult::NotRequested => {
833 println!("\nš Project initialized successfully!");
834 println!("\nš Created:");
835 println!(" ā
pgmt.yaml (configuration)");
836 println!(
837 " ā
{} directory with modular files",
838 options.schema_dir.display()
839 );
840 println!(" ā
migrations/ directory");
841 println!(" ā
schema_baselines/ directory");
842
843 println!("\nNext steps:");
844 println!(" š» Run 'pgmt apply' to sync your dev database");
845 println!(
846 " š Add schema files to {} and customize as needed",
847 options.schema_dir.display()
848 );
849 println!(" š Run 'pgmt migrate new \"description\"' to create migrations");
850 }
851 }
852
853 println!(" š Visit https://docs.pgmt.dev for more information");
854}
855
856async fn validate_schema_files(
858 schema_dir: &std::path::Path,
859 roles_file: Option<&std::path::Path>,
860 shadow_config: &ShadowDatabaseInput,
861 shadow_pg_version: Option<&String>,
862 detected_pg_version: Option<&String>,
863) -> Result<()> {
864 validate_schema_files_impl(
865 schema_dir,
866 roles_file,
867 shadow_config,
868 shadow_pg_version,
869 detected_pg_version,
870 )
871 .await
872}
873
874async fn validate_schema_files_impl(
876 schema_dir: &std::path::Path,
877 roles_file: Option<&std::path::Path>,
878 shadow_config: &ShadowDatabaseInput,
879 shadow_pg_version: Option<&String>,
880 detected_pg_version: Option<&String>,
881) -> Result<()> {
882 use crate::db::cleaner;
883 use crate::db::connection::connect_with_retry;
884 use crate::db::schema_processor::{SchemaProcessor, SchemaProcessorConfig};
885
886 let shadow_database =
888 resolve_shadow_database(shadow_config, shadow_pg_version, detected_pg_version);
889 let shadow_url = shadow_database.get_connection_string().await?;
890
891 let pool = connect_with_retry(&shadow_url).await?;
893
894 cleaner::clean_shadow_db(&pool, &crate::config::types::Objects::default()).await?;
896
897 if let Some(roles_path) = roles_file
899 && roles_path.exists()
900 {
901 crate::schema_ops::apply_roles_file(&pool, roles_path).await?;
902 }
903
904 let config = SchemaProcessorConfig {
907 verbose: false, clean_before_apply: false, ..Default::default()
910 };
911 let processor = SchemaProcessor::new(pool.clone(), config);
912 processor.process_schema_directory(schema_dir).await?;
913
914 pool.close().await;
915 Ok(())
916}
917
918async fn generate_schema_files(catalog: &Catalog, options: &InitOptions) -> Result<usize> {
920 use crate::schema_generator::{SchemaGenerator, SchemaGeneratorConfig};
921
922 let schema_path = options.project_dir.join(&options.schema_dir);
923 std::fs::create_dir_all(&schema_path)?;
924
925 let config = SchemaGeneratorConfig {
927 include_comments: options.object_config.comments,
928 include_grants: options.object_config.grants,
929 include_triggers: options.object_config.triggers,
930 include_extensions: options.object_config.extensions,
931 };
932
933 let generator = SchemaGenerator::new(catalog.clone(), schema_path.clone(), config);
935 generator.generate_files()?;
936
937 let file_count = count_generated_files(&schema_path)?;
939
940 Ok(file_count)
941}
942
943fn count_generated_files(schema_dir: &std::path::PathBuf) -> Result<usize> {
945 let mut count = 0;
946
947 if schema_dir.exists() {
948 for entry in std::fs::read_dir(schema_dir)? {
949 let entry = entry?;
950 let path = entry.path();
951
952 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("sql") {
953 count += 1;
954 } else if path.is_dir() {
955 count += count_generated_files(&path)?;
957 }
958 }
959 }
960
961 Ok(count)
962}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967
968 #[test]
969 fn test_object_management_config_default() {
970 let config = ObjectManagementConfig::default();
971 assert!(config.comments);
972 assert!(config.grants);
973 assert!(config.triggers);
974 assert!(config.extensions);
975 }
976
977 #[test]
978 fn test_count_catalog_objects() {
979 let catalog = Catalog::empty();
980 assert_eq!(count_catalog_objects(&catalog), 0);
981 }
982
983 #[test]
984 fn test_count_generated_files() {
985 use std::env;
986
987 let temp_dir = env::temp_dir().join("pgmt_test_count_files");
988 let _ = std::fs::remove_dir_all(&temp_dir);
989 std::fs::create_dir_all(&temp_dir).unwrap();
990
991 std::fs::write(temp_dir.join("test1.sql"), "SELECT 1;").unwrap();
993 std::fs::write(temp_dir.join("test2.sql"), "SELECT 2;").unwrap();
994 std::fs::write(temp_dir.join("readme.txt"), "Not SQL").unwrap();
995
996 let count = count_generated_files(&temp_dir).unwrap();
997 assert_eq!(count, 2); let _ = std::fs::remove_dir_all(&temp_dir);
1001 }
1002
1003 #[test]
1004 fn test_check_existing_config_not_found() {
1005 use std::env;
1006
1007 let temp_dir = env::temp_dir().join("pgmt_test_no_config");
1008 let _ = std::fs::remove_dir_all(&temp_dir);
1009 std::fs::create_dir_all(&temp_dir).unwrap();
1010
1011 let result = check_existing_config(&temp_dir, false).unwrap();
1013 assert!(matches!(result, ExistingConfigResult::NotFound));
1014
1015 let _ = std::fs::remove_dir_all(&temp_dir);
1017 }
1018
1019 #[test]
1020 fn test_check_existing_config_fresh_flag() {
1021 use std::env;
1022
1023 let temp_dir = env::temp_dir().join("pgmt_test_fresh_flag");
1024 let _ = std::fs::remove_dir_all(&temp_dir);
1025 std::fs::create_dir_all(&temp_dir).unwrap();
1026
1027 let config_content = r#"
1029databases:
1030 dev_url: postgres://localhost/test
1031"#;
1032 std::fs::write(temp_dir.join("pgmt.yaml"), config_content).unwrap();
1033
1034 let result = check_existing_config(&temp_dir, true).unwrap();
1036 assert!(matches!(result, ExistingConfigResult::Fresh));
1037
1038 let _ = std::fs::remove_dir_all(&temp_dir);
1040 }
1041}