pub mod database;
pub mod directory;
pub mod sql_file;
use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::catalog::Catalog;
pub use database::import_from_database;
pub use directory::import_from_directory;
pub use sql_file::{import_from_sql_file, validate_sql_file};
#[derive(Debug, Clone)]
pub enum ImportSource {
Directory(PathBuf), SqlFile(PathBuf), Database(String), }
impl ImportSource {}
pub async fn import_schema(
source: ImportSource,
shadow_url: &str,
roles_file: Option<&Path>,
objects: &crate::config::types::Objects,
) -> Result<Catalog> {
match source {
ImportSource::Directory(dir) => {
validate_directory_source(&dir)?;
import_from_directory(dir, shadow_url, roles_file, objects).await
}
ImportSource::SqlFile(file) => {
validate_sql_file(&file)?;
import_from_sql_file(file, shadow_url, roles_file, objects).await
}
ImportSource::Database(url) => {
validate_database_url(&url)?;
import_from_database(url).await
}
}
}
fn validate_directory_source(dir: &Path) -> Result<()> {
if !dir.exists() {
return Err(anyhow::anyhow!(
"Directory does not exist: {}",
dir.display()
));
}
if !dir.is_dir() {
return Err(anyhow::anyhow!(
"Path is not a directory: {}",
dir.display()
));
}
let sql_files = crate::db::sql_executor::discover_sql_files_ordered(dir)?;
if sql_files.is_empty() {
return Err(anyhow::anyhow!(
"Directory '{}' does not contain any SQL files",
dir.display()
));
}
Ok(())
}
fn validate_database_url(url: &str) -> Result<()> {
if url.trim().is_empty() {
return Err(anyhow::anyhow!("Database URL cannot be empty"));
}
if !url.starts_with("postgres://") && !url.starts_with("postgresql://") {
return Err(anyhow::anyhow!(
"Invalid database URL format. Expected postgres:// or postgresql:// scheme"
));
}
if !url.contains("://") {
return Err(anyhow::anyhow!(
"Invalid database URL format: missing protocol"
));
}
println!("✅ Database URL validation passed");
Ok(())
}
impl ImportSource {
pub fn description(&self) -> String {
match self {
ImportSource::Directory(dir) => format!("Directory: {}", dir.display()),
ImportSource::SqlFile(file) => format!("SQL file: {}", file.display()),
ImportSource::Database(url) => {
if let Some(at_pos) = url.find('@') {
if let Some(colon_pos) = url[..at_pos].rfind(':') {
let prefix = &url[..colon_pos];
let suffix = &url[at_pos..];
format!("Database: {}:***{}", prefix, suffix)
} else {
format!("Database: {}", url)
}
} else {
format!("Database: {}", url)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_import_source_description() {
let dir_source = ImportSource::Directory(PathBuf::from("/tmp/sql"));
assert!(dir_source.description().contains("Directory"));
let file_source = ImportSource::SqlFile(PathBuf::from("/tmp/dump.sql"));
assert!(file_source.description().contains("SQL file"));
let db_source = ImportSource::Database("postgres://localhost/test".to_string());
assert!(db_source.description().contains("Database"));
}
#[test]
fn test_validate_database_url() {
assert!(validate_database_url("postgres://localhost/test").is_ok());
assert!(validate_database_url("postgresql://user:pass@host:5432/db").is_ok());
assert!(validate_database_url("").is_err());
assert!(validate_database_url("mysql://localhost/test").is_err());
assert!(validate_database_url("not a url").is_err());
}
#[test]
fn test_validate_directory_source() {
let temp_dir = env::temp_dir().join(format!(
"pgmt_test_validate_directory_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&temp_dir);
assert!(validate_directory_source(&temp_dir).is_err());
std::fs::create_dir_all(&temp_dir).unwrap();
assert!(validate_directory_source(&temp_dir).is_err());
std::fs::write(temp_dir.join("test.sql"), "CREATE TABLE test (id INT);").unwrap();
assert!(validate_directory_source(&temp_dir).is_ok());
let _ = std::fs::remove_dir_all(&temp_dir);
}
}