use crate::config::Config;
use crate::migrate::{MigrationGenerationInput, generate_migration};
use crate::migration::{
BaselineConfig, ensure_baseline_for_migration, find_latest_migration,
generate_baseline_filename, get_migration_update_starting_state,
should_manage_baseline_for_migration, validate_baseline_against_catalog,
};
use anyhow::{Result, anyhow};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::debug;
pub async fn cmd_migrate_update_with_options(
config: &Config,
root_dir: &Path,
dry_run: bool,
shadow: &crate::config::ShadowDatabase,
) -> Result<()> {
if dry_run {
println!("🔍 Dry-run mode: previewing changes without applying them");
}
println!("Updating latest migration with current changes");
let migrations_dir = root_dir.join(&config.directories.migrations);
let baselines_dir = root_dir.join(&config.directories.baselines);
std::fs::create_dir_all(&migrations_dir)?;
std::fs::create_dir_all(&baselines_dir)?;
let latest_migration = find_latest_migration(&migrations_dir)?;
if latest_migration.is_none() {
return Err(anyhow::anyhow!(
"No migrations found. Use 'pgmt migrate new <description>' to create the first migration."
));
}
let latest_migration = latest_migration.unwrap();
println!("Updating migration: {}", latest_migration.path.display());
let baseline_config = BaselineConfig {
validate_consistency: config.migration.validate_baseline_consistency,
verbose: true,
};
let roles_file = root_dir.join(&config.directories.roles);
let starting_pool = shadow.connect_fresh().await?;
let old_catalog = get_migration_update_starting_state(
&starting_pool,
&baselines_dir,
&migrations_dir,
latest_migration.version,
&roles_file,
&baseline_config,
config,
)
.await?;
crate::db::branch::drop_branch(starting_pool).await?;
debug!("Applying current schema to shadow database");
let new_catalog =
crate::schema_ops::apply_current_schema_to_shadow(config, root_dir, shadow).await?;
crate::validation::apply_column_order_validation(
&old_catalog,
&new_catalog,
config.migration.column_order,
)?;
debug!("Generating updated migration steps");
let migration_result = generate_migration(MigrationGenerationInput {
old_catalog,
new_catalog: new_catalog.clone(),
description: latest_migration.description.clone(), version: latest_migration.version,
filename_prefix: config.migration.filename_prefix.clone(),
})?;
if !migration_result.has_changes {
println!("No changes detected - updating migration to be empty");
let empty_migration_sql = "-- No changes detected\n";
std::fs::write(&latest_migration.path, empty_migration_sql)?;
println!(
"Updated migration: {} (now empty)",
latest_migration.path.display()
);
return Ok(());
}
std::fs::write(&latest_migration.path, &migration_result.migration_sql)?;
println!("Updated migration: {}", latest_migration.path.display());
let baseline_filename = generate_baseline_filename(latest_migration.version);
let baseline_path = baselines_dir.join(&baseline_filename);
let should_update_baseline = should_manage_baseline_for_migration(
config,
&baseline_path,
config.migration.create_baselines_by_default,
);
if should_update_baseline {
let result = ensure_baseline_for_migration(
&baselines_dir,
latest_migration.version,
&migration_result.migration_sql,
&baseline_config,
)
.await?;
println!("Updated baseline: {}", result.path.display());
if baseline_config.validate_consistency {
let validate_pool = shadow.connect_fresh().await?;
validate_baseline_against_catalog(
&validate_pool,
&result.path,
&new_catalog,
&baseline_config,
&roles_file,
config,
)
.await?;
crate::db::branch::drop_branch(validate_pool).await?;
}
} else {
println!(
"Skipping baseline update (baseline does not exist and create_baselines_by_default is false)"
);
}
println!("Migration update complete!");
Ok(())
}
pub async fn cmd_migrate_update_specific(
config: &Config,
root_dir: &Path,
version_str: &str,
backup: bool,
dry_run: bool,
shadow: &crate::config::ShadowDatabase,
) -> Result<()> {
use crate::migration::parsing::find_migration_by_version;
if dry_run {
println!(
"🔍 Dry-run mode: previewing migration update for: {}",
version_str
);
} else {
println!("Updating migration: {}", version_str);
}
let migrations_dir = root_dir.join(&config.directories.migrations);
let baselines_dir = root_dir.join(&config.directories.baselines);
std::fs::create_dir_all(&migrations_dir)?;
std::fs::create_dir_all(&baselines_dir)?;
let target_migration = find_migration_by_version(&migrations_dir, version_str)?;
let target_migration = match target_migration {
Some(migration) => migration,
None => {
return Err(anyhow!(
"Migration '{}' not found. Use 'pgmt migrate status' to see available migrations.",
version_str
));
}
};
println!(
"Found migration: {} ({})",
target_migration.path.display(),
target_migration.description
);
if backup && !dry_run {
let backup_path = target_migration.path.with_extension("sql.bak");
std::fs::copy(&target_migration.path, &backup_path)?;
println!("💾 Backup created: {}", backup_path.display());
} else if backup && dry_run {
let backup_path = target_migration.path.with_extension("sql.bak");
println!("💾 Would create backup: {}", backup_path.display());
}
let latest_migration = find_latest_migration(&migrations_dir)?;
let is_latest = latest_migration
.map(|latest| latest.version == target_migration.version)
.unwrap_or(false);
let baseline_config = BaselineConfig {
validate_consistency: config.migration.validate_baseline_consistency,
verbose: true,
};
let roles_file = root_dir.join(&config.directories.roles);
let starting_pool = shadow.connect_fresh().await?;
let old_catalog = get_migration_update_starting_state(
&starting_pool,
&baselines_dir,
&migrations_dir,
target_migration.version,
&roles_file,
&baseline_config,
config,
)
.await?;
crate::db::branch::drop_branch(starting_pool).await?;
debug!("Applying current schema to shadow database");
let new_catalog =
crate::schema_ops::apply_current_schema_to_shadow(config, root_dir, shadow).await?;
crate::validation::apply_column_order_validation(
&old_catalog,
&new_catalog,
config.migration.column_order,
)?;
let (new_version, new_description) = if is_latest {
(
target_migration.version,
target_migration.description.clone(),
)
} else {
let new_version = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| anyhow!("System time is before Unix epoch: {}", e))?
.as_secs();
(new_version, target_migration.description.clone())
};
debug!("Generating updated migration steps");
let migration_result = generate_migration(MigrationGenerationInput {
old_catalog,
new_catalog: new_catalog.clone(),
description: new_description.clone(),
version: new_version,
filename_prefix: config.migration.filename_prefix.clone(),
})?;
if !migration_result.has_changes {
if is_latest {
println!("No changes detected - updating migration to be empty");
let empty_migration_sql = "-- No changes detected\n";
std::fs::write(&target_migration.path, empty_migration_sql)?;
println!(
"Updated migration: {} (now empty)",
target_migration.path.display()
);
} else {
println!("No changes detected - conflicts resolved by other migrations");
let empty_migration_content = format!(
"-- Migration: {}\n-- Version: {}{}\n-- Generated by pgmt migrate update (renumbered from {}{})\n-- No changes needed - conflicts resolved by intervening migrations\n",
new_description,
config.migration.filename_prefix,
new_version,
config.migration.filename_prefix,
target_migration.version
);
if is_latest {
std::fs::write(&target_migration.path, &empty_migration_content)?;
println!(
"Updated migration: {} (no changes needed)",
target_migration.path.display()
);
} else {
std::fs::remove_file(&target_migration.path)?;
let new_filename = format!(
"{}{}_{}.sql",
config.migration.filename_prefix,
new_version,
new_description.replace(' ', "_")
);
let new_path = migrations_dir.join(&new_filename);
std::fs::write(&new_path, &empty_migration_content)?;
println!(
"Migration {} updated to {} (no changes needed)",
target_migration.version, new_version
);
println!("Created: {}", new_path.display());
}
}
return Ok(());
}
if dry_run {
println!(
"📝 Preview: Generated migration content ({} chars)",
migration_result.migration_sql.len()
);
if is_latest {
println!("🔄 Would update: {}", target_migration.path.display());
} else {
let new_filename = format!(
"{}{}_{}.sql",
config.migration.filename_prefix,
new_version,
new_description.replace(' ', "_")
);
let new_path = migrations_dir.join(&new_filename);
println!(
"🔄 Would rename {} → {}",
target_migration.version, new_version
);
println!(" Delete: {}", target_migration.path.display());
println!(" Create: {}", new_path.display());
}
println!(
"\n📋 Migration preview:\n{}",
migration_result.migration_sql
);
} else if is_latest {
std::fs::write(&target_migration.path, &migration_result.migration_sql)?;
println!("Updated migration: {}", target_migration.path.display());
} else {
std::fs::remove_file(&target_migration.path)?;
let new_filename = format!(
"{}{}_{}.sql",
config.migration.filename_prefix,
new_version,
new_description.replace(' ', "_")
);
let new_path = migrations_dir.join(&new_filename);
std::fs::write(&new_path, &migration_result.migration_sql)?;
println!(
"Migration {} updated to {} (renumbered)",
target_migration.version, new_version
);
println!("Deleted: {}", target_migration.path.display());
println!("Created: {}", new_path.display());
}
let baseline_filename = generate_baseline_filename(new_version);
let baseline_path = baselines_dir.join(&baseline_filename);
let should_update_baseline = should_manage_baseline_for_migration(
config,
&baseline_path,
config.migration.create_baselines_by_default,
);
if should_update_baseline {
let result = ensure_baseline_for_migration(
&baselines_dir,
new_version,
&migration_result.migration_sql,
&baseline_config,
)
.await?;
if is_latest {
println!("Updated baseline: {}", result.path.display());
} else {
println!("Created baseline: {}", result.path.display());
}
if baseline_config.validate_consistency {
let validate_pool = shadow.connect_fresh().await?;
validate_baseline_against_catalog(
&validate_pool,
&result.path,
&new_catalog,
&baseline_config,
&roles_file,
config,
)
.await?;
crate::db::branch::drop_branch(validate_pool).await?;
}
} else if is_latest {
println!(
"Skipping baseline update (baseline does not exist and create_baselines_by_default is false)"
);
} else {
println!("Skipping baseline creation (create_baselines_by_default is false)");
}
if dry_run {
println!("🔍 Dry-run complete! No changes were made.");
} else {
println!("Migration update complete!");
}
Ok(())
}