use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SocRow {
pub entity: String,
pub soc: u32,
}
fn build_soc_sql(
src: &str,
code_maat_compat: bool,
bucket: Option<crate::options::TimeBucket>,
use_lineage: bool,
) -> String {
let threshold_op = if code_maat_compat { ">" } else { ">=" };
let good_cte = crate::analyses::coupling::good_commits_cte(bucket, use_lineage);
format!(
"WITH {good_cte},
filtered_changes AS (
-- Pre-filter `changes` against `good_commits` ONCE so both
-- downstream CTEs share the result. DuckDB materializes a CTE
-- referenced 2+ times (rev_sizes + the outer SELECT), so the
-- self-implicit double scan of the raw `{src}` table is
-- collapsed into one filter pass + two cached reads. Same
-- pattern as `coupling.rs` (see the comment there for the
-- O(N²)-to-O(K²) complexity rationale on large repos).
SELECT rev, path
FROM {src}
INNER JOIN good_commits USING(rev)
),
rev_sizes AS (
-- (rev, path) is the changes PK so per `GROUP BY rev` each
-- path appears at most once. Plain COUNT skips DuckDB's
-- distinct-tracking overhead.
SELECT rev, COUNT(path) AS n
FROM filtered_changes
GROUP BY rev
)
SELECT c.path AS entity, SUM(rs.n - 1)::INTEGER AS soc
FROM filtered_changes c
INNER JOIN rev_sizes rs USING (rev)
GROUP BY c.path
HAVING SUM(rs.n - 1) {threshold_op} ?
ORDER BY soc DESC, entity ASC
LIMIT ?"
)
}
#[tracing::instrument(name = "soc", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_soc(db: &FactsDb, opts: &Options) -> Result<Vec<SocRow>> {
crate::analyses::lineage::materialize_source(db, opts)?;
let src = crate::analyses::lineage::source_table(opts);
let threshold: u32 = opts.min_soc.unwrap_or(if opts.code_maat_compat {
opts.min_revs
} else {
1
});
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let sql = build_soc_sql(
src,
opts.code_maat_compat,
opts.time_bucket,
opts.use_canonical_lineage,
);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![opts.max_changeset_size, threshold, row_limit],
"soc",
opts,
)?;
crate::analyses::query::query_map_collect(
db,
&sql,
params![opts.max_changeset_size, threshold, row_limit],
"soc",
|r| {
Ok(SocRow {
entity: r.get::<_, String>(0)?,
soc: u32::try_from(r.get::<_, i64>(1)?).unwrap_or(u32::MAX),
})
},
)
}