use duckdb::Appender;
use super::{FactsDb, IngestStats};
use crate::identity;
use crate::{ChangeType, CodeLoreError, CommitEvent, Result};
const STANDARD_VECTOR_SIZE: u64 = 2048;
fn crosses_chunk_boundary(prev_total: u64) -> bool {
(prev_total + 1).is_multiple_of(STANDARD_VECTOR_SIZE)
}
pub(super) fn ingest_loop(
db: &FactsDb,
rx: crossbeam_channel::Receiver<CommitEvent>,
team_map: &identity::TeamMap,
bot_patterns: &identity::BotPatterns,
paths_filter: &crate::paths_filter::PathsFilter,
) -> Result<IngestStats> {
use std::collections::HashMap;
let mut stats = IngestStats::default();
let mut commits_app = db
.conn()
.appender("commits")
.map_err(|e| CodeLoreError::Analysis(format!("appender commits: {e}")))?;
let mut changes_app = db
.conn()
.appender("changes")
.map_err(|e| CodeLoreError::Analysis(format!("appender changes: {e}")))?;
let mut hunks_app = db
.conn()
.appender("hunks")
.map_err(|e| CodeLoreError::Analysis(format!("appender hunks: {e}")))?;
let mut commit_parents_app = db
.conn()
.appender("commit_parents")
.map_err(|e| CodeLoreError::Analysis(format!("appender commit_parents: {e}")))?;
let mut change_appends: u64 = 0;
let mut hunk_appends: u64 = 0;
let mut alias_map: HashMap<(String, String), (String, bool)> = HashMap::new();
for mut event in rx {
let pre_team_canonical = event
.canonical_author
.as_deref()
.unwrap_or(&event.author_email);
let canonical = identity::apply_team_map(team_map, pre_team_canonical).to_string();
if !team_map.is_empty() {
event.canonical_author = Some(canonical.clone());
}
let bot = bot_patterns.is_bot(&event.author_email, &event.author_name);
event.ai_attribution = Some(
identity::ai_attribution_with(
bot_patterns,
&event.author_email,
&event.author_name,
&event.message,
)
.to_string(),
);
alias_map
.entry((event.author_name.clone(), event.author_email.clone()))
.or_insert((canonical, bot));
append_commit(&mut commits_app, &event)?;
append_parents(&mut commit_parents_app, &event)?;
for ch in &event.changes {
let rel_path = std::path::Path::new(&ch.path);
if crate::paths_filter::is_git_metadata(rel_path)
|| paths_filter.is_excluded(rel_path, false)
{
continue;
}
append_change(
&mut commits_app,
&mut changes_app,
&mut hunks_app,
&mut change_appends,
&mut hunk_appends,
&event.rev,
ch,
)?;
stats.changes_ingested += 1;
}
stats.commits_ingested += 1;
}
commits_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush commits: {e}")))?;
changes_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush changes: {e}")))?;
hunks_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush hunks: {e}")))?;
commit_parents_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush commit_parents: {e}")))?;
let mut aliases_app = db
.conn()
.appender("author_aliases")
.map_err(|e| CodeLoreError::Analysis(format!("appender author_aliases: {e}")))?;
for ((raw_name, raw_email), (canonical, is_bot)) in &alias_map {
use duckdb::params;
aliases_app
.append_row(params![raw_name, raw_email, canonical, is_bot])
.map_err(|e| CodeLoreError::Analysis(format!("append author_alias: {e}")))?;
}
aliases_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush author_aliases: {e}")))?;
Ok(stats)
}
pub(crate) fn format_timestamp(ts: time::OffsetDateTime) -> String {
let utc = ts.to_offset(time::UtcOffset::UTC);
let y = utc.year();
let m = utc.month() as u8;
let d = utc.day();
let hh = utc.hour();
let mm = utc.minute();
let ss = utc.second();
format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}")
}
fn append_commit(app: &mut Appender<'_>, e: &CommitEvent) -> Result<()> {
use duckdb::params;
let date_str = format_timestamp(e.date);
let committer_date_str = format_timestamp(e.committer_date);
let canonical = e
.canonical_author
.as_deref()
.unwrap_or(&e.author_email)
.to_string();
let ai_attr = e.ai_attribution.as_deref().map(str::to_string);
app.append_row(params![
e.rev,
e.author_email,
e.author_name,
e.committer_email,
canonical,
ai_attr,
date_str,
committer_date_str,
e.message,
e.parents.len() > 1,
i32::try_from(e.parents.len()).unwrap_or(i32::MAX),
Option::<i32>::None,
Option::<i32>::None,
Option::<i32>::None,
Option::<f64>::None,
Option::<i32>::None,
Option::<i32>::None,
Option::<f64>::None,
Option::<bool>::None,
Option::<i32>::None,
Option::<f64>::None,
Option::<i32>::None,
Option::<i32>::None,
Option::<f64>::None,
Option::<i32>::None,
])
.map_err(|err| CodeLoreError::Analysis(format!("append commit: {err}")))?;
Ok(())
}
fn append_parents(app: &mut Appender<'_>, e: &CommitEvent) -> Result<()> {
use duckdb::params;
for (pos, parent_rev) in e.parents.iter().enumerate() {
let position = i32::try_from(pos).unwrap_or(i32::MAX);
app.append_row(params![e.rev, parent_rev, position])
.map_err(|err| CodeLoreError::Analysis(format!("append commit_parent: {err}")))?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn append_change(
commits_app: &mut Appender<'_>,
changes_app: &mut Appender<'_>,
hunks_app: &mut Appender<'_>,
change_appends: &mut u64,
hunk_appends: &mut u64,
rev: &str,
ch: &crate::FileChange,
) -> Result<()> {
use duckdb::params;
let (type_str, rename_from) = match &ch.change_type {
ChangeType::Added => ("added", None),
ChangeType::Modified => ("modified", None),
ChangeType::Deleted => ("deleted", None),
ChangeType::Renamed { from, .. } => ("renamed", Some(from.as_str())),
ChangeType::Copied { from, .. } => ("copied", Some(from.as_str())),
ChangeType::BinaryOrUnknown => ("binary", None),
};
if crosses_chunk_boundary(*change_appends) {
commits_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush commits (fk order): {e}")))?;
}
changes_app
.append_row(params![
rev,
ch.path,
type_str,
rename_from,
i32::try_from(ch.loc_added).unwrap_or(i32::MAX),
i32::try_from(ch.loc_deleted).unwrap_or(i32::MAX),
])
.map_err(|err| CodeLoreError::Analysis(format!("append change: {err}")))?;
*change_appends += 1;
for hunk in &ch.hunks {
if crosses_chunk_boundary(*hunk_appends) {
commits_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush commits (fk order): {e}")))?;
changes_app
.flush()
.map_err(|e| CodeLoreError::Analysis(format!("flush changes (fk order): {e}")))?;
}
hunks_app
.append_row(params![
rev,
ch.path,
i32::try_from(hunk.old_start).unwrap_or(i32::MAX),
i32::try_from(hunk.old_lines).unwrap_or(i32::MAX),
i32::try_from(hunk.new_start).unwrap_or(i32::MAX),
i32::try_from(hunk.new_lines).unwrap_or(i32::MAX),
])
.map_err(|err| CodeLoreError::Analysis(format!("append hunk: {err}")))?;
*hunk_appends += 1;
}
Ok(())
}
pub(crate) fn dedup_entities(
entities: Vec<crate::complexity::ComplexityEntity>,
) -> Vec<crate::complexity::ComplexityEntity> {
let mut seen: std::collections::HashSet<(String, u32, u32)> = std::collections::HashSet::new();
let mut out = Vec::with_capacity(entities.len());
for mut ent in entities {
let key = (ent.name.clone(), ent.start_line, ent.end_line);
if !seen.insert(key) {
continue;
}
ent.name = if ent.name.is_empty() {
format!("<anonymous>@{}-{}", ent.start_line, ent.end_line)
} else {
format!("{}@{}-{}", ent.name, ent.start_line, ent.end_line)
};
out.push(ent);
}
out
}
pub(crate) fn f64_to_i32_clamped(v: f64) -> i32 {
if v.is_finite() && v >= 0.0 {
#[allow(clippy::cast_possible_truncation)]
let clamped = v.round().min(f64::from(i32::MAX)) as i32;
clamped
} else {
0
}
}
pub(super) fn append_entity_row(
app: &mut duckdb::Appender<'_>,
path: &str,
ent: &crate::complexity::ComplexityEntity,
rev: &str,
) -> Result<()> {
use duckdb::params;
app.append_row(params![
path,
ent.name,
ent.kind,
i32::try_from(ent.start_line).unwrap_or(i32::MAX),
i32::try_from(ent.end_line).unwrap_or(i32::MAX),
rev,
rev,
])
.map_err(|e| CodeLoreError::Analysis(format!("append entity: {e}")))
}
pub(super) fn append_metric_row(
app: &mut duckdb::Appender<'_>,
path: &str,
ent: &crate::complexity::ComplexityEntity,
rev: &str,
) -> Result<()> {
use duckdb::params;
app.append_row(params![
path,
ent.name,
rev,
f64_to_i32_clamped(ent.cyclomatic),
f64_to_i32_clamped(ent.cognitive),
ent.halstead_volume,
ent.halstead_difficulty,
ent.halstead_effort,
ent.mi,
i32::try_from(ent.nom).unwrap_or(i32::MAX),
i32::try_from(ent.nexits).unwrap_or(i32::MAX),
i32::try_from(ent.loc).unwrap_or(i32::MAX),
i32::try_from(ent.sloc).unwrap_or(i32::MAX),
i32::try_from(ent.max_nesting).unwrap_or(i32::MAX),
ent.mean_nesting,
ent.sd_nesting,
i32::try_from(ent.total_nesting).unwrap_or(i32::MAX),
i32::try_from(ent.nargs).unwrap_or(i32::MAX),
i32::try_from(ent.bool_ops).unwrap_or(i32::MAX),
])
.map_err(|e| CodeLoreError::Analysis(format!("append metric: {e}")))
}