use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, Copy)]
pub enum MainDevMetric {
Added,
Deleted,
RevCount,
}
impl MainDevMetric {
const fn sql_expr(self) -> &'static str {
match self {
Self::Added => "SUM(c.loc_added)",
Self::Deleted => "SUM(c.loc_deleted)",
Self::RevCount => "COUNT(*)",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MainDevRow {
pub entity: String,
pub main_dev: String,
pub metric: u64,
pub total: u64,
pub ownership: f64,
}
fn run(db: &FactsDb, opts: &Options, metric: MainDevMetric) -> Result<Vec<MainDevRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let metric_expr = metric.sql_expr();
let sql = format!(
"WITH ea AS (
SELECT c.path AS entity, m.canonical_author AS author,
{metric_expr}::BIGINT AS metric
FROM changes c JOIN commits m USING (rev)
GROUP BY c.path, m.canonical_author
),
winners AS (
-- `first(... ORDER BY ...)` + per-group MAX/SUM collapses the
-- old `ranked`+`totals` pair plus their self-join into a single
-- grouped aggregate (deterministic ASC author tiebreak).
SELECT entity,
first(author ORDER BY metric DESC, author ASC) AS author,
MAX(metric) AS metric,
SUM(metric) AS total
FROM ea
GROUP BY entity
),
file_revs AS (
-- (rev, path) is the changes PK; COUNT(rev) == COUNT(DISTINCT rev)
-- per path. Plain COUNT skips DuckDB's distinct-tracking overhead.
SELECT path, COUNT(rev) AS revs FROM changes
GROUP BY path HAVING revs >= ?
)
SELECT w.entity, w.author, w.metric, w.total,
ROUND(w.metric::DOUBLE / GREATEST(w.total, 1), 2) AS ownership
FROM winners w
INNER JOIN file_revs fr ON fr.path = w.entity
ORDER BY w.entity ASC
LIMIT ?"
);
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let sql = crate::analyses::lineage::rewrite(&sql, opts);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![opts.min_revs, row_limit],
"main-dev",
opts,
)?;
crate::analyses::query::query_map_collect(
db,
&sql,
params![opts.min_revs, row_limit],
"main-dev",
|r| {
Ok(MainDevRow {
entity: r.get::<_, String>(0)?,
main_dev: r.get::<_, String>(1)?,
metric: u64::try_from(r.get::<_, i64>(2)?).unwrap_or(u64::MAX),
total: u64::try_from(r.get::<_, i64>(3)?).unwrap_or(u64::MAX),
ownership: r.get::<_, f64>(4)?,
})
},
)
}
#[tracing::instrument(name = "main-dev", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_main_dev(db: &FactsDb, opts: &Options) -> Result<Vec<MainDevRow>> {
run(db, opts, MainDevMetric::Added)
}
#[tracing::instrument(name = "main-dev-by-deletions", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_main_dev_by_deletions(db: &FactsDb, opts: &Options) -> Result<Vec<MainDevRow>> {
run(db, opts, MainDevMetric::Deleted)
}
#[tracing::instrument(name = "main-dev-by-revs", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_main_dev_by_revs(db: &FactsDb, opts: &Options) -> Result<Vec<MainDevRow>> {
run(db, opts, MainDevMetric::RevCount)
}