use super::FactsDb;
use crate::facts::GroupMap;
use crate::options::TimeBucket;
use crate::{CodeLoreError, Result};
pub fn materialize_changes_bucketed(
db: &FactsDb,
bucket: TimeBucket,
use_lineage: bool,
) -> Result<()> {
use duckdb::params;
let unit = bucket.as_sql_unit();
let src = if use_lineage {
super::lineage::materialize_changes_lineage(db)?;
"changes_lineage"
} else {
"changes"
};
let sql = format!(
"CREATE OR REPLACE TEMPORARY TABLE changes_bucketed AS \
SELECT \
CAST(date_trunc('{unit}', m.date) AS TEXT) AS rev, \
c.path, \
MAX(c.change_type) AS change_type, \
arg_max(c.rename_from, ROW(m.date, -m.rowid)) AS rename_from, \
SUM(c.loc_added)::INTEGER AS loc_added, \
SUM(c.loc_deleted)::INTEGER AS loc_deleted \
FROM {src} c \
INNER JOIN commits m ON m.rev = c.rev \
GROUP BY date_trunc('{unit}', m.date), c.path"
);
db.conn().execute(&sql, params![]).map_err(|e| {
CodeLoreError::Analysis(format!("materialize changes_bucketed ({unit}): {e}"))
})?;
tracing::info!("materialized changes_bucketed at {unit} granularity (lineage={use_lineage})");
Ok(())
}
pub fn apply_grouping(db: &FactsDb, group_map: &GroupMap) -> Result<()> {
use duckdb::params;
let conn = db.conn();
let distinct_paths: Vec<String> = {
let mut stmt = conn
.prepare("SELECT DISTINCT path FROM changes")
.map_err(|e| CodeLoreError::Analysis(format!("prepare distinct paths: {e}")))?;
let rows = stmt
.query_map([], |r| r.get::<_, String>(0))
.map_err(|e| CodeLoreError::Analysis(format!("query distinct paths: {e}")))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect distinct paths: {e}")))?
};
conn.execute(
"CREATE OR REPLACE TEMPORARY TABLE _grouping_v1 (\
raw_path TEXT PRIMARY KEY, group_name TEXT\
)",
[],
)
.map_err(|e| CodeLoreError::Analysis(format!("create _grouping_v1: {e}")))?;
{
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
let strict = group_map.strict;
let mapped: Vec<(String, Option<String>)> = distinct_paths
.par_iter()
.map(|path| {
let raw = path.clone();
let effective: Option<String> = if strict {
group_map.map_entity(path).map(str::to_owned)
} else {
Some(
group_map
.map_entity(path)
.map_or_else(|| path.clone(), str::to_owned),
)
};
(raw, effective)
})
.collect();
let mut app = conn
.appender("_grouping_v1")
.map_err(|e| CodeLoreError::Analysis(format!("appender _grouping_v1: {e}")))?;
for (raw, effective) in &mapped {
app.append_row(params![raw, effective.as_deref()])
.map_err(|e| CodeLoreError::Analysis(format!("grouping append row: {e}")))?;
}
app.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush _grouping_v1 appender: {e}")))?;
}
conn.execute(
"CREATE OR REPLACE TEMPORARY TABLE _changes_grouped AS \
SELECT \
c.rev, \
g.group_name AS path, \
MAX(c.change_type) AS change_type, \
arg_max(c.rename_from, c.path) AS rename_from, \
SUM(c.loc_added)::INTEGER AS loc_added, \
SUM(c.loc_deleted)::INTEGER AS loc_deleted \
FROM changes c \
INNER JOIN _grouping_v1 g ON g.raw_path = c.path \
WHERE g.group_name IS NOT NULL \
GROUP BY c.rev, g.group_name",
[],
)
.map_err(|e| CodeLoreError::Analysis(format!("build _changes_grouped: {e}")))?;
swap_grouped_tables(conn)?;
db.invalidate_changes_lineage();
materialize_complexity_grouped(conn)?;
tracing::info!(
"grouping applied: {} rules, {} distinct paths, strict={}",
group_map.rules.len(),
distinct_paths.len(),
group_map.strict
);
Ok(())
}
fn swap_grouped_tables(conn: &duckdb::Connection) -> Result<()> {
conn.execute(
"CREATE OR REPLACE TEMPORARY TABLE _hunks_surviving AS \
SELECT h.* FROM hunks h \
WHERE EXISTS ( \
SELECT 1 FROM changes c \
INNER JOIN _grouping_v1 g ON g.raw_path = c.path \
WHERE g.group_name = c.path \
AND c.rev = h.rev \
AND c.path = h.path \
)",
[],
)
.map_err(|e| CodeLoreError::Analysis(format!("snapshot surviving hunks: {e}")))?;
conn.execute("DROP TABLE hunks", [])
.map_err(|e| CodeLoreError::Analysis(format!("drop hunks for swap: {e}")))?;
conn.execute("DROP TABLE changes", [])
.map_err(|e| CodeLoreError::Analysis(format!("drop changes for swap: {e}")))?;
conn.execute_batch(crate::facts::schema::SCHEMA_V1)
.map_err(|e| CodeLoreError::Analysis(format!("recreate swapped tables: {e}")))?;
conn.execute(
"INSERT INTO changes (rev, path, change_type, rename_from, loc_added, loc_deleted) \
SELECT rev, path, change_type, rename_from, loc_added, loc_deleted \
FROM _changes_grouped",
[],
)
.map_err(|e| CodeLoreError::Analysis(format!("swap changes: {e}")))?;
conn.execute("INSERT INTO hunks SELECT * FROM _hunks_surviving", [])
.map_err(|e| CodeLoreError::Analysis(format!("restore surviving hunks: {e}")))?;
Ok(())
}
fn materialize_complexity_grouped(conn: &duckdb::Connection) -> Result<()> {
conn.execute("DROP TABLE IF EXISTS complexity_metrics_grouped", [])
.map_err(|e| CodeLoreError::Analysis(format!("drop complexity_metrics_grouped: {e}")))?;
conn.execute(
"CREATE TABLE complexity_metrics_grouped AS \
WITH fn_metrics AS ( \
SELECT \
g.group_name AS path, \
MAX(cm.cognitive)::INTEGER AS cognitive, \
MAX(cm.cyclomatic)::INTEGER AS cyclomatic, \
MAX(cm.loc)::INTEGER AS loc, \
MAX(cm.sloc)::INTEGER AS sloc, \
MAX(cm.nargs)::INTEGER AS nargs, \
MAX(cm.max_nesting)::INTEGER AS max_nesting, \
MAX(cm.bool_ops)::INTEGER AS bool_ops \
FROM complexity_metrics cm \
INNER JOIN _grouping_v1 g ON g.raw_path = cm.path \
WHERE g.group_name IS NOT NULL \
GROUP BY g.group_name \
), \
mi AS ( \
SELECT g.group_name AS path, MAX(cm.mi)::DOUBLE AS mi \
FROM complexity_metrics cm \
INNER JOIN entities e \
ON e.path = cm.path AND e.name = cm.name AND e.rev_last_seen = cm.rev \
INNER JOIN _grouping_v1 g ON g.raw_path = cm.path \
WHERE g.group_name IS NOT NULL AND e.kind = 'unit' AND cm.mi IS NOT NULL \
GROUP BY g.group_name \
) \
SELECT \
COALESCE(f.path, mi.path) AS path, \
NULL::TEXT AS name, \
f.cognitive, f.cyclomatic, f.loc, f.sloc, \
f.nargs, f.max_nesting, f.bool_ops, \
mi.mi \
FROM fn_metrics f FULL OUTER JOIN mi ON f.path = mi.path",
[],
)
.map_err(|e| CodeLoreError::Analysis(format!("materialize complexity_metrics_grouped: {e}")))?;
Ok(())
}