use anyhow::{Context, Result};
use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime};
use tokio_postgres::NoTls;
pub const CONTRACT_VIEW: &str = "core.v_entity_change_log";
#[derive(Debug, Clone, PartialEq)]
pub struct ChangeLogSample {
pub object_type: String,
pub modification_type: String,
pub duration_ms: Option<i32>,
pub duration_calc_version: Option<i64>,
pub created_at_epoch: f64,
pub object_id: Option<String>,
pub trace_id: Option<String>,
}
pub struct PerfReader {
pool: Pool,
}
impl PerfReader {
pub fn connect(db_url: &str) -> Result<Self> {
if !db_url.starts_with("postgres") {
anyhow::bail!(
"perf reads require a PostgreSQL connection URL (postgres://…); got: {db_url}"
);
}
let mut cfg = Config::new();
cfg.url = Some(db_url.to_string());
cfg.manager = Some(ManagerConfig {
recycling_method: RecyclingMethod::Fast,
});
cfg.pool = Some(deadpool_postgres::PoolConfig::new(2));
let pool = cfg
.create_pool(Some(Runtime::Tokio1), NoTls)
.context("failed to create PostgreSQL connection pool for perf reads")?;
Ok(Self { pool })
}
pub async fn db_now_epoch(&self) -> Result<f64> {
let client = self.pool.get().await.context("failed to acquire DB connection")?;
let row = client
.query_one("SELECT EXTRACT(EPOCH FROM now())::float8 AS now_epoch", &[])
.await
.context("failed to read database clock")?;
Ok(row.get("now_epoch"))
}
pub async fn load_samples(
&self,
window_days: i32,
object_type: Option<&str>,
) -> Result<Vec<ChangeLogSample>> {
let client = self.pool.get().await.context("failed to acquire DB connection")?;
let base = format!(
"SELECT object_type, modification_type, duration_ms, \
EXTRACT(EPOCH FROM created_at)::float8 AS created_at_epoch, \
(data->'extra_metadata'->>'duration_calc_version')::bigint AS duration_calc_version, \
object_id::text AS object_id_text, trace_id \
FROM {CONTRACT_VIEW} \
WHERE created_at >= now() - make_interval(days => $1)"
);
let rows = if let Some(object_type) = object_type {
client
.query(&format!("{base} AND object_type = $2"), &[&window_days, &object_type])
.await
} else {
client.query(&base, &[&window_days]).await
}
.with_context(|| format!("failed to read {CONTRACT_VIEW}"))?;
Ok(rows.iter().map(row_to_sample).collect())
}
}
fn row_to_sample(row: &tokio_postgres::Row) -> ChangeLogSample {
ChangeLogSample {
object_type: row.get("object_type"),
modification_type: row.get("modification_type"),
duration_ms: row.get("duration_ms"),
duration_calc_version: row.get("duration_calc_version"),
created_at_epoch: row.get("created_at_epoch"),
object_id: row.get("object_id_text"),
trace_id: row.get("trace_id"),
}
}