use std::time::Instant;
use codelore_lib::Options;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
fn build_spike_fixture() -> tempfile::TempDir {
fn git(path: &std::path::Path, args: &[&str]) {
let out = std::process::Command::new("git")
.args(args)
.current_dir(path)
.output()
.expect("git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn write(p: std::path::PathBuf, content: &str) {
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, content).unwrap();
}
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
git(path, &["init", "-b", "main", "--quiet"]);
git(path, &["config", "user.email", "test@example.com"]);
git(path, &["config", "user.name", "T"]);
let authors = [
("Alice", "alice@example.com"),
("Bob", "bob@example.com"),
("Carol", "carol@example.com"),
];
let files = ["src/a.rs", "src/b.rs", "src/c.rs", "src/d.rs", "src/e.rs"];
for i in 0..30 {
let f = files[i % files.len()];
write(
path.join(f),
&format!("// v{i}\npub fn f_{i}() -> u32 {{ {i} }}\n"),
);
let (name, email) = authors[i % authors.len()];
let author = format!("{name} <{email}>");
git(path, &["add", f]);
git(
path,
&[
"commit",
"-m",
&format!("c{i}"),
"--author",
&author,
"--quiet",
],
);
}
dir
}
const SQL_CTE: &str = "
WITH author_revs AS (
SELECT
changes.path,
commits.canonical_author AS author,
COUNT(changes.rev) AS revs
FROM changes
INNER JOIN commits ON changes.rev = commits.rev
GROUP BY changes.path, commits.canonical_author
),
totals AS (
SELECT path, SUM(revs) AS total
FROM author_revs
GROUP BY path
),
hhi AS (
SELECT
ar.path,
t.total,
1.0 - SUM(POWER(CAST(ar.revs AS DOUBLE) / NULLIF(CAST(t.total AS DOUBLE), 0), 2)) AS fv
FROM author_revs ar
INNER JOIN totals t ON ar.path = t.path
GROUP BY ar.path, t.total
)
SELECT path, total, fv FROM hhi ORDER BY path
";
const SQL_WINDOW: &str = "
WITH author_revs AS (
SELECT
changes.path,
commits.canonical_author AS author,
COUNT(changes.rev) AS revs
FROM changes
INNER JOIN commits ON changes.rev = commits.rev
GROUP BY changes.path, commits.canonical_author
),
author_revs_with_total AS (
SELECT
path,
author,
revs,
SUM(revs) OVER (PARTITION BY path) AS total
FROM author_revs
),
hhi AS (
SELECT
path,
ANY_VALUE(total) AS total,
1.0 - SUM(POWER(CAST(revs AS DOUBLE) / NULLIF(CAST(total AS DOUBLE), 0), 2)) AS fv
FROM author_revs_with_total
GROUP BY path
)
SELECT path, total, fv FROM hhi ORDER BY path
";
#[derive(Debug, PartialEq)]
struct HhiRow {
path: String,
total: i64,
fv: f64,
}
fn collect_hhi(db: &FactsDb, sql: &str) -> Vec<HhiRow> {
let mut stmt = db.prepare(sql).expect("prepare");
let rows = stmt
.query_map([], |r| {
Ok(HhiRow {
path: r.get::<_, String>(0)?,
total: r.get::<_, i64>(1)?,
fv: r.get::<_, f64>(2)?,
})
})
.expect("query_map");
rows.collect::<std::result::Result<Vec<_>, _>>()
.expect("collect")
}
#[test]
fn cte_and_window_return_byte_identical_rows() {
let fixture = build_spike_fixture();
let opts = Options {
repo_path: fixture.path().to_path_buf(),
..Options::default()
};
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
db.ingest(&repo, &opts).expect("ingest");
let t_cte = Instant::now();
let rows_cte = collect_hhi(&db, SQL_CTE);
let dur_cte = t_cte.elapsed();
let t_win = Instant::now();
let rows_win = collect_hhi(&db, SQL_WINDOW);
let dur_win = t_win.elapsed();
assert_eq!(
rows_cte.len(),
rows_win.len(),
"row counts differ: cte={} window={}",
rows_cte.len(),
rows_win.len(),
);
for (a, b) in rows_cte.iter().zip(rows_win.iter()) {
assert_eq!(a.path, b.path, "path mismatch");
assert_eq!(a.total, b.total, "total mismatch for `{}`", a.path);
assert!(
(a.fv - b.fv).abs() < 1e-12,
"fv mismatch for `{}`: cte={} window={}",
a.path,
a.fv,
b.fv,
);
}
eprintln!("[F69 spike] inline-fixture timings:");
eprintln!(" CTE-totals: {dur_cte:?}");
eprintln!(" window function: {dur_win:?}");
eprintln!(
" delta: {}",
if dur_cte > dur_win {
format!("window faster by {:?}", dur_cte.saturating_sub(dur_win))
} else {
format!("CTE faster by {:?}", dur_win.saturating_sub(dur_cte))
}
);
}
#[test]
fn capture_explain_analyze_plans() {
let fixture = build_spike_fixture();
let opts = Options {
repo_path: fixture.path().to_path_buf(),
..Options::default()
};
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
db.ingest(&repo, &opts).expect("ingest");
let dump = |label: &str, sql: &str| {
let explain_sql = format!("EXPLAIN ANALYZE {sql}");
let mut stmt = db.prepare(&explain_sql).expect("prepare");
let rows = stmt
.query_map([], |r| r.get::<_, String>(1))
.expect("query_map");
let plan = rows
.collect::<std::result::Result<Vec<_>, _>>()
.expect("collect")
.join("\n");
eprintln!("\n=== EXPLAIN ANALYZE: {label} ===\n{plan}\n");
};
dump("CTE-totals", SQL_CTE);
dump("window function", SQL_WINDOW);
}