use crate::commands::migrate::apply::apply_pending_migrations;
use crate::config::Config;
use crate::migration::baseline::apply_baseline_to_target;
use crate::migration::{discover_migrations, find_latest_baseline};
use crate::migration_tracking::{
MigrationLock, calculate_checksum, ensure_section_tracking_table, ensure_tracking_table_exists,
register_baseline_start,
};
use crate::modules::{ModuleRuntime, ModuleSelection, parse_section_files};
use anyhow::{Context, Result};
use sqlx::PgPool;
use std::collections::BTreeSet;
use std::path::Path;
pub async fn cmd_migrate_provision(
config: &Config,
root_dir: &Path,
target: &crate::config::TargetUrl,
dry_run: bool,
selection: ModuleSelection,
) -> Result<()> {
let pool =
crate::db::connection::connect_to_database(target.as_str(), "target database").await?;
let lock = MigrationLock::acquire(target.as_str(), &config.migration.tracking_table).await?;
let result = provision_inner(config, root_dir, &pool, dry_run, selection).await;
lock.release().await?;
result
}
async fn provision_inner(
config: &Config,
root_dir: &Path,
pool: &PgPool,
dry_run: bool,
selection: ModuleSelection,
) -> Result<()> {
let migrations_dir = root_dir.join(&config.directories.migrations);
let baselines_dir = root_dir.join(&config.directories.baselines);
ensure_tracking_table_exists(pool, &config.migration.tracking_table).await?;
ensure_section_tracking_table(pool, &config.migration.tracking_table).await?;
let store =
crate::migration_tracking::TrackingStore::new(pool, &config.migration.tracking_table)?;
let established = store.target_is_established().await?;
let migrations = discover_migrations(&migrations_dir)?;
let latest_baseline = find_latest_baseline(&baselines_dir)?;
if let Some(named) = selection.named()
&& named.is_empty()
{
println!(
"No --modules given: provisioning the unmoduled base only ({} declared module(s) \
not included; use --modules all for everything).",
config.modules.modules.len()
);
}
let mut runtime =
ModuleRuntime::load(pool, &config.migration.tracking_table, &baselines_dir).await?;
let files = if selection.named().is_some() {
parse_section_files(&migrations, &baselines_dir)?
} else {
Default::default()
};
if established {
let needs_baseline = match selection.named() {
Some(named) => runtime.needing_baseline_content(named.iter()),
None => Vec::new(),
};
if !needs_baseline.is_empty() {
let adoption_version = runtime
.adoption_baseline()
.map(|(version, _)| version)
.expect("modules need baseline content, so an adoption baseline exists");
let baseline = crate::migration::discover_baselines(&baselines_dir)?
.into_iter()
.find(|b| b.version == adoption_version)
.expect("the adoption baseline was discovered from this directory");
for module in &needs_baseline {
for dep in &config.modules.modules[module].depends_on {
if !runtime.established().contains(dep) && !needs_baseline.contains(dep) {
anyhow::bail!(
"adopting module '{}' requires its dependency '{}' on this target; \
adopt them together: pgmt migrate provision --modules {},{}",
module,
dep,
module,
dep
);
}
}
}
let pending = crate::modules::established_pending_through(
pool,
&config.migration.tracking_table,
&files,
runtime.established(),
baseline.version,
)
.await?;
if !pending.is_empty() {
let catch_up = if runtime.established().is_empty() {
"pgmt migrate apply".to_string()
} else {
format!(
"pgmt migrate apply --modules {}",
runtime
.established()
.iter()
.cloned()
.collect::<Vec<_>>()
.join(",")
)
};
anyhow::bail!(
"cannot adopt module(s) {} from baseline {}: this target's established \
modules are not caught up to that version (migration(s) {} still pending). \
Roll them forward first with `{}`, then re-run provision.",
needs_baseline.join(", "),
baseline.version,
pending
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(", "),
catch_up,
);
}
if dry_run {
println!(
"Would adopt module(s) {} from baseline {} (then apply pending sections).",
needs_baseline.join(", "),
baseline.version
);
return Ok(());
}
println!(
"Adopting module(s) {} from baseline {}...",
needs_baseline.join(", "),
baseline.version
);
let baseline_sql = std::fs::read_to_string(&baseline.path).with_context(|| {
format!("Failed to read baseline file: {}", baseline.path.display())
})?;
let source = baseline
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("baseline.sql");
let adopted: BTreeSet<&str> = needs_baseline.iter().map(String::as_str).collect();
let is_adopted = |section: &crate::migration::section_parser::MigrationSection| {
section
.module
.as_deref()
.is_some_and(|m| adopted.contains(m))
};
register_and_apply_baseline(
pool,
config,
baseline.version,
&baseline_sql,
source,
|section| {
is_adopted(section)
&& !crate::modules::remap_source_held(section, runtime.established())
},
)
.await?;
let satisfied: Vec<(i32, crate::migration::section_parser::MigrationSection)> =
crate::migration::parse_migration_sections(
std::path::Path::new(source),
&baseline_sql,
)?
.into_iter()
.enumerate()
.map(|(i, s)| (i as i32, s))
.filter(|(_, s)| {
is_adopted(s) && crate::modules::remap_source_held(s, runtime.established())
})
.collect();
crate::migration_tracking::section_tracking::record_sections_satisfied(
pool,
&config.migration.tracking_table,
baseline.version,
true,
&satisfied,
)
.await?;
println!("✅ Adopted module(s) {}.", needs_baseline.join(", "));
} else if dry_run {
println!(
"Database is already provisioned. Pending migrations would be applied (use `migrate apply`)."
);
return Ok(());
} else {
println!("Database is already provisioned; applying any pending migrations.");
}
if let Some(named) = selection.named() {
runtime.record_adopted(named).await?;
}
let applied_any =
apply_pending_migrations(pool, config, &migrations, &selection, &mut runtime).await?;
if !applied_any {
println!("Nothing to apply — up to date.");
}
return Ok(());
}
match latest_baseline {
Some(baseline) => {
let baseline_sql = std::fs::read_to_string(&baseline.path).with_context(|| {
format!("Failed to read baseline file: {}", baseline.path.display())
})?;
let source = baseline
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("baseline.sql");
let post_baseline: Vec<_> = migrations
.iter()
.filter(|m| m.version > baseline.version)
.cloned()
.collect();
if dry_run {
println!(
"Would provision: apply baseline {} then {} migration(s).",
baseline.version,
post_baseline.len()
);
return Ok(());
}
println!("Applying baseline {}...", baseline.version);
report_unselected_baseline_modules(
&baseline_sql,
&baseline.path,
&selection,
baseline.version,
)?;
register_and_apply_baseline(
pool,
config,
baseline.version,
&baseline_sql,
source,
|section| selection.selects(section.module.as_deref()),
)
.await?;
runtime
.record_provisioned(&selection.named().cloned().unwrap_or_default())
.await?;
apply_pending_migrations(pool, config, &post_baseline, &selection, &mut runtime)
.await?;
println!("✅ Provisioned from baseline {}.", baseline.version);
}
None => {
if dry_run {
println!(
"Would provision: apply {} migration(s) (no baseline).",
migrations.len()
);
return Ok(());
}
if migrations.is_empty() {
println!("Nothing to provision: no baseline and no migrations found.");
return Ok(());
}
println!("No baseline found; applying all migrations.");
runtime
.record_provisioned(&selection.named().cloned().unwrap_or_default())
.await?;
apply_pending_migrations(pool, config, &migrations, &selection, &mut runtime).await?;
println!("✅ Provisioned from migrations.");
}
}
Ok(())
}
fn report_unselected_baseline_modules(
baseline_sql: &str,
baseline_path: &Path,
selection: &ModuleSelection,
version: u64,
) -> Result<()> {
let sections = crate::migration::parse_migration_sections(baseline_path, baseline_sql)?;
let mut skipped: BTreeSet<String> = BTreeSet::new();
for section in §ions {
if let Some(module) = section.module.as_deref()
&& !selection.selects(Some(module))
{
skipped.insert(module.to_string());
}
}
for module in skipped {
println!(
"Skipping module '{}' sections in baseline {} (not established here)",
module, version
);
}
Ok(())
}
async fn register_and_apply_baseline(
pool: &PgPool,
config: &Config,
version: u64,
baseline_sql: &str,
source: &str,
select_section: impl Fn(&crate::migration::section_parser::MigrationSection) -> bool + Copy,
) -> Result<()> {
let new_checksum = calculate_checksum(baseline_sql);
let full_sections =
crate::migration::parse_migration_sections(Path::new(source), baseline_sql)?;
crate::migration::validate_sections(&full_sections)
.with_context(|| format!("Invalid section configuration in baseline ({})", source))?;
let recorded = crate::migration_tracking::section_tracking::fetch_section_rows(
pool,
&config.migration.tracking_table,
version,
true,
)
.await?;
let had_checksummed =
crate::migration_tracking::section_tracking::validate_and_sync_section_checksums(
pool,
&config.migration.tracking_table,
version,
true,
&full_sections,
&recorded,
)
.await?;
let store =
crate::migration_tracking::TrackingStore::new(pool, &config.migration.tracking_table)?;
let existing_checksum = store.baseline_stored_checksum(version).await?;
if let Some(stored) = existing_checksum
&& stored != new_checksum
{
if !had_checksummed {
anyhow::bail!(
"Baseline {} was modified after being partially applied!\n\
Expected checksum: {}\n\
Actual checksum: {}\n\n\
A previous provision recorded some of this baseline's sections from \
different content; resuming from the edited file would leave the target \
matching neither version. Restore the baseline to the applied content to \
resume, or reset the target and re-provision from the current baseline.",
version,
stored,
new_checksum,
);
}
crate::migration_tracking::update_stored_file_checksum(
pool,
&config.migration.tracking_table,
version,
true,
&new_checksum,
)
.await?;
}
let selected: Vec<(i32, crate::migration::section_parser::MigrationSection)> = full_sections
.iter()
.enumerate()
.filter(|(_, s)| select_section(s))
.map(|(i, s)| (i as i32, s.clone()))
.collect();
register_baseline_start(
pool,
&config.migration.tracking_table,
version,
"baseline",
&new_checksum,
&selected,
)
.await?;
apply_baseline_to_target(
pool,
&config.migration.tracking_table,
version,
&selected,
source,
)
.await
}