use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TopCommittersRow {
pub author: String,
pub commits: u32,
pub loc_added: u64,
pub loc_deleted: u64,
pub first_commit: String,
pub last_commit: String,
pub is_bot: bool,
}
const SQL_TEMPLATE: &str = "
WITH {human_aliases},
per_author AS (
SELECT
commits.canonical_author AS author,
COUNT(DISTINCT commits.rev) AS commits,
COALESCE(SUM(c.loc_added), 0)::BIGINT AS loc_added,
COALESCE(SUM(c.loc_deleted), 0)::BIGINT AS loc_deleted,
MIN(commits.date) AS first_at,
MAX(commits.date) AS last_at
FROM commits
LEFT JOIN changes c ON c.rev = commits.rev
GROUP BY commits.canonical_author
)
SELECT
pa.author,
CAST(pa.commits AS UINTEGER) AS commits,
pa.loc_added,
pa.loc_deleted,
CAST(CAST(pa.first_at AS DATE) AS TEXT) AS first_commit,
CAST(CAST(pa.last_at AS DATE) AS TEXT) AS last_commit,
(aa.canonical IS NULL) AS is_bot
FROM per_author pa
-- Pair-granular: a canonical is bot ONLY when it has NO human alias at
-- all (author_aliases is keyed on (raw_name, raw_email); a canonical
-- with at least one human alias stays eligible through that alias, even
-- if it ALSO owns a bot-classified alias). DISTINCT dedupes the lookup
-- to one row per canonical so the join can't multiply per_author rows.
LEFT JOIN (SELECT DISTINCT canonical FROM human_aliases) aa ON aa.canonical = pa.author
ORDER BY commits DESC, author ASC
LIMIT ?
";
#[tracing::instrument(name = "top-committers", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_top_committers(db: &FactsDb, opts: &Options) -> Result<Vec<TopCommittersRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let sql = SQL_TEMPLATE.replace("{human_aliases}", super::query::HUMAN_ALIASES_CTE);
super::query::explain_if_requested(db, &sql, params![row_limit], "top-committers", opts)?;
super::query::query_map_collect(db, &sql, params![row_limit], "top-committers", |r| {
Ok(TopCommittersRow {
author: r.get::<_, String>(0)?,
commits: r.get::<_, u32>(1)?,
loc_added: u64::try_from(r.get::<_, i64>(2)?).unwrap_or(u64::MAX),
loc_deleted: u64::try_from(r.get::<_, i64>(3)?).unwrap_or(u64::MAX),
first_commit: r.get::<_, String>(4)?,
last_commit: r.get::<_, String>(5)?,
is_bot: r.get::<_, bool>(6)?,
})
})
}