use anyhow::Result;
use std::fs;
use std::path::Path;
use crate::constants::{
CONFIG_FILENAME, FUNCTIONS_SUBDIR, SCHEMAS_SUBDIR, TABLES_SUBDIR, TYPES_SUBDIR, VIEWS_SUBDIR,
};
pub fn create_project_structure(options: &super::InitOptions) -> Result<()> {
fs::create_dir_all(&options.project_dir)?;
let migrations_dir = options.project_dir.join(&options.migrations_dir);
let baselines_dir = options.project_dir.join(&options.baselines_dir);
let full_schema_dir = options.project_dir.join(&options.schema_dir);
fs::create_dir_all(&migrations_dir)?;
fs::create_dir_all(&baselines_dir)?;
fs::create_dir_all(&full_schema_dir)?;
fs::create_dir_all(full_schema_dir.join(SCHEMAS_SUBDIR))?;
fs::create_dir_all(full_schema_dir.join(TYPES_SUBDIR))?;
fs::create_dir_all(full_schema_dir.join(TABLES_SUBDIR))?;
fs::create_dir_all(full_schema_dir.join(VIEWS_SUBDIR))?;
fs::create_dir_all(full_schema_dir.join(FUNCTIONS_SUBDIR))?;
Ok(())
}
pub fn generate_config_file(options: &super::InitOptions, project_dir: &Path) -> Result<()> {
let effective_version = options
.shadow_pg_version
.as_ref()
.or(options.detected_pg_version.as_ref())
.map(|v| crate::prompts::extract_major_version(v));
let shadow_config = match (&options.shadow_config, effective_version) {
(crate::prompts::ShadowDatabaseInput::Auto, Some(version)) => {
format!(" shadow:\n docker:\n version: \"{}\"", version)
}
(crate::prompts::ShadowDatabaseInput::Auto, None) => {
" shadow:\n auto: true".to_string()
}
(crate::prompts::ShadowDatabaseInput::Manual(url), _) => {
format!(" shadow:\n auto: false\n url: {}", url)
}
};
let roles_file_config = options
.roles_file
.as_ref()
.map(|f| format!("\n roles_file: {}", f))
.unwrap_or_default();
let config_content = format!(
r#"# pgmt Configuration File
# Generated by pgmt init
# Database Configuration
databases:
dev_url: {}
{}
# Directory Structure
directories:
schema_dir: {}
migrations_dir: {}
baselines_dir: {}{}
# Object Filtering (optional)
# Use include/exclude patterns to control which schemas and tables are managed.
# By default, pg_* and information_schema are excluded.
# objects:
# exclude:
# exclude_schemas: ["pg_*", "information_schema", "temp_*"]
# exclude_tables: ["*_backup"]
"#,
options.dev_database_url,
shadow_config,
options.schema_dir.display(),
options.migrations_dir,
options.baselines_dir,
roles_file_config,
);
let config_path = project_dir.join(CONFIG_FILENAME);
std::fs::write(config_path, config_content)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::init::{BaselineCreationConfig, InitOptions, ObjectManagementConfig};
use crate::config::types::Directories;
use std::env;
use std::path::{Path, PathBuf};
fn test_options(temp_dir: &Path) -> InitOptions {
let dir_defaults = Directories::default();
InitOptions {
project_dir: temp_dir.to_path_buf(),
dev_database_url: "postgres://localhost/test_db".to_string(),
shadow_config: crate::prompts::ShadowDatabaseInput::Auto,
shadow_pg_version: None,
detected_pg_version: None,
schema_dir: PathBuf::from("schema"),
migrations_dir: dir_defaults.migrations,
baselines_dir: dir_defaults.baselines,
import_source: None,
object_config: ObjectManagementConfig::default(),
baseline_config: BaselineCreationConfig::default(),
tracking_table: crate::config::types::TrackingTable::default(),
roles_file: None,
}
}
#[test]
fn test_create_project_structure() {
let temp_dir = env::temp_dir().join("pgmt_test_project_structure");
let _ = std::fs::remove_dir_all(&temp_dir);
let options = test_options(&temp_dir);
create_project_structure(&options).unwrap();
let dir_defaults = Directories::default();
assert!(temp_dir.join(&dir_defaults.migrations).exists());
assert!(temp_dir.join(&dir_defaults.baselines).exists());
assert!(temp_dir.join("schema").exists());
assert!(temp_dir.join("schema/tables").exists());
assert!(temp_dir.join("schema/views").exists());
assert!(temp_dir.join("schema/functions").exists());
assert!(temp_dir.join("schema/types").exists());
assert!(temp_dir.join("schema/schemas").exists());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_create_project_structure_custom_directories() {
let temp_dir = env::temp_dir().join("pgmt_test_project_structure_custom");
let _ = std::fs::remove_dir_all(&temp_dir);
let mut options = test_options(&temp_dir);
options.migrations_dir = "db/migrations".to_string();
options.baselines_dir = "db/baselines".to_string();
create_project_structure(&options).unwrap();
assert!(temp_dir.join("db/migrations").exists());
assert!(temp_dir.join("db/baselines").exists());
assert!(temp_dir.join("schema").exists());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_generate_config_file() {
let temp_dir = env::temp_dir().join("pgmt_test_config");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let mut options = test_options(&temp_dir);
options.schema_dir = PathBuf::from("custom_schema");
generate_config_file(&options, &temp_dir).unwrap();
let config_path = temp_dir.join("pgmt.yaml");
assert!(config_path.exists());
let content = std::fs::read_to_string(&config_path).unwrap();
let dir_defaults = Directories::default();
assert!(content.contains("postgres://localhost/test_db"));
assert!(content.contains("custom_schema"));
assert!(content.contains("auto: true"));
assert!(content.contains(&format!("migrations_dir: {}", dir_defaults.migrations)));
assert!(content.contains(&format!("baselines_dir: {}", dir_defaults.baselines)));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_generate_config_file_custom_directories() {
let temp_dir = env::temp_dir().join("pgmt_test_config_custom_dirs");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let mut options = test_options(&temp_dir);
options.migrations_dir = "db/migrations".to_string();
options.baselines_dir = "db/baselines".to_string();
generate_config_file(&options, &temp_dir).unwrap();
let config_path = temp_dir.join("pgmt.yaml");
let content = std::fs::read_to_string(&config_path).unwrap();
assert!(
content.contains("migrations_dir: db/migrations"),
"Expected custom migrations_dir, got:\n{}",
content
);
assert!(
content.contains("baselines_dir: db/baselines"),
"Expected custom baselines_dir, got:\n{}",
content
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_generate_config_file_with_detected_version() {
let temp_dir = env::temp_dir().join("pgmt_test_config_detected_version");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let mut options = test_options(&temp_dir);
options.detected_pg_version = Some("15.4".to_string());
generate_config_file(&options, &temp_dir).unwrap();
let config_path = temp_dir.join("pgmt.yaml");
let content = std::fs::read_to_string(&config_path).unwrap();
assert!(
content.contains("version: \"15\""),
"Expected detected version to be persisted, got:\n{}",
content
);
assert!(
!content.contains("auto: true"),
"Should not have auto: true when version is detected"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_generate_config_file_explicit_version_takes_precedence() {
let temp_dir = env::temp_dir().join("pgmt_test_config_explicit_version");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let mut options = test_options(&temp_dir);
options.shadow_pg_version = Some("16".to_string()); options.detected_pg_version = Some("15.4".to_string());
generate_config_file(&options, &temp_dir).unwrap();
let config_path = temp_dir.join("pgmt.yaml");
let content = std::fs::read_to_string(&config_path).unwrap();
assert!(
content.contains("version: \"16\""),
"Expected explicit version to take precedence, got:\n{}",
content
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
}