use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
pub const RECENT_DAYS: u32 = 30;
pub const BASELINE_DAYS: u32 = 90;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HotspotVelocityRow {
pub path: String,
pub revs_recent: u32,
pub revs_baseline: u32,
pub recent_per_week: f64,
pub baseline_per_week: f64,
pub acceleration: f64,
}
const SQL_TEMPLATE: &str = "
WITH anchor AS (
SELECT COALESCE(CAST(? AS TIMESTAMP), {now_anchor}) AS now_ts
FROM commits
),
win AS (
SELECT
now_ts,
now_ts - INTERVAL '{recent} days' AS recent_start,
now_ts - INTERVAL '{boundary} days' AS baseline_start
FROM anchor
),
recent AS (
SELECT ch.path, COUNT(ch.rev) AS revs_recent
FROM changes ch
INNER JOIN commits c ON c.rev = ch.rev
CROSS JOIN win w
WHERE c.date > w.recent_start AND c.date <= w.now_ts
GROUP BY ch.path
),
baseline AS (
SELECT ch.path, COUNT(ch.rev) AS revs_baseline
FROM changes ch
INNER JOIN commits c ON c.rev = ch.rev
CROSS JOIN win w
WHERE c.date > w.baseline_start AND c.date <= w.recent_start
GROUP BY ch.path
)
SELECT
r.path,
r.revs_recent,
COALESCE(b.revs_baseline, 0) AS revs_baseline,
r.revs_recent * 7.0 / {recent}.0 AS recent_per_week,
COALESCE(b.revs_baseline, 0) * 7.0 / {baseline}.0 AS baseline_per_week,
(r.revs_recent * 7.0 / {recent}.0)
- (COALESCE(b.revs_baseline, 0) * 7.0 / {baseline}.0) AS acceleration
FROM recent r
LEFT JOIN baseline b ON r.path = b.path
WHERE (r.revs_recent + COALESCE(b.revs_baseline, 0)) >= ?
ORDER BY acceleration DESC, revs_recent DESC, path ASC
LIMIT ?
";
fn build_sql() -> String {
SQL_TEMPLATE
.replace(
"{now_anchor}",
&crate::analyses::query::clamped_now_anchor("date"),
)
.replace("{recent}", &RECENT_DAYS.to_string())
.replace("{baseline}", &BASELINE_DAYS.to_string())
.replace("{boundary}", &(RECENT_DAYS + BASELINE_DAYS).to_string())
}
#[tracing::instrument(name = "hotspot-velocity", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_hotspot_velocity(db: &FactsDb, opts: &Options) -> Result<Vec<HotspotVelocityRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let anchor: Option<String> = opts.age_time_now.map(|d| {
format!(
"{:04}-{:02}-{:02} 23:59:59",
d.year(),
u8::from(d.month()),
d.day()
)
});
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let sql = crate::analyses::lineage::rewrite(&build_sql(), opts);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![anchor, opts.min_revs, row_limit],
"hotspot-velocity",
opts,
)?;
crate::analyses::query::query_map_collect(
db,
&sql,
params![anchor, opts.min_revs, row_limit],
"hotspot-velocity",
|r| {
Ok(HotspotVelocityRow {
path: r.get::<_, String>(0)?,
revs_recent: u32::try_from(r.get::<_, i64>(1)?).unwrap_or(u32::MAX),
revs_baseline: u32::try_from(r.get::<_, i64>(2)?).unwrap_or(u32::MAX),
recent_per_week: r.get::<_, f64>(3)?,
baseline_per_week: r.get::<_, f64>(4)?,
acceleration: r.get::<_, f64>(5)?,
})
},
)
}