#![allow(clippy::format_push_string)]
use serde::Serialize;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use tokio_postgres::Client;
use uuid::Uuid;
use super::error::ApplyError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApplyRecord {
pub apply_id: Uuid,
pub plan_id: String,
pub pgevolve_version: String,
pub source_rev: Option<String>,
pub target_identity: String,
pub actor: Option<String>,
pub started_at: String,
pub finished_at: Option<String>,
pub status: String,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StepRecord {
pub apply_id: Uuid,
pub step_no: i32,
pub group_no: i32,
pub kind: String,
pub destructive: bool,
pub targets: Vec<String>,
pub sql_text: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub status: String,
pub error_message: Option<String>,
}
pub async fn fetch_recent_applies(
client: &Client,
limit: i64,
) -> Result<Vec<ApplyRecord>, ApplyError> {
let rows = client
.query(
"SELECT apply_id, plan_id, pgevolve_version, source_rev, target_identity,
actor, started_at, finished_at, status, error_message
FROM pgevolve.apply_log
ORDER BY started_at DESC
LIMIT $1",
&[&limit],
)
.await?;
let mut out = Vec::with_capacity(rows.len());
for r in rows {
out.push(ApplyRecord {
apply_id: r.get(0),
plan_id: r.get(1),
pgevolve_version: r.get(2),
source_rev: r.get(3),
target_identity: r.get(4),
actor: r.get(5),
started_at: format_ts(r.get(6)),
finished_at: r.get::<_, Option<OffsetDateTime>>(7).map(format_ts),
status: r.get(8),
error_message: r.get(9),
});
}
Ok(out)
}
pub async fn fetch_apply_steps(
client: &Client,
apply_id: Uuid,
) -> Result<Vec<StepRecord>, ApplyError> {
let rows = client
.query(
"SELECT apply_id, step_no, group_no, kind, destructive, targets,
sql_text, started_at, finished_at, status, error_message
FROM pgevolve.plan_steps
WHERE apply_id=$1
ORDER BY step_no",
&[&apply_id],
)
.await?;
let mut out = Vec::with_capacity(rows.len());
for r in rows {
out.push(StepRecord {
apply_id: r.get(0),
step_no: r.get(1),
group_no: r.get(2),
kind: r.get(3),
destructive: r.get(4),
targets: r.get(5),
sql_text: r.get(6),
started_at: r.get::<_, Option<OffsetDateTime>>(7).map(format_ts),
finished_at: r.get::<_, Option<OffsetDateTime>>(8).map(format_ts),
status: r.get(9),
error_message: r.get(10),
});
}
Ok(out)
}
pub fn format_status_human(record: &ApplyRecord, steps: &[StepRecord]) -> String {
let mut s = String::new();
s.push_str(&format!(
"apply {} plan={} status={}\n",
record.apply_id, record.plan_id, record.status,
));
s.push_str(&format!(
" started_at={} finished_at={}\n",
record.started_at,
record.finished_at.as_deref().unwrap_or("(running)"),
));
s.push_str(&format!(
" pgevolve={} source_rev={} target={}\n",
record.pgevolve_version,
record.source_rev.as_deref().unwrap_or("-"),
record.target_identity,
));
if let Some(err) = &record.error_message {
s.push_str(&format!(" error: {err}\n"));
}
s.push_str(&format!(" steps ({}):\n", steps.len()));
for st in steps {
s.push_str(&format!(
" [{:>3}] g{} {} {} status={}\n",
st.step_no,
st.group_no,
st.kind,
if st.destructive { "(destructive)" } else { "" },
st.status,
));
}
s
}
pub fn format_status_json(
record: &ApplyRecord,
steps: &[StepRecord],
) -> Result<String, serde_json::Error> {
#[derive(Serialize)]
struct Wrapper<'a> {
apply: &'a ApplyRecord,
steps: &'a [StepRecord],
}
serde_json::to_string_pretty(&Wrapper {
apply: record,
steps,
})
}
fn format_ts(t: OffsetDateTime) -> String {
t.format(&Rfc3339).unwrap_or_else(|_| t.to_string())
}