use crate::baseline::operations::{BaselineCreationRequest, create_baseline};
use crate::config::{Config, ShadowDatabase};
use crate::migrate::{MigrationGenerationInput, generate_migration};
use crate::migration::{
BaselineConfig, generate_baseline_filename, get_migration_starting_state,
get_migration_update_starting_state, should_manage_baseline_for_migration,
validate_baseline_against_catalog,
};
use crate::modules::{
HistoricalAttribution, evaluate_module_generation, render_generated_migration,
section_baseline_if_moduled,
};
use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::debug;
#[derive(Clone, Copy)]
pub enum StartingState {
FullHistory,
Before(u64),
}
#[derive(Clone, Copy)]
pub enum BaselinePolicy {
Requested,
IfManaged,
Never,
}
pub struct GenerationRequest<'a> {
pub version: u64,
pub description: String,
pub starting_state: StartingState,
pub baseline: BaselinePolicy,
pub reanchor_hint: &'a str,
pub dry_run: bool,
pub stub_migration: bool,
}
pub struct BaselineOutcome {
pub path: PathBuf,
}
pub struct Generated {
pub migration_sql: Option<String>,
pub filename: String,
pub has_changes: bool,
pub partition_diverged: bool,
pub baseline: Option<BaselineOutcome>,
}
impl Generated {
pub fn is_empty(&self) -> bool {
!self.has_changes && !self.partition_diverged
}
}
pub async fn generate(
config: &Config,
root_dir: &Path,
shadow: &ShadowDatabase,
request: GenerationRequest<'_>,
) -> Result<Generated> {
let migrations_dir = root_dir.join(&config.directories.migrations);
let baselines_dir = root_dir.join(&config.directories.baselines);
let roles_file = root_dir.join(&config.directories.roles);
let baseline_config = BaselineConfig {
validate_consistency: config.migration.validate_baseline_consistency,
verbose: true,
};
let mut historical = HistoricalAttribution::default();
let attribution = config.modules.is_enabled().then_some(&mut historical);
let (baselines, migrations, roles, cfg) = (
&baselines_dir,
&migrations_dir,
&roles_file,
&baseline_config,
);
let starting_state = request.starting_state;
let old_catalog = shadow
.with_fresh(|pool| async move {
match starting_state {
StartingState::FullHistory => {
get_migration_starting_state(
&pool,
baselines,
migrations,
roles,
cfg,
config,
attribution,
)
.await
}
StartingState::Before(version) => {
get_migration_update_starting_state(
&pool,
baselines,
migrations,
version,
roles,
cfg,
config,
attribution,
)
.await
}
}
})
.await?;
debug!("Applying current schema to shadow database");
let crate::schema_ops::DesiredState {
base: shadow_base,
catalog: new_catalog,
mapping: file_mapping,
} = crate::schema_ops::apply_current_schema_to_shadow_with_mapping(config, root_dir, shadow)
.await?;
crate::validation::apply_column_order_validation(
&old_catalog,
&new_catalog,
config.migration.column_order,
)?;
debug!("Generating migration steps");
let migration_result = generate_migration(MigrationGenerationInput {
old_catalog: old_catalog.clone(),
new_catalog: new_catalog.clone(),
description: request.description.clone(),
version: request.version,
filename_prefix: config.migration.filename_prefix.clone(),
})?;
let baseline_path = baselines_dir.join(generate_baseline_filename(request.version));
let wants_baseline = match request.baseline {
BaselinePolicy::Requested => true,
BaselinePolicy::IfManaged => should_manage_baseline_for_migration(
config,
&baseline_path,
config.migration.create_baselines_by_default,
),
BaselinePolicy::Never => false,
};
let module_gen = evaluate_module_generation(
config,
&old_catalog,
&new_catalog,
&file_mapping,
&historical,
wants_baseline,
request.reanchor_hint,
)?;
let partition_diverged = module_gen.as_ref().is_some_and(|m| m.diverged);
let nothing_to_record =
!migration_result.has_changes && !partition_diverged && !request.stub_migration;
let baseline = if wants_baseline && !nothing_to_record {
let (write_dir, _scratch) = baseline_write_dir(&baselines_dir, request.dry_run)?;
let result = create_baseline(BaselineCreationRequest {
catalog: new_catalog.clone(),
base_catalog: shadow_base,
version: request.version,
description: "baseline".to_string(),
baselines_dir: write_dir,
verbose: baseline_config.verbose && !request.dry_run,
})
.await?;
section_baseline_if_moduled(
module_gen.as_ref(),
&result.path,
&new_catalog,
&file_mapping,
&historical,
)?;
if baseline_config.validate_consistency {
let (written, catalog, roles) = (&result.path, &new_catalog, &roles_file);
shadow
.with_fresh(|pool| async move {
validate_baseline_against_catalog(&pool, written, catalog, cfg, roles, config)
.await
})
.await?;
}
Some(BaselineOutcome {
path: baseline_path,
})
} else {
None
};
let migration_sql = render_generated_migration(
module_gen.as_ref(),
migration_result.has_changes,
&migration_result.migration_sql,
&old_catalog,
&new_catalog,
&file_mapping,
&historical,
)?;
Ok(Generated {
migration_sql,
filename: migration_result.migration_filename,
has_changes: migration_result.has_changes,
partition_diverged,
baseline,
})
}
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new() -> Result<Self> {
let path =
std::env::temp_dir().join(format!("pgmt-dry-run-{}", uuid::Uuid::new_v4().simple()));
std::fs::create_dir_all(&path)?;
Ok(Self(path))
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn baseline_write_dir(
baselines_dir: &Path,
dry_run: bool,
) -> Result<(PathBuf, Option<ScratchDir>)> {
if dry_run {
let scratch = ScratchDir::new()?;
let dir = scratch.0.clone();
Ok((dir, Some(scratch)))
} else {
Ok((baselines_dir.to_path_buf(), None))
}
}