use tokio_postgres::{Client, Error as PgError};
use uuid::Uuid;
fn render_pg_error(e: &PgError) -> String {
e.as_db_error().map_or_else(
|| e.to_string(),
|db| {
format!(
"[{code}] {msg}: {detail}",
code = db.code().code(),
msg = db.message(),
detail = db.detail().unwrap_or(""),
)
},
)
}
use pgevolve_core::plan::plan::StepOverride;
use pgevolve_core::plan::plan::kind_name;
use pgevolve_core::plan::raw_step::RawStep;
use pgevolve_core::plan::{Plan, TransactionGroup};
use super::audit;
use super::error::ApplyError;
fn is_step_suppressed(step: &RawStep, overrides: &[StepOverride]) -> bool {
let wire = kind_name(step.kind);
let primary_target = step
.targets
.first()
.map(pgevolve_core::identifier::QualifiedName::render_sql);
overrides.iter().any(|so| {
so.suppress && so.kind == wire && primary_target.as_deref() == Some(so.target.as_str())
})
}
pub async fn execute_plan(
client: &mut Client,
plan: &Plan,
apply_id: Uuid,
abort_after_step: Option<u32>,
) -> Result<(), ApplyError> {
for group in &plan.groups {
if group.transactional {
execute_transactional_group(
client,
apply_id,
group,
&plan.step_overrides,
abort_after_step,
)
.await?;
} else {
execute_autocommit_group(
client,
apply_id,
group,
&plan.step_overrides,
abort_after_step,
)
.await?;
}
}
Ok(())
}
async fn execute_transactional_group(
client: &mut Client,
apply_id: Uuid,
group: &TransactionGroup,
step_overrides: &[StepOverride],
abort_after_step: Option<u32>,
) -> Result<(), ApplyError> {
let tx = client.transaction().await?;
let mut abort_step: Option<u32> = None;
for step in &group.steps {
if is_step_suppressed(step, step_overrides) {
audit::mark_step_suppressed(tx.client(), apply_id, step.step_no).await?;
if abort_after_step == Some(step.step_no) {
abort_step = Some(step.step_no);
break;
}
continue;
}
audit::mark_step_running(tx.client(), apply_id, step.step_no).await?;
let resolved_sql =
crate::executor::env_interp::resolve(&step.sql, |k| std::env::var(k).ok())
.map_err(|e| ApplyError::MissingEnvVar(e.0, step.step_no))?;
if let Err(e) = tx.batch_execute(&resolved_sql).await {
let err_msg = render_pg_error(&e);
tx.rollback().await?;
audit::mark_step_failed(client, apply_id, step.step_no, &err_msg).await?;
audit::mark_steps_rolled_back(client, apply_id, group.id).await?;
return Err(ApplyError::StepFailed {
step_no: step.step_no,
group_no: group.id,
error: err_msg,
});
}
audit::mark_step_succeeded(tx.client(), apply_id, step.step_no).await?;
if abort_after_step == Some(step.step_no) {
abort_step = Some(step.step_no);
break;
}
}
tx.commit().await?;
if let Some(step_no) = abort_step {
return Err(ApplyError::AbortedAfterStep { step_no });
}
Ok(())
}
async fn execute_autocommit_group(
client: &Client,
apply_id: Uuid,
group: &TransactionGroup,
step_overrides: &[StepOverride],
abort_after_step: Option<u32>,
) -> Result<(), ApplyError> {
for step in &group.steps {
if is_step_suppressed(step, step_overrides) {
audit::mark_step_suppressed(client, apply_id, step.step_no).await?;
if abort_after_step == Some(step.step_no) {
return Err(ApplyError::AbortedAfterStep {
step_no: step.step_no,
});
}
continue;
}
audit::mark_step_running(client, apply_id, step.step_no).await?;
let resolved_sql =
crate::executor::env_interp::resolve(&step.sql, |k| std::env::var(k).ok())
.map_err(|e| ApplyError::MissingEnvVar(e.0, step.step_no))?;
if let Err(e) = client.batch_execute(&resolved_sql).await {
let err_msg = render_pg_error(&e);
audit::mark_step_failed(client, apply_id, step.step_no, &err_msg).await?;
return Err(ApplyError::StepFailed {
step_no: step.step_no,
group_no: group.id,
error: err_msg,
});
}
audit::mark_step_succeeded(client, apply_id, step.step_no).await?;
if abort_after_step == Some(step.step_no) {
return Err(ApplyError::AbortedAfterStep {
step_no: step.step_no,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pgevolve_core::identifier::{Identifier, QualifiedName};
use pgevolve_core::plan::raw_step::{RawStep, StepKind, TransactionConstraint};
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn refresh_step(target_schema: &str, target_name: &str) -> RawStep {
RawStep {
step_no: 1,
kind: StepKind::RefreshMaterializedView,
destructive: false,
destructive_reason: None,
intent_id: None,
targets: vec![qn(target_schema, target_name)],
sql: format!("REFRESH MATERIALIZED VIEW {target_schema}.{target_name};"),
transactional: TransactionConstraint::InTransaction,
}
}
#[test]
fn suppressed_step_matches_kind_and_target() {
let step = refresh_step("app", "daily_revenue");
let overrides = vec![StepOverride {
kind: "refresh_materialized_view".to_string(),
target: "app.daily_revenue".to_string(),
suppress: true,
}];
assert!(is_step_suppressed(&step, &overrides));
}
#[test]
fn non_suppressed_override_does_not_suppress() {
let step = refresh_step("app", "daily_revenue");
let overrides = vec![StepOverride {
kind: "refresh_materialized_view".to_string(),
target: "app.daily_revenue".to_string(),
suppress: false,
}];
assert!(!is_step_suppressed(&step, &overrides));
}
#[test]
fn kind_mismatch_does_not_suppress() {
let step = refresh_step("app", "daily_revenue");
let overrides = vec![StepOverride {
kind: "create_materialized_view".to_string(),
target: "app.daily_revenue".to_string(),
suppress: true,
}];
assert!(!is_step_suppressed(&step, &overrides));
}
#[test]
fn target_mismatch_does_not_suppress() {
let step = refresh_step("app", "daily_revenue");
let overrides = vec![StepOverride {
kind: "refresh_materialized_view".to_string(),
target: "app.other_mv".to_string(),
suppress: true,
}];
assert!(!is_step_suppressed(&step, &overrides));
}
#[test]
fn empty_overrides_does_not_suppress() {
let step = refresh_step("app", "daily_revenue");
assert!(!is_step_suppressed(&step, &[]));
}
#[test]
fn step_with_no_targets_is_not_suppressed() {
let step = RawStep {
step_no: 1,
kind: StepKind::RefreshMaterializedView,
destructive: false,
destructive_reason: None,
intent_id: None,
targets: vec![],
sql: "REFRESH MATERIALIZED VIEW app.daily_revenue;".to_string(),
transactional: TransactionConstraint::InTransaction,
};
let overrides = vec![StepOverride {
kind: "refresh_materialized_view".to_string(),
target: "app.daily_revenue".to_string(),
suppress: true,
}];
assert!(!is_step_suppressed(&step, &overrides));
}
}